A fresh Ubuntu server is reasonably secure, but it is not ready to be forgotten on the public internet. Secure defaults help; a defensible server also needs a small attack surface, controlled access, timely patches, useful logs, and a recovery plan.

This guide turns a new Ubuntu 24.04 LTS server into a practical baseline in 16 ordered steps. Each step explains why it matters and how to check the result. It is a baseline, not a universal policy: a Docker host, VPN gateway, database, and web server do not have identical requirements.

Keep your current SSH session open while changing access or firewall settings. Test every change from a second terminal before closing the working session. Also confirm that your provider offers a serial or VNC console in case SSH becomes unavailable.

The checklist

# Control Priority
1 Take a recovery snapshot Essential
2 Create a non-root administrator Essential
3 Restrict SSH access Essential
4 Enable a default-deny firewall Essential
5 Rate-limit repeated login failures Essential
6 Enable automatic security updates Essential
7 Review kernel and network settings Recommended
8 Confirm AppArmor is enforcing Recommended
9 Enable system auditing and persistent logs Recommended
10 Review temporary and world-writable storage Recommended
11 Remove unused services Recommended
12 Add a second SSH factor Advanced
13 Run security audits Advanced
14 Automate encrypted off-site backups Essential
15 Centralize monitoring and alerts Recommended
16 Establish a maintenance routine Recommended

1. Create a recovery point

Take a full disk snapshot from the hosting provider before changing the server. Record the public IP address and verify that you can reach the provider’s out-of-band console without SSH. A snapshot is not a backup strategy, but it is the quickest way to recover from a bad firewall or boot configuration.

2. Create a non-root administrator

Using a named account creates a useful audit trail and prevents routine commands from running with unrestricted privileges.

adduser adminuser
usermod -aG sudo adminuser
install -d -m 700 -o adminuser -g adminuser /home/adminuser/.ssh
nano /home/adminuser/.ssh/authorized_keys
chown adminuser:adminuser /home/adminuser/.ssh/authorized_keys
chmod 600 /home/adminuser/.ssh/authorized_keys

Paste your public key into authorized_keys, then test from another terminal:

ssh adminuser@SERVER_IP
sudo whoami

The second command should print root. Do not continue until both commands work.

3. Restrict SSH access

Create a backup and add a small configuration drop-in instead of rewriting the distribution’s main file:

sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
sudo nano /etc/ssh/sshd_config.d/99-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 4
AllowUsers adminuser

Validate the complete configuration before reloading SSH:

sudo sshd -t
sudo systemctl reload ssh

Open another terminal and confirm that key login works. Then confirm a password-only attempt is refused:

ssh adminuser@SERVER_IP
ssh -o PubkeyAuthentication=no adminuser@SERVER_IP

Changing port 22 can reduce log noise, but it does not replace authentication or a firewall. If you change the port, update the firewall before reloading SSH.

4. Enable a default-deny firewall

Allow the SSH port first, followed only by services the machine is intended to publish:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Do not add ports 80 and 443 unless the server actually handles web traffic. On a cloud platform, align UFW with the provider’s security groups or network firewall instead of assuming one replaces the other.

5. Block repeated login failures

Key-only SSH removes password guessing, while Fail2ban still limits noisy, repeated failures and can protect other services later.

sudo apt update
sudo apt install fail2ban -y
sudo nano /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

If trusted users share a NAT address, consider an ignoreip entry so one person cannot temporarily block everyone behind the same public IP.

6. Enable automatic security updates

Apply the current updates, install the updater, and enable its periodic job:

sudo apt update
sudo apt upgrade -y
sudo apt install unattended-upgrades apt-listchanges -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
sudo unattended-upgrade --dry-run --debug

Review /etc/apt/apt.conf.d/50unattended-upgrades. Security updates should be allowed, unused dependencies may be removed, and automatic reboot behavior must match the service’s availability requirements. An update that needs a restart is not fully deployed until the affected process—or the server—has restarted.

7. Review kernel and network settings

Create /etc/sysctl.d/99-hardening.conf with settings appropriate to the host:

# Disable routing only when this server is not a router, VPN gateway, or container host.
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0

# Reject source-routed packets and ICMP redirects.
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
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

# Keep SYN cookies and useful malformed-packet logging enabled.
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1

Apply and inspect the result:

sudo sysctl --system
sudo sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.accept_redirects

Do not blindly enable strict reverse-path filtering on hosts with asymmetric routing, multiple interfaces, policy routing, or some VPN setups. Likewise, Docker and Kubernetes hosts normally require forwarding.

8. Confirm AppArmor is enforcing

AppArmor limits what a compromised process can access beyond normal Unix file permissions.

sudo apt install apparmor apparmor-utils -y
sudo systemctl enable --now apparmor
sudo aa-status

Review profiles in complain mode rather than converting them all at once. A profile should be enforced only after its denials have been tested against the real workload.

