Tested on: Ubuntu 24.04 LTS (Noble Numbat), kernel 6.8.x, on a Hetzner Cloud CX22 instance. Steps are valid on any cloud VPS image of the same Ubuntu version.

Why this matters

A fresh Ubuntu cloud image is fine for a tutorial and unsuitable for production. Out of the box:

  • The root account often accepts SSH key auth.
  • No host firewall is enforced (the cloud provider’s network firewall is not a substitute, especially for internal traffic).
  • Security updates apply only if you manually run apt upgrade.
  • Time can drift by minutes, breaking TLS verification and log correlation.
  • Audit logging is not enabled, so post-incident investigation depends on whatever syslog happens to have captured.

This baseline closes those gaps in an opinionated order that minimises the window during which the server is reachable but not yet hardened. Before running step 1, pick your processors and decide your logging posture — some of the choices below depend on it.

Order matters

Run these steps in the order shown. In particular, do not enable the firewall before you have confirmed SSH access for your new non-root user, or you will lock yourself out.

1. First boot: create your operator account

Log in as root (or as ubuntu with passwordless sudo on most cloud images) and:

 1adduser alice
 2usermod -aG sudo alice
 3mkdir -p /home/alice/.ssh
 4chmod 700 /home/alice/.ssh
 5# Paste public key (dated, as per the SSH guide):
 6cat > /home/alice/.ssh/authorized_keys <<'EOF'
 7ssh-ed25519 AAAA...  alice@workstation 2026-05-16
 8EOF
 9chmod 600 /home/alice/.ssh/authorized_keys
10chown -R alice:alice /home/alice/.ssh
11
12# Test: open a new terminal and confirm:
13#   ssh alice@host 'sudo -n true && echo ok'
14# Do not proceed until this succeeds.

2. Apply the SSH hardening baseline

Full details and the drop-in config file are in /guides/ssh-hardening/. The short version:

 1sudo tee /etc/ssh/sshd_config.d/00-hardened.conf >/dev/null <<'EOF'
 2PasswordAuthentication no
 3PermitRootLogin        no
 4PubkeyAuthentication   yes
 5AllowGroups            sudo
 6MaxAuthTries           3
 7ClientAliveInterval    300
 8ClientAliveCountMax    2
 9EOF
10sudo sshd -t && sudo systemctl reload sshd

Verify from a second terminal before closing your current session.

3. Enable the host firewall

Ubuntu ships ufw (Uncomplicated Firewall) — a friendly wrapper around nftables. For a typical web-serving host:

 1sudo ufw default deny incoming
 2sudo ufw default allow outgoing
 3
 4# Open only what this server needs:
 5sudo ufw allow 22/tcp      comment 'SSH'
 6sudo ufw allow 80/tcp      comment 'HTTP (redirect to HTTPS)'
 7sudo ufw allow 443/tcp     comment 'HTTPS'
 8
 9# Enable.
10sudo ufw --force enable
11sudo ufw status verbose

If you operate purely on a private network (e.g. an internal API), do not open 80/443 — open the private-subnet port range and rely on perimeter controls. Document the choice.

4. Turn on automatic security updates

Ubuntu’s unattended-upgrades ships pre-configured to apply security updates daily, but it is disabled by default on some images. Verify and enable:

1sudo apt update
2sudo apt install -y unattended-upgrades apt-listchanges
3sudo dpkg-reconfigure -plow unattended-upgrades   # answer 'Yes'
4
5# Verify it will actually run:
6sudo systemctl status unattended-upgrades
7sudo unattended-upgrade --dry-run --debug | tail

Decide whether the host should auto-reboot for kernel updates. For most single-instance workloads, reboot during a chosen maintenance window is correct:

1# /etc/apt/apt.conf.d/52unattended-upgrades-local
2Unattended-Upgrade::Automatic-Reboot "true";
3Unattended-Upgrade::Automatic-Reboot-Time "04:00";
4Unattended-Upgrade::Automatic-Reboot-WithUsers "false";

