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 hosts —
containerd/ 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/.