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 root or as a user with sudo.
  • 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.

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)

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, run sudo sshd -t and sudo systemctl reload ssh. The usual causes are a wrong name in AllowUsers or a missing public key in ~/.ssh/authorized_keys.
  • Password logins still work. Another file in sshd_config.d sorts before yours and sets PasswordAuthentication yes. Check with sudo sshd -T | grep passwordauthentication and ls /etc/ssh/sshd_config.d/.
  • A service is unreachable after enabling UFW. Its port is not allowed. Add it with sudo ufw allow <port>/tcp and verify with sudo ufw status.
  • Fail2Ban does not start. Run sudo fail2ban-client -t to test the configuration and read sudo journalctl -u fail2ban for 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.