Arvind Verma

Linux server hardening checklist for Ubuntu 24.04 LTS

A tested Ubuntu 24.04 hardening checklist: SSH keys only, sudo, automatic updates, firewall, fail2ban, auditd, sysctl, logs, backups and CIS checks.

By Arvind Verma9 min read

In short

  • Put SSH settings in a drop-in that sorts first, then confirm with sshd -T. A cloud-init file can silently re-enable passwords.
  • Turn on ufw, rate-limit SSH and enable fail2ban. Ubuntu 24.04 ships an sshd jail that already uses systemd and nftables.
  • Measure, don't guess: Lynis gives a hardening index in minutes, and Ubuntu Pro's USG audits against the CIS Benchmark.
On this page
  1. Before you start
  2. 1. Patch, and keep patching
  3. 2. Users and sudo
  4. 3. SSH: keys only, no root, no passwords
  5. 4. Firewall: default deny with ufw
  6. 5. Brute-force protection: fail2ban or CrowdSec
  7. 6. Audit trail with auditd
  8. 7. File permissions
  9. 8. Kernel sysctl basics
  10. 9. Time sync
  11. 10. Ship logs off the box
  12. 11. Backups you have actually restored
  13. 12. Verify with Lynis and the CIS Benchmark
  14. Debian and RHEL differences
  15. Hardening checklist

Before a fresh Ubuntu 24.04 LTS server takes real traffic, I work through the checklist below. It suits anyone who runs their own VPS or cloud instances: developers who inherited the ops job, small teams without a security engineer, and sysadmins who want a second pair of eyes on their baseline.

Every command and config snippet below was checked against Ubuntu 24.04.5 with the current noble packages (OpenSSH 9.6p1, fail2ban 1.0.2, ufw 0.36.2, auditd 3.1.2, Lynis 3.0.9). Where Debian or RHEL behave differently, there is a short table near the end. Hardening and incident response are both part of my day-to-day work, and the items on this list are the ones that matter most when a server is attacked.

Before you start

Hardening a remote server is the easiest way to lock yourself out of it. Three habits prevent that:

  • Have out-of-band access. Know how to reach the provider’s serial or web console before you touch SSH or the firewall.
  • Keep a second SSH session open while you change SSH or firewall settings. Test new logins from a third terminal before you close anything.
  • Snapshot first. Starting a disk snapshot takes seconds on any cloud and turns a mistake into a quick rollback.

Change one area at a time, test it, then move on.

1. Patch, and keep patching

A published vulnerability with a published fix is the cheapest way in for an attacker. Patch the box fully, then make patching automatic.

sudo apt update && sudo apt full-upgrade -y
sudo apt install unattended-upgrades

unattended-upgrades is installed by default on Ubuntu Server, and the default configuration applies security updates daily. Confirm that the periodic job is on:

cat /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Kernel and libc updates need a reboot to take effect. Decide whether the server may reboot itself. If it can, set a quiet hour in /etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";
Unattended-Upgrade::Mail "[email protected]";

The mail option only works if the server can send mail. Test the whole thing without changing anything:

sudo unattended-upgrade -v --dry-run
sudo needrestart -r l   # list services still running old libraries

If unplanned reboots are not acceptable, Canonical Livepatch applies many kernel fixes without one. It comes with Ubuntu Pro, which is free for personal use on up to five machines. Run sudo pro attach, then sudo pro enable livepatch.

2. Users and sudo

Nobody should log in as root, and nobody should share an account.

sudo adduser arvind
sudo adduser arvind sudo        # full admin through sudo
sudo passwd -l root             # root has no usable password on Ubuntu by default; this makes sure

