Harden a Linux Server
Secure an Ubuntu VM with user management, firewall rules, and SSH lockdown.
Introduction
30 Second Summary
Every server connected to the internet is a target. Automated bots scan for open ports, weak passwords, and default configurations within minutes of a system going live.
In this project, you will harden an Ubuntu Linux server using only the command line. You'll configure secure user accounts, set up a firewall, lock down remote access, and inspect system logs for suspicious activity.
What You'll Build
You'll transform your Ubuntu VM from its default (vulnerable) state into a hardened system following industry-standard security practices.
By the end of this project, you'll have:
- A non-root sudo user with proper access controls and password policies.
- A UFW firewall configured with a default-deny incoming policy.
- SSH hardened to reject root login and password-based authentication.
- System logs reviewed to identify suspicious activity and verify your security measures.
- Secret Mission: Write a Bash script that automates the entire hardening process so you can secure any new server in seconds.
Do I need any prior setup?
Yes. You need Parallels Desktop with an Ubuntu VM already installed and running. No other tools or accounts are required for this project.
Prepare Your Ubuntu Environment
Before you can harden a server, you need to know what you're working with. A security professional always starts by confirming the system's identity and making sure it has the latest patches installed.
In this step, you'll launch your Ubuntu VM, verify what version you're running, and update every package to close any known vulnerabilities.
In this step, get ready to:
- Launch your Ubuntu VM and open a Terminal window.
- Verify your system info and current user.
- Update all system packages to patch known vulnerabilities.
Launch your Ubuntu VM and open Terminal
Your Parallels Desktop VM gives you a full Ubuntu system to practice on. Any changes you make here stay inside the VM and won't affect your host machine.
- Open Parallels Desktop and start your Ubuntu VM.
- Once the Ubuntu desktop loads, open a Terminal window by pressing Ctrl+Alt+T.
Can't find Terminal?
If the keyboard shortcut doesn't work, click the application grid in the bottom-left corner of your Ubuntu desktop and search for Terminal.
Verify your system info and current user
Knowing your exact OS version and user identity matters for security. Different Ubuntu versions have different packages available, and understanding your user's privileges tells you what you can and can't do.
- Check your Ubuntu version by running this command:
lsb_release -a
What does this command do?
lsb_release -a prints your Linux distribution details. You'll see the distributor ID, description, release number, and codename.
You should see output showing Ubuntu as the Distributor ID, along with a release number and codename.
- Check who you're currently logged in as by running this command:
whoami
This prints your username. You should see the name of the user account you set up when you installed Ubuntu.
- Get detailed information about your user's identity and group memberships by running:
id
What does this output mean?
The id command shows your user ID (uid), primary group ID (gid), and all the groups your account belongs to. Groups determine what permissions your account has on the system.
If you see sudo in your groups list, your account can run commands with elevated (root) privileges. You'll use this throughout the project.
🙋♀️ Command not found?
If lsb_release is not found, your Ubuntu version may have removed it. Run cat /etc/os-release instead to see your system details.
Help me troubleshoot my Ubuntu Terminal commands
Update all system packages
Outdated packages are one of the most common attack vectors on Linux servers. Updating ensures that known security vulnerabilities are patched before you start hardening.
- Update the package list and upgrade all installed packages by running this command:
sudo apt update && sudo apt upgrade -y
What does this command do?
This is actually two commands chained together with && (which means "run the second only if the first succeeds").
- sudo apt update refreshes the list of available packages from Ubuntu's servers.
- sudo apt upgrade -y installs all available updates. The -y flag automatically confirms the installation so you don't have to type "yes" manually.
This may take a few minutes depending on how many updates are available. You'll see a stream of output as packages download and install.
Seeing a dpkg lock error?
If you see a message about another process holding the lock, Ubuntu may be running automatic updates in the background. Wait a minute or two and try the command again.
Help me fix my apt update error
Once the command finishes, your system is fully patched and ready for hardening.
Your system is patched and you know exactly who you're logged in as. Next up, you'll create a dedicated admin user and lock down privileges so the system follows the principle of least privilege.
Create Users and Lock Down Privileges
Your system is patched and up to date. Now it's time to address the biggest risk on any Linux server: who can do what.
Running everything as your default user with full root access is dangerous. In this step, you'll create a dedicated admin user with controlled privileges, explore how Linux tracks users and groups, and enforce password policies.
In this step, get ready to:
- Create a dedicated admin user with controlled sudo access.
- Inspect Linux user and group configuration files.
- Set password aging policies to enforce regular credential rotation.
Create a new admin user and grant sudo access
A core security practice is the principle of least privilege. Instead of doing everything as your default user, you create separate accounts with only the access they need.
- Create a new user called secadmin by running this command:
sudo adduser secadmin
What will this command ask you?
The adduser command is interactive. It will prompt you to set a password (enter it twice), then ask for optional user information like full name, room number, and phone numbers.
Choose a memorable password for secadmin. You can press Enter to skip the optional fields. When asked to confirm the information is correct, type Y and press Enter.
- Grant secadmin sudo access by adding it to the sudo group:
sudo usermod -aG sudo secadmin
What does this command do?
The -aG flag means "append to group". This adds secadmin to the sudo group without removing them from any existing groups. Members of the sudo group can run commands as root using sudo.
- Verify that secadmin was added to the correct group by running:
groups secadmin
You should see secadmin : secadmin sudo in the output, confirming the user belongs to both its own group and the sudo group.
Not seeing sudo in the groups output?
Make sure you typed the usermod command with a capital G (not lowercase g). A lowercase -g changes the primary group instead of appending to a supplementary group.
If you made an error, run the sudo usermod -aG sudo secadmin command again and re-check with groups secadmin.
Help me debug why secadmin isn't in the sudo group.
Explore user and group files
Linux stores all user account information in plain text files. Understanding these files helps you audit who has access to your system.
- View the system's user database by running:
cat /etc/passwd
What is /etc/passwd?
Each line represents one user account. The format is username:x:UID:GID:comment:home:shell. Despite the name, this file does not contain actual passwords (those live in /etc/shadow).
Scroll through the output and look for the secadmin entry near the bottom. You'll see its UID, home directory, and default shell.
- View the group database to see all groups on the system:
cat /etc/group
Look for the sudo line. You should see secadmin listed as a member.
- Get a detailed view of secadmin's UID and GID structure by running:
id secadmin
You should see the numeric UID, primary GID, and all group memberships for secadmin in a single line of output.
What do UID and GID mean?
A UID (User ID) is the numeric identifier Linux uses internally for each user. A GID (Group ID) identifies the user's primary group. The kernel uses these numbers (not usernames) to enforce permissions.
Regular users typically get UIDs starting at 1000. System services use lower UIDs. This separation helps you quickly spot human accounts versus automated ones.
Set password policies for the admin user
Passwords that never expire are a security risk. Password aging policies force regular credential rotation, limiting how long a compromised password stays valid.
- Examine the system-wide password policy defaults by running:
cat /etc/login.defs | grep PASS
What is /etc/login.defs?
This file contains system-wide defaults for password aging and account creation. The PASS_MAX_DAYS, PASS_MIN_DAYS, and PASS_WARN_AGE values set the default policy for newly created users.
These defaults apply at creation time. To set per-user policies on an existing account, you use the chage command.
- Set a password aging policy for secadmin by running:
sudo chage -m 7 -M 90 -W 14 secadmin
What do these flags mean?
Each flag controls a different aspect of the password lifecycle:
- -m 7 sets the minimum number of days between password changes to 7. This prevents users from immediately cycling back to an old password.
- -M 90 sets the maximum password age to 90 days. After 90 days, the user must change their password.
- -W 14 gives a 14-day warning before the password expires. The user sees a reminder at login.
- Verify the policy was applied by running:
sudo chage -l secadmin
You should see Minimum number of days between password change set to 7, Maximum number of days between password change set to 90, and Number of days of warning before password expires set to 14.
Seeing different values than expected?
Double-check the flags in your chage command. The -m (lowercase) is minimum days and -M (uppercase) is maximum days. Swapping them sets the wrong values.
If needed, re-run sudo chage -m 7 -M 90 -W 14 secadmin with the correct flags and verify again.
Help me fix my password policy settings for secadmin.
You've created a dedicated admin user with controlled privileges and enforced password rotation. Next up, you'll secure file permissions to protect sensitive data on your server.
Secure File Permissions
In the previous step, you created the secadmin user and configured password policies. Your server now has proper user accounts, but there is another layer of defense to apply.
Linux file permissions control exactly who can read, write, or execute every file and directory on the system. Misconfigured permissions are one of the most common security vulnerabilities on servers. In this step, you will practice setting permissions to protect sensitive data.
In this step, get ready to:
- Create a directory structure with varying permission levels.
- Set and verify file permissions using octal notation.
- Audit system file permissions for security compliance.
Create a sensitive directory structure and set permissions
A well-organized server separates files by sensitivity level. You will create three directories that simulate real-world access tiers: confidential (owner-only), shared (group-accessible), and public (world-readable).
- Create all three directories at once by running this command:
mkdir -p ~/security-lab/{confidential,shared,public}
What does this command do?
The -p flag tells mkdir to create parent directories as needed. The curly braces {confidential,shared,public} expand into three separate directory names, creating all of them inside ~/security-lab/ in one command.
- Confirm the directories exist by running:
ls ~/security-lab/
You should see confidential, shared, and public listed in the output.
- Now lock down the confidential directory so only the owner can access it by running:
chmod 700 ~/security-lab/confidential
- Set the public directory to allow everyone to read and traverse it by running:
chmod 755 ~/security-lab/public
What is octal notation?
Linux file permissions use a three-digit number where each digit represents a different group: owner, group, and others. Each digit is the sum of read (4), write (2), and execute (1).
So 700 means the owner gets 7 (read + write + execute = 4+2+1), while group and others get 0 (no access at all). And 755 means the owner gets full access (7), while group and others can read and execute (5 = 4+1) but not write.
- Verify your permission changes took effect by running:
ls -la ~/security-lab/
You should see drwx------ next to confidential (only owner access) and drwxr-xr-x next to public (readable by all).
Not seeing the expected permissions?
Make sure you typed the full path including the tilde (~). If the chmod command reported no errors but permissions look wrong, run ls -la ~/security-lab/ again and look at the first column of each row.
help me troubleshoot file permissions on my Ubuntu VM
Understand octal permissions and change ownership
Knowing how to read permission output is just as important as setting permissions. The stat command shows detailed metadata including the octal permission value directly.
- View detailed permission metadata for the confidential directory by running:
stat ~/security-lab/confidential
What does stat show?
The stat command displays the file's size, inode number, block count, and permissions in both symbolic (drwx------) and octal (0700) form. It also shows the owning user and group. This is more detailed than ls -la and is useful for confirming exact permission values.
Right now, the confidential directory is owned by your current user. On a real server, you would transfer ownership to the specific user who should control it. You will transfer it to secadmin now.
- Change ownership of the confidential directory to secadmin by running:
sudo chown secadmin:secadmin ~/security-lab/confidential
What does chown do?
The chown command (change owner) sets both the user and group owner of a file or directory. The format secadmin:secadmin sets the owner to secadmin and the group to secadmin. Combined with 700 permissions, only the secadmin user can access this directory.
- Verify the ownership change by running:
ls -la ~/security-lab/
You should see secadmin secadmin as the owner and group columns for the confidential directory.
Permission denied when running chown?
The chown command requires sudo because only root can transfer file ownership. Make sure you included sudo at the start of the command.
help me fix my chown command
Audit system file permissions
Beyond your own files, a security-conscious admin audits critical system files to make sure they have not been loosened. Two key files to check are the SSH configuration and the password shadow file.
- Check the permissions on the SSH configuration file by running:
ls -la /etc/ssh/sshd_config
You should see restrictive permissions on this file. The SSH config typically shows -rw------- (permission 600), meaning only root can read or write it. This prevents non-root users from viewing or tampering with SSH settings.
- Now check the shadow password file by running:
ls -la /etc/shadow
You should see -rw-r----- with owner root and group shadow. This means root can read and write, the shadow group can read (for system utilities), and everyone else has zero access.
Why is /etc/shadow so locked down?
The /etc/shadow file contains the hashed passwords for every user account on the system. If an attacker could read this file, they could run offline password-cracking tools against those hashes. Restrictive permissions are the first line of defense.
This is an example of the principle of least privilege in action. Each file should be accessible only to the users and processes that genuinely need it.
Your files and directories are now properly locked down. Next up, you will configure a firewall to control which network traffic can reach your server.
Configure the UFW Firewall
Your file permissions are locked down, but there's another layer of defense every server needs.
Right now, your Ubuntu VM has no firewall enabled. That means every network port is potentially open to incoming connections. In this step, you'll set up UFW (Uncomplicated Firewall) to block everything by default and only allow what you explicitly permit.
In this step, get ready to:
- Set default firewall policies to deny all incoming connections.
- Enable UFW with SSH access allowed.
- Practice adding and removing firewall rules.
Check firewall status and set default policies
UFW comes pre-installed on Ubuntu but is disabled by default. Before making changes, you need to see the current state.
- Check the current firewall status by running this command:
sudo ufw status verbose
You should see Status: inactive confirming the firewall is not yet running.
What is UFW?
UFW stands for Uncomplicated Firewall. It's a user-friendly front end for the more complex iptables firewall system built into Linux.
Instead of writing complicated iptables rules, UFW lets you manage firewall rules with simple commands like "allow" and "deny."
The most important security principle for a firewall is default deny. This means blocking all incoming traffic unless you explicitly allow it.
- Set the default policy to deny all incoming connections by running this command:
sudo ufw default deny incoming
- Set the default policy to allow all outgoing connections by running this command:
sudo ufw default allow outgoing
Why deny incoming but allow outgoing?
Denying incoming traffic stops outsiders from reaching services on your server unless you've opened a specific port.
Allowing outgoing traffic lets your server fetch updates, resolve DNS, and communicate with the internet normally. This is the standard baseline for most servers.
You should see confirmation messages after each command indicating the default policy has been updated.
Seeing a permission error?
Make sure you include sudo at the start of each command. UFW requires root privileges to modify firewall rules.
If you're prompted for a password, enter your user account password (the one you use to log in to the VM).
Help me troubleshoot UFW permission errors on my Ubuntu VM.
Allow SSH and enable the firewall
Before you turn on the firewall, you must allow SSH access through it. On a remote server, enabling a firewall without an SSH rule would lock you out permanently.
- Allow SSH connections through the firewall by running this command:
sudo ufw allow 22/tcp
What does 22/tcp mean?
Port 22 is the default port for SSH connections. The /tcp part specifies that only TCP traffic is allowed on this port. SSH uses TCP, so this is the precise rule you need.
- Activate the firewall by running this command:
sudo ufw enable
Seeing a confirmation prompt?
UFW will display: Command may disrupt existing ssh connections. Proceed with operation (y|n)?
Type y and press Enter to proceed. Since you already allowed SSH in the previous command, you won't lose access.
- Confirm the firewall is active and your rules are in place by running this command:
sudo ufw status verbose
You should see Status: active with the default policies listed (deny incoming, allow outgoing) and your SSH rule allowing port 22/tcp.
Don't see Status: active?
If the status still shows inactive, make sure you typed y when prompted during the enable step. Run sudo ufw enable again and confirm with y.
Help me troubleshoot why UFW isn't showing as active after I enabled it.
Practice adding and removing rules
Managing firewall rules is something you'll do regularly as a server admin. Let's practice adding a rule and then removing it cleanly.
- Allow HTTP traffic on port 80 by running this command:
sudo ufw allow 80/tcp
- View all rules with their numbers by running this command:
sudo ufw status numbered
You should see your rules listed with numbers in brackets. Port 22/tcp and port 80/tcp should both appear as ALLOW IN.
Why use numbered status?
The numbered view is helpful when you want to delete a specific rule by its position in the list. It shows both IPv4 and IPv6 rules.
Since this server doesn't actually run a web server, you don't need port 80 open. Let's remove it.
- Delete the HTTP rule by running this command:
sudo ufw delete allow 80/tcp
- Verify the rule was removed by running this command:
sudo ufw status verbose
You should see only the SSH rule (22/tcp) remaining. The HTTP rule is gone.
What is the principle of least privilege here?
You just practiced the principle of least privilege applied to networking. Only the services your server actually runs should have open ports.
Every unnecessary open port is a potential entry point for attackers. When in doubt, deny it.
🙋♀️ Rule didn't delete?
Make sure the delete command matches the rule exactly. If you allowed 80/tcp, you must delete allow 80/tcp (not just 80).
Help me remove a UFW firewall rule that isn't deleting properly.
Your firewall is now guarding your server with a default-deny policy. Only SSH can get through. Next up, you'll harden that SSH service itself to make it even tougher to break into.
Harden SSH Access
Your firewall is now filtering incoming traffic. The next layer of defence is SSH, the protocol that lets administrators connect to a server remotely.
A default SSH configuration allows root login and password-based authentication, both of which are prime targets for automated brute-force attacks. In this step, you'll lock those down so only authorised key-based users can connect.
In this step, get ready to:
- View the current SSH configuration and back it up safely.
- Edit the SSH config to disable root login, password authentication, and limit login attempts.
- Validate and restart the SSH service to apply your hardening.
Check if Openssh Server is installed
Run the following command to check if openssh server is currently installed.
- dpkg -l | grep openssh
If only openssh-client is listed, run the following commands to installed openssh-server
-sudo apt update && sudo apt install openssh-server -i
View the current SSH configuration and create a backup
Before changing any system file, you need to understand what's currently set and keep a copy of the original in case something goes wrong.
- View only the active (uncommented) SSH settings by running this command:
cat /etc/ssh/sshd_config | grep -v "^#" | grep -v "^$"
What does this command do?
This command reads the SSH configuration file and filters out comment lines (starting with #) and blank lines. What remains are the settings your SSH server is actually using right now.
You should see a short list of settings. Look for lines mentioning PermitRootLogin and PasswordAuthentication. These are the ones you'll change shortly.
- Create a backup of the original configuration file by running:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
- Confirm the backup exists by running:
ls -la /etc/ssh/sshd_config.bak
You should see the backup file listed with today's date and a file size matching the original.
Permission denied?
Make sure you included sudo at the start of the cp command. The SSH config directory is owned by root, so copying into it requires elevated privileges.
Help me fix a permission denied error when backing up sshd_config
Edit the SSH configuration file
Now that you have a backup, you'll modify three critical settings that control who can log in and how.
- Open the SSH configuration file in nano by running:
sudo nano /etc/ssh/sshd_config
The file opens in the nano text editor. You'll search for three settings and change each one.
- Press Ctrl+W to open the search bar.
- Type PermitRootLogin and press Enter.
- Change that entire line to read exactly:
PermitRootLogin no
What if the line starts with a # symbol?
A # at the start means the line is commented out (inactive). Remove the # and any space after it, then set the value. For example, change #PermitRootLogin yes to PermitRootLogin no.
- Press Ctrl+W again to search.
- Type PasswordAuthentication and press Enter.
- Change that entire line to read exactly:
PasswordAuthentication no
- Press Ctrl+W one more time.
- Type MaxAuthTries and press Enter.
- Change that entire line to read exactly:
MaxAuthTries 3
- Save the file by pressing Ctrl+O, then press Enter to confirm the filename.
- Exit nano by pressing Ctrl+X.
What do these three settings do?
PermitRootLogin no prevents anyone from logging in directly as the root user over SSH. Attackers always try root first because it has unlimited power.
PasswordAuthentication no forces all logins to use SSH keys instead of passwords. Keys are exponentially harder to brute-force than passwords.
MaxAuthTries 3 disconnects any client that fails to authenticate within 3 attempts. This slows down automated attack tools.
- Verify your edits saved correctly by running:
cat /etc/ssh/sshd_config | grep -v "^#" | grep -v "^$"
You should see PermitRootLogin no, PasswordAuthentication no, and MaxAuthTries 3 in the output.
Don't see your changes in the output?
If you see the old values, nano may not have saved properly. Re-open the file with sudo nano /etc/ssh/sshd_config and check that your edits are present. Make sure you pressed Ctrl+O then Enter to save before exiting.
If a setting appears twice (once commented, once not), delete the commented line so only your active setting remains.
Help me fix my sshd_config edits that aren't showing up
Validate and restart the SSH service
A typo in the SSH config can lock you out of the server entirely. Always validate the configuration before restarting the service.
- Check that your configuration has no syntax errors by running:
sudo sshd -t
What should I see?
If your configuration is valid, this command produces no output at all. No news is good news. If there's a problem, you'll see an error message pointing to the specific line number.
- Apply your changes by restarting the SSH service:
sudo systemctl restart sshd
- Verify the service restarted successfully by running:
sudo systemctl status sshd
You should see active (running) in green text. This confirms your hardened SSH configuration is live.
What does systemctl do?
systemctl is the command for managing services on systemd-based Linux systems. You use restart to stop and start a service (applying new config), and status to check whether it's running.
🙋♀️ Seeing an error when validating or restarting?
If sudo sshd -t reports an error, it will include the line number. Open the config again with sudo nano /etc/ssh/sshd_config and check that line for typos or extra spaces.
If the service fails to start, restore your backup with sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config and try your edits again.
Help me fix my SSH service that won't restart after editing the config
Your SSH access is now hardened. Remote attackers can no longer brute-force passwords or log in as root. Next up, you'll learn to read system logs so you can spot suspicious activity and verify that your security measures are working.
Monitor System Logs and Detect Threats
Your server is now locked down with user controls, file permissions, a firewall, and SSH hardening. But security is not a set-and-forget task.
Attackers probe servers constantly, and your best early-warning system is the logs your system generates. In this step, you will learn to read and search system logs to identify suspicious activity and verify your security measures are working.
In this step, get ready to:
- Explore the log directory and read authentication logs.
- Use journalctl to search for SSH-related events and failed login attempts.
- Audit listening services and ports to verify only expected services are active.
Explore the log directory and authentication log
Linux stores all system logs in a central directory. Every login attempt, service restart, and security event gets recorded here automatically.
- List all available log files by running this command:
ls /var/log/
You should see a list of files including auth.log, syslog, kern.log, and others.
What are these log files?
Each file records a different category of system activity. auth.log tracks authentication events (logins, sudo usage, SSH connections). syslog captures general system messages.
For security monitoring, auth.log is the most important file. Every failed password attempt and every sudo command appears here.
- Read the last 20 lines of the authentication log by running:
sudo cat /var/log/auth.log | tail -20
You should see recent authentication events, including your own sudo commands from earlier in this project.
What does this output show?
Each line has a timestamp, hostname, process name, and event description. Look for entries mentioning sudo (your privileged commands), sshd (SSH daemon activity), or pam_unix (authentication module events).
On a production server, you would look for unfamiliar usernames, repeated failed attempts, or login times outside normal hours.
🙋♀️ Don't see auth.log?
Some Ubuntu versions use journalctl exclusively and may not have a traditional auth.log file. If the file does not exist, skip to the next substep where you will use journalctl instead.
Help me find authentication logs on my Ubuntu VM
Use journalctl to view SSH events
The journalctl command queries the systemd journal. It provides powerful filtering so you can isolate events from a specific service and time window.
- View SSH-related events from the last hour by running:
sudo journalctl -u ssh --since "1 hour ago"
What does this command do?
The -u ssh flag filters the journal to show only messages from the SSH service unit. The --since "1 hour ago" flag limits output to recent events.
You should see entries related to your SSH restart from the previous step. On Ubuntu, the SSH service is named ssh in the journal.
- Search specifically for failed login attempts by running:
sudo journalctl -u ssh | grep -i "failed"
If no one has attempted to log in, this may return no results. That is expected on a fresh VM.
Why search for failed attempts?
On a real server exposed to the internet, you would see hundreds or thousands of failed SSH login attempts daily. These are automated brute-force attacks trying common username/password combinations.
Your SSH hardening from the previous step (disabling password authentication and limiting MaxAuthTries to 3) makes these attacks far less effective.
🙋♀️ Seeing 'No entries' or empty output?
On some Ubuntu versions, journalctl -u ssh may return no entries if the service name alias is not resolved. Try running sudo journalctl -u ssh.service instead.
If you still see no output, it means no SSH events have been logged in the requested time window. The SSH restart from the previous step should have generated at least one entry.
Help me find SSH logs on my Ubuntu VM
Check listening services and ports
The final check in any security audit is verifying which services are listening for incoming connections. Any unexpected open port could be a backdoor or an unneeded service expanding your attack surface.
- List all listening TCP ports and the processes using them by running:
sudo ss -tlnp
What does this command do?
The ss command dumps socket statistics. The flags break down as follows:
- -t shows TCP sockets only.
- -l displays only listening sockets (not established connections).
- -n shows numeric port numbers instead of resolving service names.
- -p reveals the process name and PID using each socket.
You should see port 22 listed with sshd as the owning process. This is the only port your UFW firewall allows through.
What if I see other ports?
You might see additional services listening on localhost (127.0.0.1). These are internal-only and not accessible from the network. The key concern is services listening on 0.0.0.0 (all interfaces), which are exposed to incoming connections.
If you spot an unexpected service on 0.0.0.0, you would investigate it with systemctl status and potentially disable it if not needed.
🙋♀️ Command not found?
The ss command is part of the iproute2 package, which is installed by default on Ubuntu. If it is missing, install it by running sudo apt install iproute2.
Help me check listening ports on my Ubuntu VM
Secret mission
Automate the Hardening with a Bash Script
You just hardened your server manually. But what if you need to do it again on a new machine? In this secret mission, write a reusable Bash script that performs every hardening step automatically so you can secure any new Ubuntu server in seconds.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely inside your Ubuntu VM in Parallels Desktop, so there are no ongoing cloud costs. Decide whether to keep your hardened configuration in place, revert the security changes, or remove the project files entirely.
Resources you used:
- User account secadmin with sudo access and password policy.
- UFW firewall (enabled with default-deny incoming, allow 22/tcp).
- SSH hardening changes in /etc/ssh/sshd_config.
- Directory ~/security-lab/ with subdirectories.
- Bash script ~/harden.sh.
Keep everything running
No action needed. Choose this if you want to keep your hardened VM as a reference or continue practising Linux security commands.
- Your UFW firewall stays active with default-deny incoming.
- SSH remains hardened (root login disabled, password auth off).
- The secadmin user and ~/harden.sh script remain available for future use.
Pause - I'll come back to this later
Shut down your Ubuntu VM to free up system memory and CPU. All your changes persist on disk until you boot the VM again.
- In your Ubuntu Terminal, shut down the VM gracefully by running:
- Alternatively, in Parallels Desktop, click the Actions menu and select Shut Down.
sudo shutdown now
When you return, start the VM from Parallels Desktop and all your hardening is still in place.
Delete - I don't want to use this again
Remove all project files and revert the security changes to return your VM to its pre-project state.
- Remove the project files by running:
rm -rf ~/security-lab/
rm ~/harden.sh
- Delete the secadmin user and their home directory by running:
sudo userdel -r secadmin
- Disable the UFW firewall by running:
sudo ufw disable
- Restore the original SSH configuration from your backup by running:
sudo cp /etc/ssh/sshd_config.bak /etc/ssh/sshd_config
sudo systemctl restart sshd
- Remove the SSH backup file by running:
sudo rm /etc/ssh/sshd_config.bak
Your VM is now back to its default state with no hardening applied.
Nice Work!
Nice Work!
You did it. You took a default Ubuntu server and transformed it into a hardened system using industry-standard security practices, all from the command line.
You've learned how to:
- Create and manage user accounts with proper privileges using adduser, usermod, chmod, and chage.
- Configure a UFW firewall with a default-deny incoming policy and explicit allow rules for only the services you need.
- Harden SSH access by editing sshd_config and inspect system logs with journalctl and ss to detect threats.
- Secret Mission: Automated the entire hardening process with a reusable Bash script.
Ready to quiz yourself?