Learn ← All posts
Post

Hardening a small server

security · ssh · server

Key-only SSH, a ban list that reads the app's own log, and no inbound port on the public internet.

A server with a public IP collects attempts. Most of it is noise, but the rules that keep it quiet are cheap, so they are in place from the first day rather than after the first problem.

SSH

One drop-in file, so the distribution's defaults stay untouched and every change I made is in a single readable place:

PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3

The point is not that a key can't be broken. It's that getting in requires something an attacker has to hold, not something they can guess.

Nothing listens in public

The services bind to loopback only, and the outside reaches them through an outbound tunnel. The connection is opened from inside the machine:

internet  ->  tunnel  ->  127.0.0.1:<port>  ->  app

There is no inbound port to scan, no port left open by accident, and no firewall detail to get wrong at 2am. It also means one hardened door instead of four.

fail2ban, with two details that matter

Jails read the applications' own failed-login logs and ban repeat offenders. Two things I would do again:

  • The web jail bans its own port, not SSH. If I lock myself out, I can still get back in over SSH and lift the ban on my own address. A control that can lock out the person who owns the machine is a liability, not a safeguard.
  • Behind a tunnel the peer is always localhost. A naive port ban would then ban everyone or nobody. Bans have to be decided on the real client address carried in the proxy headers, and those headers are only trusted when the connection genuinely arrived from the proxy.

Keep the log honest

Anything written to a log that fail2ban reads is an injection point. Put a username into that log unsanitised and someone can type a newline plus a fake ban line into the login form and have the log believe it. So every value is collapsed into a single token before it is written: spaces, tabs, newlines and control characters become underscores, and the sanitising happens in one function that all writes go through.

Fewer moving parts

No sign-up page, no third-party dashboard, nothing with a default password. One account, created from a shell prompt, unreachable without a second factor. Most of the work was deciding what not to expose.