Harden a Linux Server with CIS Controls
Harden an Ubuntu Server using CIS-aligned controls and measure improvement with Lynis.
Introduction
30 Second Summary
Every server connected to the internet faces a constant barrage of automated attacks. Within minutes of going live, bots scan for weak passwords, open ports, and unpatched software.
In this project, you will take a fresh Ubuntu Server and systematically harden it against the most common attack vectors. You will measure your progress with a security auditing tool that gives you a concrete score before and after your changes.
What You'll Build
A hardened Ubuntu Server 24.04 VM with CIS Benchmarks Level 1 aligned security controls, measured by a before-and-after Lynis security audit.
By the end of this project, you'll have:
- SSH key-based authentication with password and root login disabled, following the exact lockdown used on production cloud instances.
- A default-deny firewall with rate-limited SSH using UFW, plus kernel network hardening via sysctl parameters.
- Automated security patching and intrusion prevention with fail2ban protecting against brute-force attacks.
- Secret Mission: Add auditd with CIS-aligned rules to track privilege escalation and critical file changes.
Are there any prerequisites?
This project assumes intermediate Linux command-line skills. You should be comfortable with sudo, editing config files, and using systemctl to manage services.
Set Up Ubuntu Server VM and Baseline Audit
Every hardening workflow begins with two things: a clean system to harden and a baseline measurement of its current security posture. Without a baseline, you have no way to prove your changes actually improved security.
In this step, you will create a fresh Ubuntu Server VM, install the Lynis security auditing tool, and run your first scan. The resulting hardening index gives you a concrete number to improve against in later steps.
In this step, get ready to:
- Create an Ubuntu Server 24.04 LTS VM in virt-manager.
- Install Lynis and run a baseline security audit.
- Review the findings to build a remediation roadmap.
Create a fresh Ubuntu Server 24.04 LTS VM
You need an isolated environment to harden without risking your host system. A KVM virtual machine gives you a disposable sandbox that mirrors a real production server.
Already have an Ubuntu Server 24.04 VM?
If you already have a fresh Ubuntu Server 24.04 LTS VM with OpenSSH enabled, you can skip ahead to the Lynis installation substep. The VM should have at least 2 GB RAM and 20 GB disk.
- Open virt-manager by pressing the Super key, typing Virtual Machine Manager, and pressing Enter.
- Click File in the menu bar, then click New Virtual Machine.
- Select Local install media (ISO image or CDROM) and click Forward.
- Click Browse... and then Browse Local to locate your ubuntu-24.04.4-live-server-amd64.iso file.
- Select the ISO and click Forward.
- Set Memory to 2048 MB and CPUs to 1.
- Click Forward.
- Set the disk size to 20 GB and click Forward.
- Give the VM a recognizable name (such as ubuntu-hardening) and click Finish.
The Ubuntu Server installer launches in a new console window. Walk through the installer screens with these settings:
- Accept the default language and keyboard layout.
- Leave networking on the default (DHCP).
- On the storage screen, select Use an entire disk and confirm.
- Enter a username, hostname (such as hardening-lab), and password.
- On the SSH Setup screen, press Space to check Install OpenSSH server, then select Done.
- Skip the Featured Snaps screen by selecting Done.
- Wait for the installation to complete, then select Reboot Now.
After the reboot, log in with the username and password you set during installation.
What is KVM?
KVM (Kernel-based Virtual Machine) is the native Linux hypervisor built directly into the kernel. It is the same technology that powers AWS (Nitro) and Google Cloud infrastructure.
virt-manager is a graphical front-end that makes creating and managing KVM virtual machines straightforward without memorizing command-line XML syntax.
VM not booting or installer not appearing?
Make sure virtualization is enabled in your BIOS/UEFI settings (look for VT-x or AMD-V). Also confirm that the libvirtd service is running on your host.
Check the service status by running:
systemctl status libvirtd
If it is not active, start it with:
sudo systemctl enable --now libvirtd
help me troubleshoot my virt-manager VM not booting
Install Lynis and run the baseline audit
Lynis is a security auditing tool that scans your system against hundreds of hardening checks. It produces a numerical hardening index between 0 and 100 that tells you exactly how secure your system is right now.
- Update the package lists and install Lynis by running these commands:
# Update package lists and install the Lynis security auditor
sudo apt update
sudo apt install lynis -y
What does this command do?
The apt update command refreshes the list of available packages from the Ubuntu repository. The apt install lynis command downloads and installs the Lynis security auditing tool.
- Verify Lynis installed correctly by checking the version:
lynis --version
You should see a version number in the output.
- Run the full system audit by running this command:
# Run a full security audit of the system
sudo lynis audit system
What does this audit do?
Lynis checks hundreds of security controls across your system: file permissions, kernel parameters, network configuration, authentication settings, installed software, and more. At the end, it produces a hardening index score and a list of specific suggestions for improvement.
The audit takes a few minutes to complete. You will see sections scroll by as Lynis checks each category.
When the audit finishes, look for the Hardening index line near the bottom of the output. On a fresh Ubuntu Server 24.04 install, expect a score between 55 and 60.
Audit seems stuck or takes too long?
If the audit pauses and waits for input at certain checks, run it with the quick flag to skip interactive prompts:
sudo lynis audit system --quick
help me troubleshoot Lynis audit hanging
Review the Lynis output and build your remediation roadmap
The audit output is your to-do list for the rest of this project. Instead of guessing what to fix, Lynis tells you exactly which controls are missing.
- Scroll through the Lynis output and look for the Suggestions section near the end.
- Note down 3-5 key findings from the suggestions list.
On a fresh install, you will typically see suggestions in these categories:
- SSH configuration needs hardening (password authentication enabled, root login allowed).
- No firewall is configured (all ports are exposed).
- Kernel network parameters are not hardened (IP forwarding, ICMP redirects, SYN cookies).
- No intrusion prevention tool is installed.
- Automatic security updates are not configured.
Why audit before you harden?
This audit-first approach mirrors the workflow enterprise security teams use. You establish a measurable baseline, apply targeted remediations, then re-audit to prove improvement.
This pattern is called the audit-remediate-reaudit cycle. It is how organizations demonstrate compliance with standards like CIS Benchmarks.
- Record your hardening index score somewhere you can reference later (a text file, a note, or just remember the number).
You will compare this baseline score to your final score after completing all hardening steps.
Your baseline is set and your roadmap is clear. Next up, you will tackle the single highest-impact hardening control: locking down SSH access with key-based authentication.
Harden SSH Access
Your Lynis audit identified SSH hardening as a top priority. That makes sense. SSH is the single most targeted service on any internet-facing server, and a default configuration leaves the door wide open to credential-stuffing attacks.
In this step, you will lock down SSH access by switching to key-based authentication and restricting the cryptographic algorithms your server accepts. This eliminates the most common attack vector with zero impact on functionality.
In this step, get ready to:
- Generate an Ed25519 SSH key pair and install it on your server.
- Disable password authentication and root login in sshd_config.
- Configure strong cryptographic algorithms, idle timeouts, and authentication limits.
Generate an Ed25519 SSH key pair and copy it to the server
Before you change any SSH settings, you need a working key-based login as a fallback. If you disable password authentication without a working key, you will lock yourself out of the server.
- On your host machine (not the server VM), generate an Ed25519 key pair by running this command:
ssh-keygen -t ed25519
What does this command do?
Ed25519 is a modern elliptic-curve signature scheme. It produces smaller, faster keys that are more secure than RSA or ECDSA.
The command creates two files in ~/.ssh/: a private key (id_ed25519) and a public key (id_ed25519.pub).
- When prompted for a file location, press Enter to accept the default path.
- When prompted for a passphrase, enter a strong passphrase or press Enter to skip (for this lab environment, skipping is acceptable).
- Copy your public key to the server by running this command:
ssh-copy-id [[SERVER_USER="your-username"]]@[[SERVER_IP="your-server-ip-address"]]
What does ssh-copy-id do?
This command appends your public key to the ~/.ssh/authorized_keys file on the server. Once the key is in place, the server can verify your identity without a password.
- When prompted, enter your server user's password to complete the copy.
- Verify that key-based login works by connecting without a password:
ssh [[SERVER_USER="your-username"]]@[[SERVER_IP="your-server-ip-address"]]
- You should be logged in without being prompted for a password (or prompted only for your key passphrase if you set one).
Still being asked for a password?
- Check that ~/.ssh/authorized_keys on the server contains your public key by running cat ~/.ssh/authorized_keys on the server.
- Verify that the .ssh directory permissions are 700 and authorized_keys permissions are 600.
- Run ssh -v to see verbose connection output and identify where authentication fails.
Help me troubleshoot my SSH key-based login. I ran ssh-copy-id but the server still asks for a password.
Disable password authentication and root login
Now that key-based login is confirmed working, you can safely disable the weaker authentication methods. This prevents brute-force password attacks entirely.
- On the server, open the SSH daemon configuration file by running:
sudo nano /etc/ssh/sshd_config
- Find the line that contains #PasswordAuthentication yes (it may be commented out with a #).
- Replace it with:
PasswordAuthentication no
- Find the line that contains #PermitRootLogin and replace it with:
PermitRootLogin no
- Add the following CIS-aligned cryptographic algorithm restrictions to the file. Find the section with # Ciphers and keying and add these lines below it:
# Strong key exchange algorithms (CIS 5.2.16)
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
# Strong ciphers only (CIS 5.2.14)
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
# Strong MACs only (CIS 5.2.15)
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
What do these algorithm settings do?
- KexAlgorithms restricts key exchange to modern algorithms. Curve25519 is preferred for speed and security.
- Ciphers limits encryption to authenticated ciphers (ChaCha20-Poly1305 and AES-GCM) with AES-CTR as a compatibility fallback. No CBC mode ciphers are allowed.
- MACs restricts message authentication to SHA-2 family only. The -etm (encrypt-then-MAC) variants are preferred because they prevent padding oracle attacks.
OpenSSH 9.6 on Ubuntu 24.04 only supports protocol 2 (protocol 1 was removed years ago), so no explicit Protocol directive is needed.
- Save the file by pressing Ctrl+O, then Enter, then Ctrl+X to exit nano.
- Validate your configuration syntax before restarting by running:
sudo sshd -t
- If no output appears, the configuration is valid.
- Restart the SSH daemon to apply changes:
sudo systemctl restart ssh
- Open a new terminal window on your host and verify you can still connect with your key:
ssh [[SERVER_USER="your-username"]]@[[SERVER_IP="your-server-ip-address"]]
Do not close your existing SSH session yet
Always test SSH changes from a new connection while keeping your existing session open. If the new connection fails, you can still use the old session to fix the configuration.
- Verify that password authentication is rejected by attempting to connect with password only:
ssh -o PubkeyAuthentication=no [[SERVER_USER="your-username"]]@[[SERVER_IP="your-server-ip-address"]]
- You should see a Permission denied error. This confirms password authentication is disabled.
Locked out of the server?
- Access the VM directly through the virt-manager console (click the VM, then Open). This bypasses SSH entirely.
- Fix the /etc/ssh/sshd_config file and restart sshd from the console.
- Ensure PubkeyAuthentication yes is set (it should be by default).
Help me regain access to my server after an SSH lockout.
Set idle timeout and authentication limits
Idle sessions and unlimited login attempts give attackers more opportunities. These settings automatically disconnect inactive sessions and limit brute-force attempts per connection.
- Open /etc/ssh/sshd_config again:
sudo nano /etc/ssh/sshd_config
- Add the following lines at the end of the file:
# Disconnect idle sessions after 10 minutes (300s x 2 missed keepalives)
ClientAliveInterval 300
ClientAliveCountMax 2
# Limit authentication attempts per connection
MaxAuthTries 3
What do these settings do?
- ClientAliveInterval 300 sends a keepalive message every 300 seconds (5 minutes) to check if the client is still active.
- ClientAliveCountMax 2 disconnects after 2 missed keepalives. Combined with the interval, this means an idle session is closed after 10 minutes.
- MaxAuthTries 3 limits each connection to 3 authentication attempts. This slows down brute-force attacks that cycle through passwords on a single connection.
- Save the file (Ctrl+O, Enter, Ctrl+X).
- Validate and restart sshd:
sudo sshd -t && sudo systemctl restart ssh
- Verify the settings are active by running:
sudo sshd -T | grep -E "(passwordauthentication|permitrootlogin|maxauthtries|clientaliveinterval|clientalivecountmax)"
- You should see output confirming: passwordauthentication no, permitrootlogin no, maxauthtries 3, clientaliveinterval 300, and clientalivecountmax 2.
Seeing a configuration error?
- If sshd -t reports an error, check for typos in directive names. They are case-sensitive.
- Ensure there are no duplicate directives. If the same directive appears twice, only the first occurrence takes effect.
- Check that you did not accidentally remove the Include /etc/ssh/sshd_config.d/*.conf line at the top of the file.
Help me fix an sshd configuration error on Ubuntu 24.04.
✔️ Awesome, I've got everything!
Great. Double check you have saved /etc/ssh/sshd_config and that sshd is running without errors.
ⓧ I'd like to double check the full code
Your /etc/ssh/sshd_config should contain these hardening directives (in addition to the default content):
# Authentication hardening
PasswordAuthentication no
PermitRootLogin no
# Strong key exchange algorithms (CIS 5.2.16)
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256
# Strong ciphers only (CIS 5.2.14)
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
# Strong MACs only (CIS 5.2.15)
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
# Disconnect idle sessions after 10 minutes (300s x 2 missed keepalives)
ClientAliveInterval 300
ClientAliveCountMax 2
# Limit authentication attempts per connection
MaxAuthTries 3
Your SSH service is now hardened against the most common attack vectors. Next up, you will configure a default-deny firewall and harden the kernel network stack.
Configure Firewall and Kernel Network Hardening
Your SSH access is now locked down with key-only authentication and strict session controls. The next layer of defense is reducing the server's network exposure.
A fresh Ubuntu Server listens on every port by default and accepts any network traffic the kernel receives. In this step, you will configure a default-deny firewall that blocks everything except SSH, then harden the kernel's network stack to resist common attacks like IP spoofing and SYN floods.
In this step, get ready to:
- Configure UFW with a default-deny incoming policy and rate-limited SSH.
- Apply CIS-aligned kernel network hardening parameters via sysctl.
- Verify the firewall rules and confirm the sysctl values are active.
Configure UFW with default-deny and rate-limited SSH
A default-deny firewall policy means all incoming traffic is blocked unless you create an explicit exception. This is the foundation of the defense in depth approach.
- Set the default policy to deny all incoming connections by running this command:
sudo ufw default deny incoming
- Allow all outgoing connections by running:
sudo ufw default allow outgoing
- Allow SSH access with rate limiting by running:
sudo ufw limit ssh/tcp
What does rate limiting do?
The limit rule automatically denies connections from any IP that attempts 6 or more connections within 30 seconds. This provides basic brute-force protection at the firewall level before traffic even reaches sshd.
- Enable firewall logging by running:
sudo ufw logging on
- Enable the firewall by running:
sudo ufw enable
Will this disconnect my SSH session?
UFW will prompt you to confirm before enabling because enabling the firewall flushes existing chains. Since you already added the SSH limit rule above, your connection will remain active after you confirm. Type y and press Enter at the prompt.
- Verify the firewall is active and rules are correct by running:
sudo ufw status verbose
- You should see output showing Status: active, a default deny incoming policy, default allow outgoing policy, and port 22/tcp listed with LIMIT IN.
Locked out of SSH?
If you accidentally enabled UFW without the SSH rule, connect to the VM directly through the virt-manager console (not SSH) and run sudo ufw allow ssh/tcp followed by sudo ufw reload.
If UFW status shows inactive, make sure you typed y at the confirmation prompt. Help me troubleshoot my UFW configuration
Apply CIS-aligned kernel network hardening via sysctl
The Linux kernel has network parameters that control how your server handles packets. By default, many of these are permissive for compatibility.
The CIS Benchmarks recommend tightening these parameters to prevent IP spoofing, redirect attacks, and SYN floods. You will store these settings in a persistent configuration file so they survive reboots.
- Create the sysctl hardening configuration file by running:
sudo tee /etc/sysctl.d/99-hardening.conf << 'EOF'
net.ipv4.ip_forward=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.tcp_syncookies=1
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
EOF
What do these parameters do?
- net.ipv4.ip_forward=0 disables packet forwarding. Your server is not a router.
- net.ipv4.conf.all.accept_redirects=0 and the default variant prevent ICMP redirect attacks that could reroute your traffic.
- net.ipv4.conf.all.send_redirects=0 stops your server from sending ICMP redirects to other hosts.
- net.ipv4.tcp_syncookies=1 protects against SYN flood denial-of-service attacks.
- net.ipv4.conf.all.log_martians=1 logs packets with impossible source addresses, helping you detect spoofing attempts.
- net.ipv4.conf.all.accept_source_route=0 and the default variant prevent attackers from specifying the route packets take through the network.
- Apply the new kernel parameters immediately by running:
sudo sysctl -p /etc/sysctl.d/99-hardening.conf
- You should see each parameter echoed back with its new value, confirming the changes are active.
Seeing 'No such file or directory'?
Make sure you typed the filename exactly as 99-hardening.conf and that the tee command completed without errors. You can verify the file exists by running ls /etc/sysctl.d/99-hardening.conf.
Help me troubleshoot my sysctl configuration
Verify the firewall and confirm sysctl values
Now confirm both layers of network hardening are working as expected.
- Check the full firewall status one more time by running:
sudo ufw status verbose
- Confirm the output shows default deny (incoming), default allow (outgoing), and SSH on port 22 with LIMIT IN.
- Verify the sysctl parameters are active by running:
sysctl net.ipv4.ip_forward net.ipv4.conf.all.accept_redirects net.ipv4.tcp_syncookies net.ipv4.conf.all.log_martians net.ipv4.conf.all.accept_source_route
- You should see net.ipv4.ip_forward = 0, net.ipv4.conf.all.accept_redirects = 0, net.ipv4.tcp_syncookies = 1, net.ipv4.conf.all.log_martians = 1, and net.ipv4.conf.all.accept_source_route = 0.
- From your host machine, run a port scan to confirm only SSH is open by running:
nmap -sT [[SERVER_IP="your server's IP address"]]
- The scan should show port 22 (SSH) open and all other ports filtered or closed.
No nmap installed?
If nmap is not installed on your host, install it with sudo apt install nmap. Alternatively, you can check listening ports from the server itself using ss -tlnp to confirm only sshd is listening.
Why is a default-deny policy so important?
A default-deny firewall ensures that any new service you install is blocked from the network until you explicitly allow it. Without this, a misconfigured application could accidentally expose a listening port to the internet.
Combined with the kernel hardening parameters, your server now ignores spoofed packets, resists SYN floods, and refuses routing tricks. These two layers work together to minimize your attack surface.
✔️ Awesome, I've got everything!
Great. Double check that you have saved your sysctl file and that UFW is enabled.
ⓧ I'd like to double check the full code
Here is the complete /etc/sysctl.d/99-hardening.conf file:
net.ipv4.ip_forward=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.tcp_syncookies=1
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.accept_source_route=0
Your UFW rules should look like this when you run sudo ufw status verbose:
- Default: deny (incoming), allow (outgoing), disabled (routed).
- 22/tcp with LIMIT IN from Anywhere.
Your server now has two strong layers of network protection. Next up, you will configure automated security patching and reduce the server's service footprint to minimize what an attacker can target.
Enable Automated Patching and Reduce Attack Surface
Your server's network perimeter is locked down with SSH hardening, a default-deny firewall, and kernel-level protections. But a hardened server with unpatched software is still vulnerable.
In this step, you will enable automated security patching so your server receives critical fixes without manual intervention. You will also audit running services and shut down anything unnecessary to shrink the server's attack surface.
In this step, get ready to:
- Install and configure unattended-upgrades for automatic security updates.
- Audit running services and disable unnecessary daemons.
- Verify only essential services are listening on network ports.
Install and configure unattended-upgrades
Unpatched vulnerabilities are one of the most exploited attack vectors on production servers. Automated security patching ensures critical fixes land within hours of release, not weeks.
- On Ubuntu Server 24.04, the unattended-upgrades package is installed by default. Confirm it is present and ensure the configuration is active by running:
sudo apt install unattended-upgrades
What does this command do?
If unattended-upgrades is already installed (the default on Ubuntu 24.04 Server), apt confirms it is at the latest version and does nothing else.
If the package was somehow removed, this reinstalls it and creates the default configuration files.
- Verify that automatic updates are enabled by checking the auto-upgrades configuration file:
cat /etc/apt/apt.conf.d/20auto-upgrades
- You should see both values set to "1", which means the system refreshes package lists and applies unattended upgrades daily:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
What do these settings mean?
APT::Periodic::Update-Package-Lists "1" tells the system to refresh its list of available packages once per day.
APT::Periodic::Unattended-Upgrade "1" tells the system to apply available unattended upgrades once per day. Together they form a daily automatic patching cycle driven by apt-daily.timer and apt-daily-upgrade.timer.
- Now verify that the upgrade origins are limited to security updates only by inspecting the main configuration file:
grep -A 10 'Allowed-Origins' /etc/apt/apt.conf.d/50unattended-upgrades
- Confirm that the Allowed-Origins section includes the security origins and that -updates, -proposed, and -backports lines remain commented out. The active origins should look like this:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
// "${distro_id}:${distro_codename}-updates";
// "${distro_id}:${distro_codename}-proposed";
// "${distro_id}:${distro_codename}-backports";
};
Why only security origins?
By keeping -updates commented out, the server only applies security patches automatically. Feature updates and non-security bug fixes remain manual, which avoids unexpected behaviour changes on a production system.
This is the CIS Benchmark recommended approach: patch vulnerabilities automatically while keeping everything else under controlled change management.
- Run a dry run to confirm unattended-upgrades can process correctly:
sudo unattended-upgrade --dry-run
- The command should complete without errors. It simulates an upgrade run without actually installing anything.
Seeing an error about missing configuration?
If /etc/apt/apt.conf.d/20auto-upgrades does not exist or shows "0" values, run sudo dpkg-reconfigure unattended-upgrades and select Yes when prompted to enable automatic updates.
help me troubleshoot unattended-upgrades configuration issues on my Ubuntu Server
Audit running services and disable unnecessary daemons
Every running service is a potential attack vector. A fresh Ubuntu Server installation often includes services designed for desktop use or hardware that a server does not need.
- List all active services to see what is currently running on your server:
systemctl list-units --type=service --state=running
What am I looking for?
Scan the output for services you do not need on a headless server. Common unnecessary services on Ubuntu Server include avahi-daemon (mDNS discovery for local networks), cups (printing), and ModemManager (cellular modem management).
Your specific output will vary depending on your installation. Not all of these services may be present.
- Disable and stop any unnecessary services that are present on your system. For each service you find, run:
# Disable avahi-daemon if present (mDNS/DNS-SD discovery - not needed on servers)
sudo systemctl disable --now avahi-daemon
# Disable cups if present (printing service - not needed on servers)
sudo systemctl disable --now cups
# Disable ModemManager if present (cellular modem management)
sudo systemctl disable --now ModemManager
What does this command do?
The --now flag combines two operations: it stops the service immediately AND disables it from starting at boot. Without --now, the service would only be disabled on the next reboot.
If a service is not installed on your system, you will see a message saying the unit was not found. That is fine and can be ignored.
- Remove the packages for any services you disabled to prevent them from being reactivated by future updates:
# Remove packages for disabled services (only those that were present)
sudo apt purge avahi-daemon cups ModemManager -y
# Clean up any orphaned dependencies
sudo apt autoremove -y
Why purge instead of just disabling?
Disabling a service stops it from running, but the software remains installed. A future package update could re-enable it. Purging removes the package and its configuration files entirely, eliminating the attack surface permanently.
- Verify by re-listing running services:
systemctl list-units --type=service --state=running
- You should no longer see avahi-daemon, cups, or ModemManager in the output.
Service still showing as active?
Some services have associated socket units that can restart them. Check for lingering socket or timer units with systemctl list-units --type=socket and disable any related ones (e.g., cups.socket).
help me figure out why a disabled service is still running after I used systemctl disable --now
Verify only essential services are listening on network ports
The final check confirms that only the services you intentionally kept are exposed to the network. Any unexpected listener is a potential entry point for an attacker.
- List all services currently listening on TCP ports by running:
sudo ss -tlnp
What does this command show?
The ss command shows socket statistics. The flags mean: -t (TCP only), -l (listening sockets), -n (numeric ports, no DNS resolution), -p (show the process using each socket).
On a properly hardened server, you should see only sshd listening on port 22 and possibly systemd-resolved on port 53 (for local DNS resolution). Anything else warrants investigation.
- Confirm that your output shows only essential services. A healthy result looks like sshd on port 22 and systemd-resolved on 53 (bound to 127.0.0.53, meaning local only).
Seeing unexpected services listening?
If you see a service you do not recognise, identify it with systemctl status <service-name> and then disable it with the same systemctl disable --now pattern from the previous substep.
help me identify and disable an unknown service listening on a network port on my Ubuntu Server
Your server now patches itself automatically and runs only the services it needs. Next up, you will add intrusion prevention with fail2ban and run a final Lynis audit to measure your hardening progress.
Deploy Intrusion Prevention and Re-Audit
Your server now has hardened SSH, a locked-down firewall, kernel-level network protections, and automated patching. One attack vector remains unaddressed: persistent brute-force attempts against your SSH service.
In this step, you will deploy fail2ban to automatically ban IPs that repeatedly fail authentication. Then you will re-run Lynis to measure how much your hardening index has improved over the baseline.
In this step, get ready to:
- Install and configure fail2ban with a minimal sshd jail.
- Verify the jail is active and test brute-force protection.
- Re-run the Lynis audit and document your score improvement.
Install and configure fail2ban
fail2ban monitors your system logs for repeated authentication failures and automatically bans offending IP addresses using your firewall. On Ubuntu 24.04, sshd logs to the systemd journal rather than a traditional log file, so the jail configuration must specify the systemd backend explicitly.
- Install fail2ban by running this command:
sudo apt install fail2ban
Why a minimal jail.local instead of copying jail.conf?
On Ubuntu 24.04, copying the full jail.conf to jail.local causes path-resolution conflicts that break the sshd jail. The correct approach is to create a minimal jail.local containing only the overrides you need.
- Create the jail configuration file with only the sshd overrides by running:
sudo tee /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
backend = systemd
maxretry = 3
bantime = 3600
EOF
What does this configuration do?
- enabled = true activates the sshd jail so fail2ban monitors SSH authentication events.
- backend = systemd tells fail2ban to read from the systemd journal, which is where Ubuntu 24.04 logs sshd events.
- maxretry = 3 bans an IP after 3 failed authentication attempts.
- bantime = 3600 keeps the IP banned for 3600 seconds (one hour).
- Enable fail2ban to start at boot and start it immediately by running:
sudo systemctl enable --now fail2ban
- Verify the file was created correctly by running:
cat /etc/fail2ban/jail.local
- You should see the exact four lines you wrote: the [sshd] section header followed by the four directives.
Seeing a service start error?
- Check for typos in /etc/fail2ban/jail.local with cat /etc/fail2ban/jail.local and compare against the configuration above.
- Make sure you did NOT copy the full jail.conf into jail.local. If you did, delete jail.local and recreate it with only the minimal overrides shown above.
- Run sudo systemctl status fail2ban to see the specific error message.
help me troubleshoot fail2ban not starting on Ubuntu 24.04
✔️ Awesome, I've got everything!
Great. Double check you have saved /etc/fail2ban/jail.local and the fail2ban service is running.
ⓧ I'd like to double check the full code
Here is the complete /etc/fail2ban/jail.local file:
[sshd]
enabled = true
backend = systemd
maxretry = 3
bantime = 3600
Verify the jail and test brute-force protection
A running service is not enough. You need to confirm the sshd jail is actually active and that it bans IPs as expected.
- Check the fail2ban sshd jail status by running:
sudo fail2ban-client status sshd
- You should see output showing the sshd jail is active with Currently failed: and Currently banned: counters at 0.
Seeing 'no such jail' error?
- Make sure enabled = true is set under the [sshd] section in /etc/fail2ban/jail.local.
- Restart fail2ban with sudo systemctl restart fail2ban and check the status again.
help me fix fail2ban jail not found on Ubuntu 24.04
Now test that fail2ban actually bans an IP after repeated failures. You will attempt SSH connections as a nonexistent user from your host machine, which the server will reject and fail2ban will count.
- From your host machine (not from inside the VM), attempt to SSH as a fake user 4 times by running each of these commands and letting them fail:
ssh fakeuser@your-vm-ip
ssh fakeuser@your-vm-ip
ssh fakeuser@your-vm-ip
ssh fakeuser@your-vm-ip
Why use a nonexistent username?
The fail2ban sshd filter reliably detects failed attempts for invalid users in its default mode. Each connection as fakeuser is immediately rejected and logged, incrementing the failure counter. After 3 failures (your maxretry setting), the 4th connection confirms you are banned.
- Each attempt should be rejected immediately. After the 4th attempt, you should see a connection timeout or explicit refusal, confirming the ban is active.
- Return to your VM console (not via SSH, since your host IP is now banned) and verify the ban by running:
sudo fail2ban-client status sshd
- You should now see Currently banned: 1 and your host IP listed under Banned IP list.
- Unban your host IP so you can SSH again later by running:
sudo fail2ban-client set sshd unbanip your-host-ip
Access the VM directly during bans
While your host IP is banned, SSH from that machine will not work. Use the virt-manager console window to access the VM directly until you unban yourself.
Re-run Lynis and document your improvement
You started this project with a baseline Lynis score of approximately 55-60. After applying SSH hardening, firewall rules, kernel parameters, automated patching, and intrusion prevention, your score should have improved significantly.
- Run the Lynis security audit again by running:
sudo lynis audit system
- When the audit finishes, scroll to the bottom of the output and find the Hardening index line.
- Your score should now be 75 or higher, reflecting all the controls you applied across this project.
What does a 75+ score mean?
A Lynis hardening index of 75 or above places your server in the "well-hardened" category. This means you have addressed the majority of common attack vectors that automated scanners and opportunistic attackers exploit.
Compare this to your baseline of ~55-60 to see the tangible impact of the controls you applied: SSH key-only auth, default-deny firewall, kernel hardening, automated patching, and intrusion prevention.
- Record your new hardening index alongside your baseline score. This before-and-after comparison is your documented evidence of the hardening work.
Score still below 75?
- Review the Lynis suggestions section for any remaining high-priority items you may have missed.
- Make sure fail2ban is running and the sshd jail is active. Lynis checks for active intrusion detection.
- Confirm unattended-upgrades is properly configured. Run apt-config dump | grep Unattended to verify.
help me improve my Lynis hardening score on Ubuntu 24.04
Your server is now measurably hardened. You have taken it from a default-configured install scoring ~55 to a 75+ hardened system with key-only SSH, a default-deny firewall, kernel protections, automated patching, and brute-force prevention. Next up: push even further with the Secret Mission.
Secret mission
Add auditd for Privilege Escalation Tracking
Your server is hardened against external threats. But what about insider actions? In this secret mission, you'll deploy the Linux Audit Framework to track privilege escalation, critical file changes, and denied access attempts. This gives you the forensic visibility that enterprise security teams rely on.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally on your machine, so there are no ongoing cloud costs. Decide whether to keep your VM running, pause it to come back later, or delete it entirely.
Resources you used:
- Ubuntu Server 24.04 VM in KVM/virt-manager (2 GB RAM, 20 GB disk).
- SSH Ed25519 key pair on your host machine.
Keep everything running
No action needed. Choose this if you're still actively experimenting with your hardened server or want to use it as a reference for future projects.
- Your VM continues to run and consume 2 GB of RAM on your host.
- Your SSH key pair remains in ~/.ssh/ on your host machine.
- All hardening configurations (sshd, UFW, fail2ban, sysctl, auditd) remain active inside the VM.
Pause - I'll come back to this later
Shut down the VM to free up RAM while keeping the disk image intact. You can start it again later with all your configurations preserved.
- Inside the VM, run:
sudo shutdown now
- Alternatively, right-click your VM in virt-manager and select Shut Down.
- To resume later, open virt-manager and click Play on your VM.
Delete - I don't want to use this again
Remove the VM and all associated files to reclaim disk space. This cannot be undone.
- If the VM is still running, shut it down first by running sudo shutdown now inside the VM.
- In virt-manager, right-click your Ubuntu Server VM.
- Select Delete.
- Check the box for Delete associated storage files to remove the disk image.
- Click Delete to confirm.
Remove the SSH key pair from your host machine by running:
rm ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub
- If you added the server to your ~/.ssh/known_hosts file, remove the corresponding entry as well.
Nice Work!
Nice Work!
You just hardened a Linux server from a default install to a production-ready state following the same prioritized workflow that enterprise security engineers use every day.
You've learned how to:
- Harden SSH with Ed25519 key-based authentication, disabled root login, and a default-deny UFW firewall with rate-limited access.
- Apply CIS Benchmark-aligned kernel network hardening via sysctl parameters and deploy fail2ban intrusion prevention to protect against brute-force attacks.
- Follow the audit-remediate-reaudit workflow using Lynis to measure a concrete hardening index improvement from a baseline of ~55 to 75+.
- Secret Mission: Deployed auditd with CIS-aligned rules for privilege escalation tracking and file integrity monitoring.
Ready to quiz yourself?