Points to check:

  • One named account per human. Shared accounts make your audit trail useless during an incident.
  • Service accounts get no shell and no sudo, or a sudo rule limited to the exact commands they need. Write rules with sudo visudo -f /etc/sudoers.d/deploy, never by editing files directly.
  • Avoid NOPASSWD: ALL for humans. If a laptop key is stolen, a sudo password is the only thing left between the attacker and root.
  • Review access regularly: getent group sudo and sudo -l -U username.
  • Home directories are private. Since Ubuntu 21.10, new home directories are created with mode 0750. Servers upgraded from older releases may still have 0755 homes; check with ls -ld /home/*.

3. SSH: keys only, no root, no passwords

Know your real defaults

The table compares OpenSSH’s compiled-in defaults with what Ubuntu 24.04 ships and what you want. The middle column is the output of sshd -T on a clean install.

Setting OpenSSH default Ubuntu 24.04 effective Target
PermitRootLogin prohibit-password without-password (same thing) no
PasswordAuthentication yes yes no
KbdInteractiveAuthentication yes no no
X11Forwarding no yes no
MaxAuthTries 6 6 3
LoginGraceTime 120 120 30

Password logins are still on by default. That matters, because bots try passwords against every public IPv4 address all day long.

Create and install a key first

On your workstation, not the server:

ssh-keygen -t ed25519 -C "arvind@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
ssh [email protected]     # must work before you continue

Use a drop-in that sorts first

Ubuntu’s /etc/ssh/sshd_config starts with Include /etc/ssh/sshd_config.d/*.conf, and for most keywords the first value sshd reads wins. On many cloud images and some ISO installs, cloud-init writes /etc/ssh/sshd_config.d/50-cloud-init.conf, which may contain PasswordAuthentication yes. A file named 99-hardening.conf loses to it without any warning. Name yours so it sorts first:

sudo groupadd sshusers
sudo adduser arvind sshusers
sudo nano /etc/ssh/sshd_config.d/01-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitEmptyPasswords no
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
AllowGroups sshusers

Two lines need judgement. AllowTcpForwarding no breaks SSH tunnels; leave it out if your team reaches databases that way. AllowGroups sshusers locks out anyone not in that group, so check your own membership twice.

Test, then apply, then prove it

sudo sshd -t                                   # syntax check, silent on success
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|kbdinteractive|allowgroups|authenticationmethods'
sudo systemctl reload ssh

sshd -T prints the settings sshd will actually use, which is the only view that counts. If sshd -t complains about a missing /run/sshd, the SSH service has not started yet; run the check from an SSH session or create the directory with sudo mkdir -p /run/sshd.

From another machine, confirm passwords are refused:

ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
# Expected: Permission denied (publickey).

Changing the port on 24.04

Moving SSH off port 22 cuts log noise, though it adds no real security. If you do it, know that Ubuntu 24.04 uses systemd socket activation: ssh.socket owns the listening port. After you change Port in a drop-in, open the new port in the firewall, then run:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

Restarting only ssh.service leaves the old port listening. Ubuntu’s own sshd_config comments say the same thing.

4. Firewall: default deny with ufw

ufw is installed on Ubuntu Server but disabled. Allow SSH before you enable it.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit 22/tcp          # allow, but deny an IP that opens 6+ connections in 30 seconds
sudo ufw allow 80,443/tcp      # only if this is a web server
sudo ufw enable
sudo ufw status verbose

Better still, if you have a fixed office IP or a VPN, restrict SSH to it:

sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp

Keep the cloud firewall (security group, NSG or VPC firewall) as a second layer with the same rules. If one of them is ever opened by mistake, the other still holds.

If the server sits behind Cloudflare, allow 80 and 443 only from Cloudflare’s published IP ranges, or use Cloudflare Tunnel and open no web ports at all. Cloudflare security and performance settings for every plan covers both, along with the zone settings in front of them.

On 24.04, ufw drives iptables through the nf_tables backend. If you prefer to write nftables rules directly, disable ufw first so the two don’t fight over the same tables.

5. Brute-force protection: fail2ban or CrowdSec

With passwords off, brute force cannot succeed, but it still fills logs and burns CPU. On 24.04 the fail2ban package ships /etc/fail2ban/jail.d/defaults-debian.conf, which enables an sshd jail with the systemd backend and the nftables ban action. You only tune it:

sudo apt install fail2ban
sudo nano /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled  = true
maxretry = 5
findtime = 10m
bantime  = 1h
bantime.increment = true
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

Put your own static IP in ignoreip, and set port = 2222 if you moved SSH. Then:

sudo fail2ban-client -t                 # config test
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

CrowdSec is the alternative when you run several servers. It parses logs like fail2ban, then shares attacker IPs with its community blocklist. Install from CrowdSec’s own repository:

curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec crowdsec-firewall-bouncer-nftables

Run one or the other on a host, not both, so you only have one thing to debug when a legitimate user gets blocked.

6. Audit trail with auditd

Logs tell you what services reported. auditd tells you who changed /etc/shadow at 02:14. Install it and add rules for the files attackers touch first.

sudo apt install auditd audispd-plugins
sudo nano /etc/audit/rules.d/50-hardening.rules
## Identity and privilege
-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
-w /etc/sudoers -p wa -k actions
-w /etc/sudoers.d/ -p wa -k actions
## SSH configuration
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
## Common persistence points
-w /etc/crontab -p wa -k cron
-w /etc/cron.d/ -p wa -k cron
-w /var/spool/cron/ -p wa -k cron
-w /etc/systemd/system/ -p wa -k systemd
## Commands run as root by a logged-in human
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=unset -k root-cmd

Load and query:

sudo augenrules --load
sudo auditctl -l
sudo ausearch -k identity --start today -i
sudo aureport --summary

augenrules merges every .rules file in /etc/audit/rules.d/ into /etc/audit/audit.rules, and auditctl -l shows what the kernel actually loaded. The last rule can be noisy on busy admin hosts; watch the log volume for a week.

7. File permissions

Ubuntu’s defaults are sensible (/etc/shadow is 640 root:shadow, SSH host private keys are 600). What drifts over time is everything you add. Check:

# World-writable files outside /proc, /sys and /tmp
sudo find / -xdev -type f -perm -0002 -not -path '/tmp/*' -not -path '/var/tmp/*' -ls

# SUID/SGID inventory: save it now, diff it later
sudo find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -printf '%m %u %p\n' | sort > ~/suid-baseline.txt

# SSH key files that sshd will refuse (StrictModes)
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys

Keep secrets such as .env files at 600 and owned by the service user, never by root and never world-readable. Web roots should not be writable by the web server process except for the specific upload directory.

8. Kernel sysctl basics

Ubuntu already sets several protections in /etc/sysctl.d/10-*.conf and /usr/lib/sysctl.d/: reverse-path filtering in loose mode (rp_filter=2), kernel.kptr_restrict=1, Yama ptrace_scope=1, and the protected hardlink, symlink, FIFO and regular-file settings. Add a small file for the network items it leaves open:

sudo nano /etc/sysctl.d/60-hardening.conf
# Ignore ICMP redirects and source-routed packets
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.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# A server is not a router
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Log packets with impossible source addresses
net.ipv4.conf.all.log_martians = 1
# SYN flood protection and smurf protection (kernel defaults, set explicitly)
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Only CAP_SYSLOG can read the kernel log
kernel.dmesg_restrict = 1
# No core dumps from setuid programs
fs.suid_dumpable = 0
sudo sysctl --system

Do not add net.ipv4.ip_forward = 0 on hosts that run Docker, Kubernetes, WireGuard or anything that routes traffic. Container networking depends on forwarding, and a CIS scanner will flag it anyway. Mark it as an accepted exception rather than breaking the host.

9. Time sync

Correlating logs across servers during an incident is painful when clocks disagree. Ubuntu 24.04 uses systemd-timesyncd by default; Ubuntu switched to chrony from 25.10.

timedatectl status          # look for "System clock synchronized: yes"
timedatectl timesync-status

If you need NTS (authenticated time) or tighter accuracy, install chrony, which replaces timesyncd. Keep servers on UTC so timestamps line up with cloud provider logs.

10. Ship logs off the box

The first thing a competent intruder does is clean the logs on the machine they are on. Logs you keep elsewhere survive that. Ubuntu 24.04 still runs rsyslog alongside the journal, so forwarding is one file:

sudo nano /etc/rsyslog.d/90-forward.conf
action(
  type="omfwd"
  target="logs.example.internal"
  port="514"
  protocol="tcp"
  queue.type="linkedList"
)
sudo systemctl restart rsyslog
logger "rsyslog forwarding test from $(hostname)"

Plain TCP syslog is unencrypted, so use it only across a private network or VPN. Across the internet, use rsyslog’s TLS options or an agent such as Vector, Fluent Bit, the CloudWatch agent or New Relic’s infrastructure agent (see my notes on New Relic’s free tier for infrastructure monitoring). Whatever you choose, alert on SSH logins, sudo use and auditd identity events.

11. Backups you have actually restored

Hardening reduces the chance of a bad day. Backups decide how bad it gets. The rules that matter:

  • Off the server and off the account. A backup in the same cloud account as the server can be deleted with the same stolen key. Use a separate account or provider with delete protection or object lock.
  • Automatic and monitored. A backup job can fail quietly for weeks. Alert on failures and on backups that are older than expected.
  • Restore-tested on a schedule. Restore to a scratch server monthly and time it. That number is your real recovery time.
  • Include the config. /etc, crontabs, systemd units and your hardening files, so a rebuild matches the old server.

If a server is ever compromised, clean backups are what let you rebuild instead of guessing. My first-hour checklist for a compromised Linux server covers that day.

12. Verify with Lynis and the CIS Benchmark

Run a scanner now and after every change. It shows you what you missed and gives you a number to track.

Lynis is open source and needs nothing but a shell:

sudo apt install lynis
sudo lynis audit system

It prints a hardening index out of 100 plus warnings and suggestions, and writes details to /var/log/lynis.log and /var/log/lynis-report.dat. Save the first score and watch it climb as you work down this list. The Ubuntu archive carries Lynis 3.0.9; CISOfy’s own repository carries the latest release.

CIS Benchmark. The current CIS Ubuntu Linux 24.04 LTS Benchmark is v2.0.0, and CIS provides the PDF free for non-commercial use. With Ubuntu Pro attached, Canonical’s Ubuntu Security Guide (USG) audits against it:

sudo pro enable usg
sudo apt install usg
sudo usg audit cis_level1_server

Reports land in /var/lib/usg/ as HTML and XML. usg fix applies changes automatically; generate a tailoring file first (sudo usg generate-tailoring cis_level1_server tailor.xml) and switch off rules that would break your workload. Check which benchmark version your USG release implements, because Canonical’s profile can trail the latest CIS version. Level 1 is a sensible target for most servers; Level 2 costs usability and needs testing.

Debian and RHEL differences

The checklist carries over to other distributions with these changes:

Area Ubuntu 24.04 Debian 12/13 RHEL 9/10, Rocky, Alma
SSH unit ssh.socket activates ssh.service ssh.service, no socket by default sshd.service
SSH drop-ins /etc/ssh/sshd_config.d/ same same (also crypto-policies include)
Admin group sudo sudo wheel
Automatic updates unattended-upgrades installed install unattended-upgrades dnf-automatic, enable dnf-automatic-install.timer, set upgrade_type = security
Firewall ufw (disabled) nftables, ufw optional firewalld (firewall-cmd)
MAC framework AppArmor AppArmor SELinux, keep it enforcing
Syslog rsyslog plus journald journald only; rsyslog not installed by default since 12 journald with rsyslog

On Debian, without rsyslog, ship logs with systemd-journal-upload or an agent instead of the rsyslog snippet above.

Hardening checklist

Work down this list on every new server, then keep the scanner output as evidence:

  • Console access confirmed, snapshot taken, second SSH session open
  • Fully patched; unattended-upgrades on; reboot policy decided; Livepatch if reboots are hard
  • Named admin accounts in sudo; root locked; no shared logins; no NOPASSWD: ALL for humans
  • SSH keys only via /etc/ssh/sshd_config.d/01-hardening.conf; confirmed with sshd -T and a refused password login
  • ufw default deny; SSH limited or restricted to known IPs; cloud firewall mirrors it
  • fail2ban (or CrowdSec) running with your IP in ignoreip
  • auditd rules for identity files, sudoers, SSH config, cron and systemd
  • No unexpected world-writable files; SUID baseline saved
  • Network sysctl file applied; forwarding left alone on container hosts
  • Clock synchronised, servers on UTC
  • Logs forwarded off the host, with alerts on logins and sudo
  • Backups off-account, monitored and restore-tested
  • Lynis run and saved; CIS Level 1 audit via USG where Ubuntu Pro is available

If you would rather have someone else run this across a fleet and keep it in place, that is the kind of work I do through NoDowntime Technologies.

Sources

  1. sshd_config(5), Ubuntu 24.04 (openssh-server 9.6p1) manpages.ubuntu.com
  2. SSHd now uses socket-based activation (Ubuntu 22.10 and later), Ubuntu Discourse discourse.ubuntu.com
  3. cloud-init issue #5934: Cannot disable PasswordAuthentication due to 50-cloud-init.conf github.com
  4. Ubuntu Server documentation: OpenSSH server ubuntu.com
  5. Ubuntu Server documentation: Automatic updates ubuntu.com
  6. Ubuntu Server documentation: User management ubuntu.com
  7. Ubuntu Server documentation: Firewalls ubuntu.com
  8. ufw(8), Ubuntu 24.04 manpages.ubuntu.com
  9. augenrules(8), Ubuntu 24.04 manpages.ubuntu.com
  10. audit-userspace sample rules: 30-stig.rules github.com
  11. CrowdSec: Linux installation docs.crowdsec.net
  12. CrowdSec: Firewall remediation component docs.crowdsec.net
  13. Linux kernel documentation: /proc/sys/kernel docs.kernel.org
  14. Linux kernel documentation: IP sysctl docs.kernel.org
  15. Ubuntu Server documentation: About time synchronisation ubuntu.com
  16. rsyslog documentation: omfwd forwarding output module docs.rsyslog.com
  17. Lynis: Get started cisofy.com
  18. CIS Benchmarks: Ubuntu Linux cisecurity.org
  19. CIS compliance with USG for Ubuntu 24.04 LTS, Ubuntu Discourse discourse.ubuntu.com
  20. Ubuntu Livepatch ubuntu.com
  21. Release notes for Debian 12: Changes to system logging debian.org
  22. RHEL 10: Managing and monitoring security updates (DNF Automatic) docs.redhat.com