Every hardening guide on the internet is either four bullet points or four hundred. This is the list we actually apply to a public Linux host before it carries anything, with the reasoning attached — because a step you do not understand is a step you will undo later when it becomes inconvenient.
It takes about an hour. Roughly in this order.
1. Fix SSH before anything else
This is first because until it is done, everything else is being configured over a channel protected by a password.
- Keys only.
PasswordAuthentication no. Automated password guessing against port 22 is constant and indiscriminate; keys remove the entire category. - No root login.
PermitRootLogin no. Root is the one username an attacker can rely on existing. - Name who may log in.
AllowUsers youruser. An explicit allow-list means a service account created later by a package cannot become a login path.
Then — and this matters — open a second terminal and confirm you can still log in before closing the first. Locking yourself out of a cloud VM at this step is a rite of passage best skipped.
Changing the SSH port is optional and does not do much: it cuts log noise from opportunistic scanning, and stops nothing that is looking for you specifically. Do it if the quieter logs help you spot real events; do not count it as security.
2. Close the firewall, then verify from outside
Default deny inbound, with exceptions for what you actually serve — typically 22, 80, 443.
The critical part is the verification, and there are two traps:
Cloud VMs have two firewalls. The one in the guest OS, and the provider’s virtual network firewall (security group, security list, VPC rules). Traffic must pass both, which means a rule you set in only one place produces a host that is either mysteriously unreachable or quietly open. Configure both, deliberately.
Docker writes its own rules. Publishing a container port inserts rules ahead of your
INPUT policy, so your carefully written drop rules do not apply to it. Bind published
ports to a specific interface — 127.0.0.1:8080:8080, never 8080:8080 — on any public
host. There is more on both traps here.
Then verify from somewhere else entirely. iptables -L tells you what the host believes.
A scan from another network tells you what is true.
3. Turn on automatic security updates
unattended-upgrades on Debian and Ubuntu, dnf-automatic on the Red Hat side, restricted
to the security repository.
The objection is that unattended updates might break something. Occasionally true. Set against it: the overwhelming majority of compromised servers are running a patch that has been available for months, because patching was somebody’s intention rather than somebody’s cron job. Take the small risk of a bad update over the large risk of a known-vulnerable service.
Configure it to reboot in a maintenance window if a kernel update needs one, or you will accumulate patched-but-not-active kernels for a year.
4. Turn off what you are not using
Cloud images ship with services you did not ask for. rpcbind is the usual example — an
RPC portmapper, listening, unused by anything you installed, and historically a reliable
source of remote vulnerabilities.
ss -tulpn | grep -v '127.0.0.1\|::1'
Anything in that list that you cannot name and justify should be disabled. This one command finds more real exposure than most hardening checklists.
5. fail2ban, with realistic expectations
It watches logs and temporarily bans addresses that fail repeatedly. With key-only SSH already set, it is not preventing a break-in — it is keeping your logs readable and trimming resource waste from constant scanning.
Worth ten minutes. Not worth believing it is doing more than that.
6. Give it somewhere to shout
A host nobody is watching is a host you learn about from a customer. Before it carries anything real:
- An external uptime check, from outside your network
- Disk, memory and load metrics with sensible alert thresholds
- Alerts routed somewhere a human reads, firing only on failure
There is more on getting this ratio right in monitoring that tells you something.
7. Make sure it can be rebuilt
The real question is not whether the server is hardened. It is whether you could produce another one exactly like it.
Everything above should exist as a script, a configuration management role, or at minimum a written runbook in version control. Otherwise you have a snowflake — hardened today, drifting from tomorrow, and impossible to reason about in six months when nobody remembers which of these steps was applied.
What is deliberately not on this list
Worth stating, because their absence is a choice:
- SELinux or AppArmor tuning. Genuinely valuable, genuinely more than an hour. Leave the distribution default enabled rather than disabling it to make something work, which is the common and regrettable shortcut.
- Host intrusion detection. Meaningful once you have somewhere for the alerts to go and someone to read them. Before that, it generates noise you will learn to ignore.
- CIS benchmark compliance. If you need it, you know you need it. Applying a full benchmark to a small web server produces a long remediation list with limited practical benefit.
The seven above cover the realistic attack surface of a small public host. The rest is worth doing when you have a specific reason, and worth skipping when the reason is that a checklist mentioned it.
We do this as part of infrastructure security work — and the hardened VPS serving this site is the same list, applied.