OS and host hardening
Every running service, open port, and shipped default is attack surface. Hardening shrinks a host to the job it actually does, swaps permissive defaults for safe ones, and pins a measurable baseline so drift becomes visible.
A fresh Ubuntu cloud image boots, and before you SSH in for the first time, ss -tlnp already shows a handful of listeners you never asked for: a recursive resolver bound to a public interface, a print-spooler from the desktop meta-package, an SNMP daemon with the public community string still set. Each one is a service you don’t use, didn’t audit, and won’t patch on time — and each is a doorway. Nobody attacked you yet. The image just shipped with the doors open, and “default” is the most dangerous word in infrastructure.
By the end of this lesson you’ll know how to shrink a host to the exact job it does, replace permissive defaults with safe ones, and pin a baseline so configuration drift becomes a thing you can see instead of a thing you discover during an incident.
Attack surface is the sum of what’s reachable
Before you can harden a host you need a precise mental model of what an attacker can actually touch. A host’s attack surface is the union of every interface where untrusted input meets your code: listening network ports, running services and their RPC endpoints, local setuid binaries, kernel modules, exposed filesystems, scheduled jobs, and the credentials each of those runs as. Every item is a potential entry or a potential escalation. The defender’s leverage is brutally simple and brutally effective: you cannot be exploited through a service that isn’t running. Removed code has no CVEs.
This is why attack-surface reduction is the highest-leverage move in host security and the cheapest. A patch fixes one known bug in a service; removing the service fixes every bug it will ever have, including the ones not yet discovered. When the 2014 Shellshock and 2021 PrintNightmare classes of bugs landed, the hosts that shrugged weren’t the ones that patched fastest — they were the ones that had never installed the vulnerable component in the first place. Reduction beats reaction.
Service minimization: subtract before you add
Service minimization is the disciplined practice of running the smallest set of processes a host needs to do its one job, and nothing else. A box that serves HTTP does not need a mail transfer agent, a desktop DNS resolver, Avahi/mDNS, CUPS, or an X server. The operational test for every listener is one sentence: name the specific function this serves and the team that owns it. If you can’t, it shouldn’t be running.
In practice you walk the surface and cut:
- Audit listeners —
ss -tulpn(orss -tlnpfor TCP) maps every open port to the process and PID holding it. Anything you can’t name gets stopped. - Disable, then remove —
systemctl disable --now <svc>stops it surviving reboot;apt purge/dnf removedeletes the binary so it can’t be re-enabled or exploited at all. - Bind to the narrowest interface — a service that only talks to localhost should listen on
127.0.0.1, never0.0.0.0. A database bound to0.0.0.0with a default password is how you end up in a ransom note. - Default-deny the firewall — drop all inbound, then allow only the named ports. The firewall is your backstop for the service you forgot to disable.
Secure defaults: the survivors must be safe out of the box
Whatever services survive minimization, their defaults are tuned for getting started, not for production safety — and those are opposite goals. Shipped defaults optimize for “works on first run with zero config,” which means broad binds, verbose errors, permissive permissions, and sample credentials. Hardening flips every one of those to deny by default: a config starts from the most restrictive setting and opens only what’s named.
| Surface | Risky default | Hardened setting | Why it matters |
|---|---|---|---|
| SSH | Password auth + root login | Keys only, PermitRootLogin no | Kills credential-stuffing and brute force |
| DB bind | 0.0.0.0, sample password | 127.0.0.1, rotated secret | Database not reachable from the internet |
| Firewall | Allow-all inbound | Default-drop, allow named ports | Backstop for the service you forgot |
| Services | Run as root | Unprivileged user, dropped caps | Compromise stays contained, not host-wide |
| Errors | Verbose stack traces, banners | Generic errors, version hidden | Stops free recon for the attacker |
The two highest-value defaults to fix are authentication and privilege. SSH with password auth and root login enabled is the single most-brute-forced surface on the public internet; switching to key-only auth with PermitRootLogin no removes the entire credential-guessing class of attack. And the principle of least privilege applies to every process: a web server compromised while running as root is a compromised host; the same exploit against a service running as an unprivileged user with dropped Linux capabilities is a compromised process — the difference between an incident and a catastrophe.
▸Why this works
Why is removing a service strictly better than patching it? A patch is a promise about known bugs — it closes the CVEs disclosed so far, on the day you apply it, assuming you apply it on time (and the industry median time-to-patch is measured in weeks, not hours). A removed service has no attack surface at all: no zero-days, no patch lag, no agent to keep current, no config to drift. Patching is a race you run forever; removal ends the race. That’s why “do we even need this?” is the first hardening question, asked before “is it patched?”
Baselines: make the hardened state measurable and durable
A host you hardened by hand is hardened once. Three months and forty deploys later, someone re-enabled a service to debug an outage, a config-management run reverted a setting, an image rebuild dropped your firewall rules — and you have no idea, because “hardened” lived only in one engineer’s memory. The fix is a baseline: a written, machine-checkable definition of the host’s known-good secure state, plus continuous comparison against it.
This is exactly what the CIS Benchmarks provide — consensus-built, prescriptive configuration baselines per OS and platform, with each control specifying the audit check and the remediation. NIST SP 800-123 frames the same idea at the program level: harden to a documented standard, then maintain it. The baseline turns hardening from a heroic one-time effort into an enforced property:
- Pin it as code — encode the baseline in Ansible/Chef/Puppet or a golden image so every host is born hardened, not hardened afterward.
- Audit continuously — tools like OpenSCAP,
Lynis, or a CIS-CAT scan grade a live host against the benchmark and emit a score plus a list of failed controls. - Alert on drift — when a live host diverges from the baseline, that delta is a security event, not a footnote. The drift is often the first visible sign of either a sloppy change or an active intrusion.
A scan finds a CUPS print-spooler listening on a production web host that has no printers and serves only HTTP. Pick the best hardening response.
Why is removing an unused service generally a stronger control than keeping it patched?
You harden a host by hand and it passes a CIS scan with a high score. Six weeks later, what is the most likely reason it's no longer compliant?
Order the host-hardening workflow from first step to last:
- 1 Enumerate listeners and services (ss -tulpn) to map the real surface
- 2 Minimize: disable and purge every service you can't name
- 3 Apply secure defaults to the survivors (deny-by-default, least privilege)
- 4 Pin the hardened state as a CIS-aligned baseline (as code)
- 5 Continuously scan for drift and alert on divergence
- 01What is a host's attack surface, and why is reducing it higher-leverage than patching?
- 02What is a baseline, and why does hardening without one decay over time?
Host hardening is the discipline of shrinking a machine to exactly the job it does and keeping it there. Start by mapping the attack surface — every listening port, service, setuid binary, and the privileges each runs as — because you can’t be exploited through a service that isn’t running. Minimize first: enumerate listeners with ss -tulpn, then disable and purge anything whose function and owner you can’t name, since removed code has no CVEs and removal beats the perpetual patch race. Then make the survivors safe out of the box by flipping permissive defaults to deny-by-default and least privilege: key-only SSH with no root login, databases bound to localhost, default-drop firewalls, services running unprivileged with dropped capabilities. Finally, make it durable: pin the hardened state as a CIS-aligned baseline expressed as code, audit live hosts continuously with OpenSCAP or CIS-CAT, and treat any drift from the baseline as a security event — because a host hardened by hand is hardened exactly once, and decays with the next deploy. Now when you boot a fresh image and ss -tlnp shows listeners you never asked for, your first question isn’t ‘is it patched?’ — it’s ‘can I name what this does, and if not, why is it running?’.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.