open atlas
↑ Back to track
Defensive Security BLUE · 03 · 01

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.

BLUE Middle ◷ 15 min
Level
FoundationsJuniorMiddleSenior

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 listenersss -tulpn (or ss -tlnp for TCP) maps every open port to the process and PID holding it. Anything you can’t name gets stopped.
  • Disable, then removesystemctl disable --now <svc> stops it surviving reboot; apt purge / dnf remove deletes 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, never 0.0.0.0. A database bound to 0.0.0.0 with 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.

SurfaceRisky defaultHardened settingWhy it matters
SSHPassword auth + root loginKeys only, PermitRootLogin noKills credential-stuffing and brute force
DB bind0.0.0.0, sample password127.0.0.1, rotated secretDatabase not reachable from the internet
FirewallAllow-all inboundDefault-drop, allow named portsBackstop for the service you forgot
ServicesRun as rootUnprivileged user, dropped capsCompromise stays contained, not host-wide
ErrorsVerbose stack traces, bannersGeneric errors, version hiddenStops 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.
Pick the best fit

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.

Quiz

Why is removing an unused service generally a stronger control than keeping it patched?

Quiz

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 steps

Order the host-hardening workflow from first step to last:

  1. 1 Enumerate listeners and services (ss -tulpn) to map the real surface
  2. 2 Minimize: disable and purge every service you can't name
  3. 3 Apply secure defaults to the survivors (deny-by-default, least privilege)
  4. 4 Pin the hardened state as a CIS-aligned baseline (as code)
  5. 5 Continuously scan for drift and alert on divergence
Recall before you leave
  1. 01
    What is a host's attack surface, and why is reducing it higher-leverage than patching?
  2. 02
    What is a baseline, and why does hardening without one decay over time?
Recap

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.

recallapplystretch0 of 8 done
Connected lessons

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.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.