How to Secure an Always-On Home Server
The safest service on the internet is one that isn’t on it, so start with a private network, then stack simple, independent locks behind it.
Ingredients
- An always-on Linux machine — an old laptop works fine; see How I Turned an Old Laptop Into a Headless Linux Server (one you already own)
- A private network — such as Tailscale, which links only your own devices (free tier)
- UFW — the “Uncomplicated Firewall,” built into Ubuntu and Mint (free)
- Fail2ban — automatically bans addresses that keep failing to log in (free)
- An email API — for login alerts; Resend has a free tier (free)
- Node.js + Express — for the optional remote-control API (free)
- Claude Code — terminal AI for writing configs and scripts ($200/yr)
The Idea: Don’t Put It on the Internet
The moment a machine answers on the public internet, automated scripts start probing it. They scan huge ranges of addresses, look for a login prompt, try common passwords, and move on. It’s background noise, and it starts within hours.
Most home servers are fine by default because the router doesn’t let that traffic in. The risk starts when you want to reach the server from outside: from your phone on the road, or a laptop at a coffee shop. The tempting fix is to open a port on the router. The better fix is to not open anything at all.
So the plan has two parts. First, make the server reachable only from your own devices. Second, stack independent locks behind that, so a single mistake never exposes the machine.
Layer 1: A Private Network
A private network like Tailscale puts your devices on their own encrypted network, no matter where they are. Your phone at a coffee shop and your server at home can reach each other as if they were on the same Wi-Fi. Everyone else on the internet sees nothing: no open ports, no login prompt, nothing to probe.
Install it on the server and on each device you’ll connect from, sign in to the same account, and that’s it. No router changes. This one step removes most of the risk, because there’s no public door left to attack.
Every other layer in this post makes a public door harder to get through. A private network means there isn’t a public door. Start here, then add the rest as backup.
Layer 2: A Firewall That Denies by Default
UFW makes firewall rules readable. The strategy: block everything coming in, then allow only what you need, from only where you need it.
🔧 Developer section: UFW setup
sudo ufw default deny incoming— block all inbound connectionssudo ufw default allow outgoing— let the server reach out normallysudo ufw allow ssh— allow SSH so you don’t lock yourself outsudo ufw enable, thensudo ufw status verboseto check- For any other service, allow it only from your own devices:
sudo ufw allow from [your-private-range] to any port [service-port]
Allow SSH before you enable the firewall. Enable a default-deny firewall with no SSH rule and you’re locked out on the spot, and you’ll need a keyboard and monitor to fix it.
Layer 3: SSH Keys Only, No Passwords
A password can be guessed. An SSH key, practically, can’t. Once you’ve confirmed you can log in with your key, turn password logins off entirely.
🔧 Developer section: key-only SSH
- Edit
/etc/ssh/sshd_config - Set
PasswordAuthentication noandPubkeyAuthentication yes - Restart SSH:
sudo systemctl restart ssh - Open a second session and confirm your key works before closing the first
Test every SSH or firewall change from a second connection while the first stays open. If the new setup is wrong, the old session is how you fix it. Closing the only session before testing is how people get locked out.
Layer 4: Fail2ban
Fail2ban watches login logs and temporarily bans addresses that keep failing. With a private network and key-only SSH it rarely has anything to do, which is the point: it’s there for the day something else is misconfigured.
🔧 Developer section: Fail2ban setup
sudo apt install fail2ban -y- In
/etc/fail2ban/jail.local, setmaxretrylow,bantimeto hours rather than the default minutes, andfindtimeto the window failures are counted in sudo systemctl enable --now fail2ban- Check it with
sudo fail2ban-client status sshd
Layer 5: An Alert on Every Login
Locks are only half of it. You also want to know when someone gets in, even if it’s you. OpenSSH can run a script on every successful login; point that script at an email API and you get a message with the time and source of each login within seconds.
The rule of thumb: if you see a login alert you didn’t cause, that’s your cue to investigate. If you never get alerts you didn’t cause, the layers are doing their job.
A Private Remote Control
SSH is great for a full terminal, but most of the time you don’t need one. You want to run one job, check that the server is healthy, or pull a quick report. A tiny API does that in one request, from a phone shortcut or a script.
Keep it simple: a few capabilities, each doing one thing.
- Health check — uptime, memory and disk, so you know it’s alive
- Run a job — start a named script and return right away
- Queue a longer task — hand it to a background worker, check on it later
- Pull a report — the latest resource snapshot
The security rule is the same as everything else here: reachable only over the private network, allowed only from your own devices by the firewall, and every request carries a long secret key that lives outside the code. Three independent locks, so no single mistake exposes it.
🔧 Developer section: keep it running with systemd
- Write a small service file, e.g.
/etc/systemd/system/my-api.service Restart=alwaysrestarts it if it crashes; a shortRestartSecavoids crash loopsWantedBy=multi-user.targetstarts it on boot- Launch long-running jobs as detached background processes so the request doesn’t time out
- Make the key file readable by the service’s user and nobody else
No Private Network? Port Knocking as an Alternative
If you can’t use a private network, port knocking is an older technique that hides SSH instead. The SSH port stays closed. To open it, you send connection attempts to a secret sequence of ports, in order; if the sequence matches, the firewall briefly opens SSH for your address only. Scanners see a machine with nothing open.
🔧 Developer section: knockd, briefly
sudo apt install knockd -y, then configure/etc/knockd.conf- Pick your own random high ports for the sequence; never reuse an example’s
- On a correct knock, add a UFW rule for the knocking address; on the close sequence, remove it
- Set the right network interface in
/etc/default/knockd(find it withip a); the wrong one means knockd hears nothing - Your router has to forward the knock ports and the SSH port to the server
It works, but it means opening router ports, and it’s more moving parts to get right. If a private network is an option, use that instead.
The Checklist
- Private network — the server is reachable only from your own devices
- Default-deny firewall — every service allowed only from where it’s needed
- Key-only SSH — password logins turned off
- Fail2ban — a backstop for anything misconfigured
- Login alerts — every successful login emails you
- Private remote control — behind all of the above, plus its own key
What goes fast
- The private network — install, sign in, done
- UFW and Fail2ban — a handful of commands and one config file
- Turning off password logins — a few lines and a restart
- The API — a working health check and job runner in under an hour with Claude Code
What needs patience
- Not locking yourself out — test every change from a second session
- File permissions for secrets — readable by the one service that needs them, and nothing else
- Long-running jobs — start them in the background so the API answers immediately
None of these layers is clever on its own. The point is that they’re independent: the network hides the server, the firewall limits who can talk to it, keys make logins unguessable, Fail2ban slows anyone who tries, and alerts tell you about every success. Get the first one right and the rest are insurance.