Published 2026-05-10 · 11 min read

Before you expose a service to the web — the non-negotiable Linux server hardening checklist

Self-hosters' worst incidents almost never come from a sophisticated adversary. They come from the moment a service became reachable from the internet without anyone re-checking the boring stuff first. This is the pre-exposure checklist: not general hardening, not a monthly review — the eight things you do once you've decided to open a port.

For background on the wider hygiene loop, the 30-minute homelab security baseline covers the steady-state. This post is narrower: the decision point. You're about to add a public-facing DNS record, port-forward, Cloudflare Tunnel, or take a service off Tailscale-only. What changes?

Step 1 — Decide whether the service has to be public at all

Before any hardening, ask the question that obviates most of it: does this service really need to be on the public internet? If the answer is "I want to reach it from outside my LAN," the right answer is almost always one of these:

If any of these solve your access problem, stop reading this post and use one. Pre-exposure hardening for a service that could have been zero-exposure is wasted entropy. The rest of this checklist is for the genuine cases — public APIs, web apps that need anonymous reach, services with users you don't control.

Step 2 — Confirm what's actually listening, from outside

Most self-hosters have a mental model of what their box exposes that disagrees with reality. The reality is the only one that counts. Run two scans and reconcile them:

From outside your network (a $5 VPS, a friend's home, your phone hotspot — anything off-LAN):

nmap -Pn -p- -T4 your.public.ip.here
# or against a hostname:
nmap -Pn -p- -T4 your-public-hostname.example.com

From the host itself:

ss -tulpn          # every TCP/UDP listener with the owning process
sudo ss -tulpn     # add process names if not running as root

Compare. Anything in the external scan that's not the service you intend to publish is a bug. Anything in ss -tulpn bound to 0.0.0.0 that you didn't expect is a future bug — even if your firewall is currently catching it. A reboot, a rule mistake, or a forgotten Docker -p flag will eventually expose it.

The classic surprises:

Step 3 — Lock down SSH before anything else

SSH is the one port almost every self-hoster has open. It's also the easiest place to drift. Open /etc/ssh/sshd_config and confirm:

PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding no
MaxAuthTries 3
LoginGraceTime 30s

Reload sshd: sudo systemctl reload sshd. Don't restart and disconnect mid-session — keep one shell open until you've verified a fresh login works.

Then audit ~/.ssh/authorized_keys on every host. The keys you forgot are the ones that bite. (See SSH key hygiene for homelabs for the full audit.)

For the cipher/MAC/KEX side specifically, weak choices accumulate after distro upgrades. The Ubuntu 22.04 hardening checklist has the exact Ciphers / MACs / KexAlgorithms lines worth pinning before any service goes public.

Step 4 — Patch the host and confirm zero known-exploited CVEs

Run the package manager update on every host before exposing anything:

# Debian / Ubuntu
sudo apt update && sudo apt full-upgrade -y

# Rocky / Alma / RHEL
sudo dnf upgrade -y

# Alpine
sudo apk update && sudo apk upgrade

# Arch
sudo pacman -Syu --noconfirm

Before exposing the service, also cross-check the live per-distro CVE tracker for any open critical findings — for Ubuntu 24.04, 22.04, Debian 12, AlmaLinux 9 or Rocky 9. The trackers rebuild daily and surface the exact source-package fix version per release, so you can confirm apt full-upgrade actually pulled the patched build rather than something pinned in a stale repo.

Updates without a reboot don't apply to a running kernel or libraries that are still mapped into long-lived processes. If a kernel landed: reboot. If libssl updated: restart every service that links against it (or just reboot — homelab, not high-availability).

Then check the CISA Known Exploited Vulnerabilities catalog against your installed package versions. KEV is the smallest list of CVEs you can't afford to ignore — every entry has confirmed exploitation in the wild. (See how to triage CVE findings for the rest of the prioritisation hierarchy.)

Two CVEs worth knowing about by name before exposing any OpenSSH-bearing host: regreSSHion (CVE-2024-6387) and the xz/liblzma backdoor (CVE-2024-3094). Both are exactly the kind of thing that ships on a long-running LTS box and never gets noticed without an active scan.

Step 5 — Harden TLS and certificate hygiene

Anything serving a login form or API token over plaintext HTTP is a liability the moment it's public. Cheap fixes:

Test the result with SSL Labs or testssl.sh. An A grade isn't a vanity metric; B and below means there's a known weakness someone can target.

Step 6 — Authenticate every admin surface

The single highest-leverage rule when something goes public: no admin UI, anywhere, ever, reachable without auth. Not "behind a hard-to-guess port." Not "only people who know the URL." Not "I'll fix it after launch." Authenticated, or not reachable.

The dangerous services to double-check before exposure:

Even with auth, prefer a layered approach: an authenticating reverse proxy (Authelia, Authentik, OAuth2 Proxy) or zero-trust mesh in front of the app's own auth. The app's auth might have a CVE next month; the proxy still catches anonymous traffic.

For more on why this is the dominant compromise vector, see exposed admin surfaces: the #1 homelab compromise vector.

Step 7 — Set firewall rules to default-deny

A firewall isn't a substitute for hardening, but it's the cheap backstop when hardening drifts. Set the default to deny, then explicitly allow only the public service:

# Ubuntu / Debian (ufw)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'ssh'
sudo ufw allow 443/tcp comment 'https — public service'
sudo ufw enable

# Verify
sudo ufw status verbose

nftables / iptables / firewalld / pf — pick whichever your distro speaks natively. The pattern is always the same: deny inbound by default, list the exceptions explicitly.

Recheck the rules after every container change. docker run -p 80:80 and compose ports: entries punch holes through ufw on most distros without warning. The "I added a service and it's accidentally public" bug starts here.

Step 8 — Plan for drift detection, not a one-time pass

You've now done the pre-exposure work. The question that decides whether the service stays safe is what happens in the months after.

The realistic threat model for a self-hosted Linux box isn't a zero-day from a nation-state. It's drift:

None of these are detectable by a one-time hardening pass. They're detectable by running the same audit nightly and looking at what changed since yesterday. That's the difference between hardening as a checkbox and hardening as a process.

Three options at the homelab scale:

  1. Lynis on a cron — run weekly, diff the report against last week's by hand.
  2. OSSEC / Wazuh — heavier, agent-based, designed for SIEM-style correlation. Worth it if you already have time-series infrastructure; usually overkill at homelab scale.
  3. An agentless scanner from your Mac. Noxen sits here — runs the same SSH-driven audit nightly, shows only what changed since the last scan, $79 one-time. Built for exactly this gap. (See agent vs agentless for the architectural trade-off.)

Whichever you pick, the principle is identical: the goal isn't to pass a one-time audit; it's to notice when the audit's answer changes.

Common mistakes

FAQ

Do I need enterprise tooling for any of this?

No. Every check above runs with what's already on the box — nmap, ss, the package manager, sshd, ufw. Tools like Lynis, Wazuh, and Noxen automate the recurring part; they don't replace the one-time pass.

Is Tailscale really enough that I can skip the rest?

Tailscale removes the public-internet attack surface, which is the highest-impact change you can make. SSH hardening, package patching, and admin-surface auth still matter — your ACLs aren't perfect, your devices can be lost or compromised, and lateral movement inside a flat tailnet is real. Tailscale lowers the ceiling; it doesn't raise the floor.

How often should I re-run this checklist?

The full checklist: every time you newly expose something. The audit-style version (Steps 2–7 as recurring checks): nightly via tooling, weekly by hand if not. See how often should you scan your homelab.

What's the single most overlooked item?

Step 6. Self-hosters tend to spend hours on TLS configuration and then leave a Pi-hole admin or a Portainer instance reachable on the same domain with default credentials. Exposed admin surfaces are the dominant compromise vector at the homelab scale.

Try Noxen$79 one-time, agentless, diff-from-yesterday reports for your homelab and small VPS fleet.

Scan your Linux fleet from your Mac

Noxen runs nightly agentless audits over SSH and shows only what changed since the last scan — new CVEs, config drift, newly exposed admin services. Mac-native control plane, no SaaS round-trip.

Buy Noxen — $79 one-time Download free trial