For clustered services, set Automatic-Reboot "false" and orchestrate reboots externally — otherwise two nodes can reboot simultaneously and take the cluster down.

5. Time sync — chrony

Reliable time is a TLS prerequisite (clock skew breaks certificate validation) and a logging prerequisite (correlation across hosts). The default systemd-timesyncd is acceptable; chrony is more accurate and much better instrumented.

1sudo apt install -y chrony
2sudo systemctl enable --now chrony
3chronyc tracking
4chronyc sources

6. Audit logging — auditd

auditd records security-relevant kernel events: privilege escalations, file changes in sensitive paths, network configuration changes. It does not replace application logs — it complements them with the events the kernel itself sees.

1sudo apt install -y auditd
2sudo systemctl enable --now auditd

A minimal ruleset at /etc/audit/rules.d/baseline.rules:

# Identity / authentication events
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/group  -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity

# SSH config changes
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd

# Privileged commands
-a always,exit -F arch=b64 -S execve -F euid=0 -F auid>=1000 -F auid!=4294967295 -k privileged

# Make the rules immutable until reboot.
-e 2

Apply and verify:

1sudo augenrules --load
2sudo systemctl restart auditd
3sudo auditctl -l

Forward auditd logs to a host other than the one being audited. A local audit log on a compromised host is evidence the attacker controls.

7. AppArmor — verify, do not disable

AppArmor is enabled by default on Ubuntu Server. Confirm:

1sudo aa-status | head -5

You should see a list of profiles in enforce mode. If you see “AppArmor not loaded,” reinstall apparmor and reboot. Resist the temptation to disable AppArmor when an application is denied a syscall it claims it needs — diagnose and tune the profile (aa-logprof) instead.

8. Remove what you do not need

Cloud images install services many operators never touch. Audit and remove:

1# Listening sockets right now:
2sudo ss -ltnp
3
4# Installed services you may not need (review, do not blindly remove):
5systemctl list-unit-files --state=enabled --type=service

Common removals on a web-tier host: snapd if you use only apt packages, postfix if not sending mail from this host, cups if not printing, bluetooth on a server, avahi-daemon.

9. Optional: brute-force log filtering

If your SSH port is internet-exposed despite key-only auth, the constant scan attempts in your logs are noise. fail2ban filters them at IP level:

1sudo apt install -y fail2ban
2sudo systemctl enable --now fail2ban

The default jail covers SSH. This is log hygiene, not a security control — your protection is the absence of password auth.

10. Run a baseline scan

lynis is a defensible-baseline auditor maintained by CISOfy:

1sudo apt install -y lynis
2sudo lynis audit system --quick

Investigate every WARNING. Document any you intentionally accept.

Gotchas

The cloud network firewall is not a host firewall

Your cloud provider’s firewall rules apply at their edge. Traffic between two instances in the same private network often bypasses those rules. Always enable ufw (or nftables) on the host as well.

unattended-upgrades rebooting at peak hours

The default reboot time is 02:00 system local time — which may not match your business’s idea of “off hours”. Set it explicitly.

auditd logs filling the disk

auditd defaults to ~6 MB of logs total before rotating. Verbose rules can overflow this. Tune max_log_file and num_logs in /etc/audit/auditd.conf, and forward to a central host.

Locking yourself out via ufw enable

ufw enable warns and asks for confirmation when run over SSH, if it detects the SSH session — but not always. Always add the SSH rule before enabling. Take a snapshot first when working on a cloud VPS.

What this guide deliberately does not cover

  • CIS Benchmark compliance in full — that requires tooling (CIS-CAT, OpenSCAP); a profile guide is planned.
  • Container hostscontainerd / Docker installs are excluded intentionally (this site is no-Docker).
  • SELinux on Ubuntu — not the supported MAC on Ubuntu; if you need SELinux, see /guides/rhel-baseline/.