A Linux Server Hardening Baseline

The concrete, non-negotiable set of changes to make on every Linux server before it goes anywhere near the internet -- SSH, firewall, updates, and fail2ban, done right.

What you'll build and why

This is a fixed, minimal set of changes every Linux server in this section's other guides should have before you consider it done: SSH hardened to key-only auth, a host firewall running with a deny-by-default posture, fail2ban watching for brute-force attempts, and a deliberate decision about automatic updates. None of this is exotic -- it's the baseline that separates "a server someone will compromise within days of being found" from "a server that takes real effort."

Don't build this if: honestly, there's no reason not to do this on every server you run. The only judgment call is the automatic-updates tradeoff in Step 4, not whether to do the rest.

How it works

  Internet
│
▼
ufw (host firewall, deny-by-default) -- blocks
│   everything except what's explicitly allowed
▼
SSH (key-only auth) -- password auth disabled
│   entirely, closing the most common attack path
│
▼
fail2ban -- watches auth logs, temporarily bans
IPs with repeated failed attempts,
a backstop even against allowed traffic

Each layer covers what the one before it doesn't: the firewall blocks unwanted traffic outright; key-only SSH makes password-guessing attacks against what's left pointless; fail2ban slows down anyone still probing (including against other exposed services, not just SSH).

Before you start

Decision: is SSH key auth already set up? This guide assumes you already have SSH key-based access working before you disable password auth in Step 2 -- disabling password auth without a working key first locks you out of a remote server with no other access path. If you're not certain, test key-based login and confirm it works before touching sshd_config.

Steps

Step 1: Update the system

$ sudo apt update && sudo apt upgrade -y