9. Enable auditing and persistent logs

Auditd records security-relevant changes that ordinary application logs may miss:

sudo apt install auditd audispd-plugins -y
sudo systemctl enable --now auditd
sudo nano /etc/audit/rules.d/hardening.rules
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
sudo augenrules --load
sudo auditctl -l
sudo aureport --summary

For systemd-journald, set Storage=persistent in /etc/systemd/journald.conf, restart it, and check disk limits so logs cannot consume the filesystem.

10. Review temporary and world-writable storage

Find unusual world-writable files and directories on the root filesystem:

sudo find / -xdev -type f -perm -0002 -print
sudo find / -xdev -type d -perm -0002 ! -perm -1000 -print

Review every result; do not delete matches automatically. Mount options such as nodev, nosuid, and noexec can reduce risk on /tmp or /dev/shm, but noexec may break installers, build tools, and applications. Test changes in a maintenance window and keep console access available.

To restrict core dumps from privileged programs:

echo 'fs.suid_dumpable = 0' | sudo tee /etc/sysctl.d/60-suid-dumpable.conf
sudo sysctl --system

11. Remove unused services and ports

Inventory what listens on the network and what starts at boot:

sudo ss -tulpn
systemctl list-unit-files --state=enabled

Disable a service only after identifying why it is installed and confirming the workload does not need it. Common candidates on a headless machine include CUPS, Bluetooth, and Avahi, but package choices vary by image:

sudo systemctl disable --now cups
sudo systemctl disable --now bluetooth
sudo systemctl disable --now avahi-daemon
sudo apt autoremove --purge

Re-run ss -tulpn; every remaining listener should have an owner and a reason.

12. Add a second factor to SSH

For high-value systems, require both a key and a time-based one-time password:

sudo apt install libpam-google-authenticator -y
google-authenticator

Store the emergency codes offline. Add this to /etc/pam.d/sshd:

auth required pam_google_authenticator.so

Then update the SSH drop-in:

KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
PasswordAuthentication no

Run sudo sshd -t, reload SSH, and verify key plus OTP login in a second window. Keep the original connection open until the complete flow succeeds.

13. Run security audits

Lynis provides a broad configuration review. Rootkit scanners can provide another signal, but their findings need interpretation and do not prove a machine is clean.

sudo apt install lynis rkhunter -y
sudo rkhunter --propupd
sudo rkhunter --check --sk
sudo lynis audit system

Only run rkhunter --propupd after establishing that the server itself is trusted; that command accepts the current file properties as the baseline.

14. Automate encrypted off-site backups

Hardening cannot prevent a failed disk, destructive deployment, stolen account, or ransomware. Use the 3-2-1 principle: three copies, two storage types, and one copy off-site. Restic encrypts data before uploading it:

sudo apt install restic -y
export RESTIC_REPOSITORY='s3:https://OBJECT_STORAGE_ENDPOINT/BUCKET'
export RESTIC_PASSWORD_FILE='/root/.config/restic/password'
restic init
restic backup /etc /home /var/www
restic snapshots

Do not place the repository password directly in a globally readable cron entry or shell history. Use a root-readable environment file or secret manager, then schedule backups and retention with a systemd timer. Back up application data in a consistent state—databases generally need a native dump or snapshot.

Test a restore regularly:

mkdir /tmp/restic-restore-test
restic restore latest --target /tmp/restic-restore-test

Inspect restored files, then remove the test directory when finished. A backup that has never been restored is only an assumption.

15. Centralize monitoring and alerts

Local logs may disappear with the server. Send authentication and audit events to another host or a managed logging service. At minimum, alert on:

A daily summary from Logwatch is a useful small-server starting point:

sudo apt install logwatch -y
sudo logwatch --detail high --range today

Monitoring is valuable only when an alert reaches someone who can respond.

16. Make hardening a routine

Configuration drifts as software is installed, firewall rules change, and keys accumulate. Put these checks on the calendar:

Cadence Check
Weekly Review firewall rules, Fail2ban activity, failed units, and backup results.
Monthly Confirm automatic updates are succeeding and inspect listening ports.
Quarterly Restore representative files and review every authorized SSH key.
Quarterly Re-run Lynis and compare new findings with the previous report.
Yearly Review the server’s purpose, exposed services, owners, and recovery plan.

Final verification

Before calling the baseline complete, capture the output of these commands in a private operations record:

sudo sshd -t
sudo ufw status verbose
sudo fail2ban-client status sshd
sudo ss -tulpn
sudo aa-status
sudo auditctl -l
systemctl --failed
sudo unattended-upgrade --dry-run

Hardening is not a single command or a perfect score. It is a layered system: prevent unnecessary access, limit what a successful intrusion can do, preserve evidence, detect changes, and maintain a tested path back to working service.

This guide targets Ubuntu 24.04 LTS. Re-check package behavior and defaults before using it on another release, and adapt every control to the server’s actual role.