← All Writing
March 7, 20269 min read

How I Secured the Home Linux Server

Layered security for an always-on home machine — UFW firewall, Fail2ban brute-force protection, SSH key-only auth, port knocking for remote access, and instant login alerts

YieldA hardened home Linux server with layered defenses — resistant to brute-force attacks, accessible remotely without exposing SSH publicly
DifficultyIntermediate (firewall rules, SSH config, knockd daemon, systemd services)
Total Cook Time~2 hours: firewall + Fail2ban (~45 min) + SSH hardening (~30 min) + port knocking (~45 min)

Ingredients

The Problem: SSH Is a Target

The moment you connect a machine to the internet with SSH running, bots start knocking. Not metaphorically — literally. Automated scripts scan entire IP ranges looking for open port 22, try common username/password combinations, and move on. It’s background noise on the internet, and it starts within hours of going online.

Most home setups are "secure enough" because they’re behind a router that doesn’t forward SSH. But I wanted to SSH in from outside my home network — from my phone at a coffee shop, from a laptop while traveling. That meant opening a path in from the public internet, which meant I needed to be intentional about security.

The approach: layered defense. No single lock — multiple locks, where even breaking one doesn’t hand you the keys. And alerts so I know immediately if anything unusual happens.

Layer 1: UFW Firewall — Default Deny

~15 minutes

UFW (Uncomplicated Firewall) is a front-end for iptables that makes firewall rules readable. The strategy: deny everything by default, then explicitly allow only what I need.

🔧 Developer section: UFW setup

Critical order of operations

Always allow SSH before enabling the firewall. If you enable UFW with default deny and no SSH rule, you’ll lock yourself out of the machine immediately — and need a monitor and keyboard to fix it. The order matters: allow → enable, not enable → allow.

I also restricted my personal API server to LAN-only access — no reason for that port to be reachable from the public internet:

🔧 Developer section: LAN-only port rule

Layer 2: Fail2ban — Auto-Ban Brute Force Attempts

~20 minutes

Fail2ban watches authentication logs and temporarily bans IP addresses that fail login attempts repeatedly. Even with key-only SSH (more on that below), it’s a useful layer — it stops bots from hammering the port and cluttering logs.

🔧 Developer section: Fail2ban setup

I also set up a nightly cron that emails me a count of new bans since the previous day. It’s useful to see the number trending — a sudden spike could mean something targeted, while a steady background rate is just normal internet noise.

Layer 3: SSH Hardening — Keys Only, No Passwords

~15 minutes

Password-based SSH is inherently weaker than key-based auth. A password can be guessed. An SSH private key cannot (practically). Once I confirmed my key-based login was working, I disabled password authentication entirely:

🔧 Developer section: Disable SSH password auth

Test before you commit

Always keep your current SSH session open while testing the new config. If something’s wrong and you can’t connect with a key, you still have the existing session to fix it. Closing the only session before verifying is how people get locked out.

Layer 4: Port Knocking — Hidden SSH from the Internet

~45 minutes

Port knocking is a technique where SSH port 22 is closed by default — completely hidden from the outside world. To open it, you send a specific sequence of connection attempts to a set of ports (the "knock sequence") in order. If the sequence matches, the firewall temporarily opens SSH for your IP. Anything that doesn’t know the sequence sees nothing.

This solves the remote access problem without exposing port 22 to public scanners. Bot scripts see a machine with no open ports. Only someone who knows the knock sequence can even attempt to connect.

🔧 Developer section: knockd setup

Now SSH is invisible from outside the network. To connect remotely, I first run a knock script on my Mac, then SSH normally:

🔧 Developer section: Mac knock script

Router setup for remote access

For port knocking to work remotely, your router needs to forward your three knock ports and port 22 to the server’s LAN IP. In Google Wifi: Settings → Network & general → Advanced networking → Port management. Add forwarding rules for each port pointing to the server’s reserved LAN IP.

Layer 5: SSH Login Alerts

~20 minutes

Even with all the above, I wanted to know immediately if someone successfully SSH’d into the server — especially from outside my home network. OpenSSH has a built-in hook that runs a script automatically on every successful login. I wired that to a Resend alert.

🔧 Developer section: SSH login alert

Why skip LAN logins?

LAN SSH connections (from my Mac at home) are routine. Alerting on every one of those would bury real alerts in noise. The check is simple: if the connecting IP matches your home subnet, it’s local — skip the email. Anything else gets alerted.

Final Security Stack

Five layers, each independent — breaking one doesn’t break the others:

Terminal — Remote Access from Anywhere
# From a coffee shop, on phone hotspot:
$ bash ~/knock.sh
Knocking [PORT1]... [PORT2]... [PORT3]
SSH port opened.

$ ssh homeserver
Welcome to Linux Mint 22.1
user@homeserver:~$

# Meanwhile, email arrives:
📬 SSH login from [external IP] at 14:22:07

Knock, connect, get alerted. SSH access from anywhere — with a full audit trail.

What went fast

What needed patience

The entire stack took about two hours. What it gets me: a machine that’s been running for weeks, publicly accessible from anywhere via SSH, that has had zero successful unauthorized access attempts — and I’d know immediately if that changed.

← Back to all writing