Server hardening means reducing what an attacker can reach and making every remaining entry point harder to abuse. Most compromises of Internet-facing Linux servers come from a small set of causes: unpatched software, weak or reused SSH passwords, services exposed that should not be, and nobody noticing the attack. In this tutorial you will address each of them on a fresh Ubuntu 24.04 server: automatic security updates, key-only SSH, a default-deny firewall, brute-force protection with Fail2Ban, safer kernel network settings, audit logging and a final Lynis scan to measure the result.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, such as a CubePath VPS, and console or SSH access as
rootor as a user withsudo. - A computer with an SSH client (OpenSSH on Linux and macOS, or the built-in OpenSSH client on Windows 10 and later).
- A way into the server that does not depend on SSH, such as the web console in your provider's panel. If a firewall or SSH change locks you out, this is how you get back in.
ImportantMake every SSH and firewall change while keeping your current session open, and test the new settings from a second terminal before closing it.
Step 1 - Updating the system and enabling automatic security updates
Unpatched packages are the easiest way into a server, so start by installing all pending updates:
sudo apt update
sudo apt full-upgrade
If the upgrade installed a new kernel or core libraries, the file /var/run/reboot-required exists. Reboot in that case:
[ -f /var/run/reboot-required ] && sudo reboot
Ubuntu Server ships unattended-upgrades, which installs security updates every day. Make sure it is installed and enabled:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Answer Yes when asked whether to download and install stable updates automatically. Verify the result:
cat /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Kernel updates only take effect after a reboot. If the server can tolerate a short restart at night, let unattended-upgrades reboot it when needed. Create a local override instead of editing the packaged 50unattended-upgrades file:
sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Test the configuration with a dry run:
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 5
The output ends with the list of packages that would be upgraded, or No packages found that can be upgraded unattended, and no errors.
Step 2 - Creating an administrative user with SSH keys
Logging in directly as root means that any leaked credential gives full control, and every action is anonymous. Use a personal user with sudo instead. If you are logged in as root, create one and replace your_user with your user name:
adduser your_user
usermod -aG sudo your_user
Next, set up key-based authentication. On your local computer, generate an Ed25519 key pair if you do not have one yet, and protect it with a passphrase when prompted:
ssh-keygen -t ed25519 -C "your_user@laptop"
Copy the public key to the server:
ssh-copy-id your_user@your_server_ip
Open a new terminal and log in with the key. You should get a shell without being asked for the account password (only for the key passphrase, if you set one):
ssh your_user@your_server_ip
sudo whoami
root
Do not continue until this works, because the next step disables password logins.
Step 3 - Hardening the SSH server
Ubuntu's /etc/ssh/sshd_config includes every file in /etc/ssh/sshd_config.d/ at the top, and for most options sshd uses the first value it reads. A drop-in file whose name sorts first therefore overrides both the main file and files such as 50-cloud-init.conf that cloud images add to enable password logins.
Create the drop-in:
sudo nano /etc/ssh/sshd_config.d/10-hardening.conf
# Keys only, no root logins
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
# Limit guessing and idle unauthenticated connections
MaxAuthTries 3
LoginGraceTime 30
# Features a typical server does not need
X11Forwarding no
# Only these users may log in over SSH
AllowUsers your_user
Replace your_user in AllowUsers with your user name. If other people or automation accounts need SSH, list them separated by spaces.
Validate the configuration. The command prints nothing when the syntax is correct:
sudo sshd -t
Check the values sshd will actually use after merging all files:
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|allowusers)'
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
maxauthtries 3
allowusers your_user
Apply the changes. On Ubuntu 24.04 the service is called ssh:
sudo systemctl reload ssh
Keep your current session open and, from a second terminal, confirm that your key still works and that password logins are refused:
ssh your_user@your_server_ip
ssh -o PubkeyAuthentication=no your_user@your_server_ip
your_user@your_server_ip: Permission denied (publickey).
Changing the SSH port reduces log noise from bots but is not a security control on its own, and on Ubuntu 24.04 it also requires adjusting the ssh.socket unit. Key-only authentication plus Fail2Ban in Step 5 is what actually stops brute-force attacks.
Step 4 - Configuring the UFW firewall
A default-deny firewall ensures that only the services you explicitly allow are reachable, even if a package starts listening on a new port. Ubuntu includes UFW. Set the default policies:
sudo ufw default deny incoming
sudo ufw default allow outgoing
Allow SSH before enabling the firewall. limit allows the connection but denies an IP that opens 6 or more connections within 30 seconds, which slows down brute-force attempts:
sudo ufw limit OpenSSH
Allow any other service the server offers, for example a web server:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Enable the firewall and confirm the rules:
sudo ufw enable
sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
OpenSSH LIMIT IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
OpenSSH (v6) LIMIT IN Anywhere (v6)
80/tcp (v6) ALLOW IN Anywhere (v6)
443/tcp (v6) ALLOW IN Anywhere (v6)
NoteDocker publishes container ports by writing its own iptables rules, which bypass UFW. If you run Docker, bind published ports to
127.0.0.1or put them behind a reverse proxy.
Step 5 - Blocking brute-force attacks with Fail2Ban
Fail2Ban watches log files for repeated authentication failures and bans the offending IP in the firewall for a while. Even with password logins disabled, it cuts down the noise and blocks scanners that also probe other services. Install it:
sudo apt install fail2ban
Never edit /etc/fail2ban/jail.conf, which is replaced on upgrades. Put your settings in jail.local:
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
# Never ban your own office or VPN address
ignoreip = 127.0.0.1/8 ::1 your_trusted_ip
[sshd]
enabled = true
Replace your_trusted_ip with the public IP you administer the server from, or remove it from the line. With these values, an IP that fails 5 times within 10 minutes is banned for one hour. Enable the service and check the SSH jail:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 37
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 4
`- Banned IP list: 198.51.100.7
If you ban yourself, unban your IP from the provider's console:
sudo fail2ban-client set sshd unbanip your_ip
Step 6 - Hardening kernel network settings
The kernel exposes network behavior through sysctl. Ubuntu already enables several protections, such as SYN cookies and loose reverse path filtering, in /etc/sysctl.d/10-network-security.conf. The following settings add protection against ICMP redirect and source routing tricks, which a server that is not a router never needs, and hide kernel addresses from unprivileged users:
sudo nano /etc/sysctl.d/99-hardening.conf
# Do not accept or send ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv6.conf.default.accept_source_route = 0
# Log packets with impossible source addresses
net.ipv4.conf.all.log_martians = 1
# Hide kernel pointers and the kernel log from unprivileged users
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
These settings do not touch IP forwarding, so Docker, VPNs and Kubernetes keep working. Load all sysctl files and check one value:
sudo sysctl --system | tail -n 5
sysctl net.ipv4.conf.all.accept_redirects
net.ipv4.conf.all.accept_redirects = 0
Step 7 - Removing unnecessary services
Every listening service is attack surface. List the ports that are open on the server and the process behind each one:
sudo ss -tulpn
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=512,fd=13))
tcp LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=801,fd=3),("systemd",pid=1,fd=103))
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1422,fd=6))
Services bound to 127.0.0.1, 127.0.0.53 or ::1 are only reachable from the server itself. For anything else, ask whether it needs to be there. Databases, caches and admin panels usually should listen on localhost or a private network only.
Also review the running services:
systemctl list-units --type=service --state=running
Stop and disable anything you do not use, or remove the package entirely. For example, if a leftover FTP server is running:
sudo systemctl disable --now vsftpd
sudo apt purge vsftpd
Step 8 - Checking AppArmor
AppArmor confines programs such as rsyslogd, tcpdump or mysqld to the files and capabilities listed in their profile, which limits the damage if one of them is exploited. It is enabled by default on Ubuntu; confirm it is still active:
sudo aa-status | head -n 5
apparmor module is loaded.
112 profiles are loaded.
52 profiles are in enforce mode.
/usr/bin/man
/usr/lib/NetworkManager/nm-dhcp-client.action
If a program misbehaves because of AppArmor, look for apparmor="DENIED" in sudo journalctl -k and adjust its local profile rather than disabling AppArmor.
Step 9 - Recording security-relevant changes with auditd
The Linux audit system records who changed what at the kernel level, which is essential to reconstruct what happened after an incident. Install it:
sudo apt install auditd
Add rules that watch the files attackers typically modify: accounts, sudo rules and the SSH configuration:
sudo nano /etc/audit/rules.d/50-hardening.rules
# Accounts and groups
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/gshadow -p wa -k identity
# Privilege escalation rules
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
# SSH server configuration
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
-w watches a path, -p wa records writes and attribute changes, and -k sets a key you can search for later. Load the rules and list them:
sudo augenrules --load
sudo auditctl -l
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
...
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
Test a rule by creating a throwaway user, then search the audit log by key:
sudo useradd audittest
sudo userdel audittest
sudo ausearch -k identity -i | tail -n 5
The output shows the useradd and userdel commands, the time and the login user who ran them through sudo.
Step 10 - Auditing the result with Lynis
Lynis is an open source security auditing tool that checks hundreds of settings and suggests improvements. It is a good way to verify your work and find what is left:
sudo apt install lynis
sudo lynis audit system
The scan takes a minute or two. At the end, Lynis prints warnings, suggestions and a hardening index:
Lynis security scan details:
Hardening index : 71 [############## ]
Tests performed : 262
The full report is saved in /var/log/lynis-report.dat and the detailed log in /var/log/lynis.log. Read the suggestions critically: not every item applies to every server, but warnings deserve attention. Run the audit again after major changes to compare the index.
Troubleshooting
- Locked out after changing SSH. Log in through your provider's web console, fix or remove
/etc/ssh/sshd_config.d/10-hardening.conf, runsudo sshd -tandsudo systemctl reload ssh. The usual causes are a wrong name inAllowUsersor a missing public key in~/.ssh/authorized_keys. - Password logins still work. Another file in
sshd_config.dsorts before yours and setsPasswordAuthentication yes. Check withsudo sshd -T | grep passwordauthenticationandls /etc/ssh/sshd_config.d/. - A service is unreachable after enabling UFW. Its port is not allowed. Add it with
sudo ufw allow <port>/tcpand verify withsudo ufw status. - Fail2Ban does not start. Run
sudo fail2ban-client -tto test the configuration and readsudo journalctl -u fail2banfor the exact error.
Conclusion
Your Ubuntu 24.04 server now installs security updates automatically, accepts only SSH keys for non-root users, exposes only the ports you chose, bans brute-force sources, uses safer kernel network defaults, records changes to critical files and has a Lynis baseline to measure future work against. As next steps, set up off-site backups so you can recover from anything hardening does not stop, send the server's logs to a central log server so an attacker cannot erase them locally, and schedule a periodic Lynis run to catch configuration drift.