(Debian/Ubuntu-family; use your distribution's equivalent otherwise.) Start from a current patch level -- everything else in this guide is undermined by known, unpatched vulnerabilities.

Step 2: Harden SSH -- key-only auth, no root login

Edit /etc/ssh/sshd_config:

PasswordAuthentication no
PermitRootLogin no

Before restarting SSH, confirm your key-based login already works in a separate session -- do not close your current session until you've verified a new connection succeeds with the new config.

$ sudo systemctl restart sshd

Step 3: Install and configure ufw (deny-by-default firewall)

$ sudo apt install ufw
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw allow OpenSSH
$ sudo ufw enable

Expected output: a confirmation prompt (since enabling this can disconnect an active SSH session if the SSH rule wasn't added first -- the order above matters). Add any other rules your specific services need (sudo ufw allow <port>/tcp) before or after enabling.

Step 4: Decide on automatic security updates

$ sudo apt install unattended-upgrades
$ sudo dpkg-reconfigure --priority=low unattended-upgrades

The real tradeoff here: automatic security patching closes vulnerabilities faster than you manually checking, but an unattended update can occasionally break something on a server you're relying on. A reasonable middle ground many homelabbers use: automatic security-only updates (not full version upgrades), with a habit of checking /var/log/unattended-upgrades/ periodically.

Step 5: Install and configure fail2ban

$ sudo apt install fail2ban
$ sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

In /etc/fail2ban/jail.local, under [sshd], confirm enabled = true. Then:

$ sudo systemctl enable --now fail2ban

Editing the .local copy rather than jail.conf directly is fail2ban's own documented convention -- .local overrides survive package updates that would otherwise overwrite jail.conf.

Verify it works

  • ssh <user>@<host> with a password (not a key) is refused -- confirms PasswordAuthentication no took effect
  • sudo ufw status verbose shows deny (incoming) as the default policy, with only your intended ports explicitly allowed
  • sudo fail2ban-client status sshd shows the jail active
  • Failure test: from a separate machine, deliberately fail an SSH login several times in a row against a test/throwaway account or IP you control, and confirm sudo fail2ban-client status sshd shows that IP banned afterward -- proves fail2ban is actually watching and acting, not just installed
  • sudo ssh -o PreferredAuthentications=none <user>@<host> (from a session where you're not currently banned) should be refused, confirming root login and password auth are both genuinely closed off

Secure it

(This entire guide is the "Secure it" section for every other guide in this content set -- the points below are what to layer on top of these five steps.)

  • The single worst-case failure this guide defends against: an internet-facing server with password SSH auth and no firewall gets compromised via automated brute-force/credential-stuffing, often within hours of being discoverable -- this is not a hypothetical, it's the default outcome for an unhardened server with SSH exposed.
  • Key management: since this guide makes SSH keys the only way in, losing your private key (with no other configured access) locks you out just as effectively as it locks attackers out -- keep a backup of your key material somewhere secure, separate from the server itself.
  • This baseline is a floor, not a ceiling: it doesn't cover application-level security for whatever services you run on top of it -- see the network scanning guide elsewhere in this section for checking what's actually exposed once services are running.

Back it up and maintain it

What matters: /etc/ssh/sshd_config, /etc/ufw/, and /etc/fail2ban/jail.local -- small, worth version-controlling (with the Ansible guide elsewhere in this section, this baseline is a natural first playbook to write, applied consistently across every host).

Update cadence: the system updates from Step 1 should be ongoing (Step 4's decision), not one-time; fail2ban and ufw themselves update with normal system updates.

What to monitor: sudo fail2ban-client status sshd periodically for ban activity (a sudden spike can indicate your server has become a more active target); /var/log/auth.log for anything fail2ban isn't catching.

Troubleshooting

Logs: /var/log/auth.log (SSH and fail2ban activity), sudo journalctl -u ssh, sudo journalctl -u fail2ban, sudo ufw status verbose.

Symptom Likely cause Diagnostic Fix
Locked out of SSH entirely after Step 2 Key-based auth wasn't actually working before password auth was disabled Console/out-of-band access to the server (cloud provider console, physical access, or a Proxmox VM console) Re-enable PasswordAuthentication yes temporarily via console access, fix key auth, then re-disable
Legitimate connections getting banned by fail2ban Ban time/retry threshold too aggressive for your own usage pattern (e.g. a flaky client retrying) sudo fail2ban-client status sshd shows currently banned IPs sudo fail2ban-client set sshd unbanip <ip>; adjust maxretry/findtime in jail.local if this recurs
A needed service is unreachable after enabling ufw The port wasn't explicitly allowed before/after enabling deny-by-default sudo ufw status verbose against the service's actual port sudo ufw allow <port>/tcp (or /udp) for that service
Unattended upgrade broke something A specific package update had a breaking change (rare, but the real risk in Step 4's tradeoff) Check /var/log/unattended-upgrades/ for what was updated around the time of the issue Roll back the specific package if possible; this is the concrete risk being weighed in Step 4, not a sign the whole approach is wrong
fail2ban shows the jail as not running Log path in jail.local doesn't match where your distribution actually writes auth logs sudo fail2ban-client status and check the configured logpath against your actual log location Correct the logpath for your distribution

Undo

Reverting is mostly the inverse of each step: sudo ufw disable (removes firewall enforcement -- confirm you have another reason to feel safe before doing this), set PasswordAuthentication yes back in sshd_config and restart SSH, sudo systemctl disable --now fail2ban. There's rarely a good reason to undo any of this on a server that's actually in use.

Go further

  • Network Scanning with nmap and Trivy -- once hardened, actually verify what's exposed from an outside perspective
  • Explore SSH key restrictions further (AllowUsers, non-standard port as a minor noise-reduction measure, not a real security control on its own)
  • Apply this exact baseline via the Ansible guide elsewhere in this section, so every new server gets it automatically and consistently instead of manually redone each time

Resources

Official documentation:

Source and releases:

Community:

Related DaemonPress projects:


Last verified: 2026-09-21, checked against official OpenSSH, ufw, and fail2ban documentation.