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
Ingredients
- A running Linux server — mine is the old laptop from the previous post (already set up)
- UFW — Uncomplicated Firewall, pre-installed on Ubuntu/Mint (free)
- Fail2ban — automatically bans IPs after repeated failed SSH attempts (free)
- knockd — port knocking daemon for hidden SSH access from outside your network (free)
- Resend — email API for SSH login alerts (free tier)
- Claude Code — terminal AI for writing configs and scripts ($200/yr)
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
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
sudo ufw default deny incoming— block all inbound connections by defaultsudo ufw default allow outgoing— allow all outbound (the server can initiate connections)sudo ufw allow ssh— allow SSH (port 22) so I don’t lock myself outsudo ufw enable— activate the firewallsudo ufw status verbose— verify the rules look right before closing the terminal
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
sudo ufw allow from 192.168.x.0/24 to any port [your-port]- This allows the port only from devices on your home network (your LAN subnet)
- Any connection attempt from outside that subnet is silently dropped
Layer 2: Fail2ban — Auto-Ban Brute Force Attempts
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
sudo apt install fail2ban -y- Create
/etc/fail2ban/jail.localwith:maxretry— number of failures before a ban (keep it low)bantime— how long to ban (go higher than the default 10 minutes — hours or days)findtime— the window in which failures are counted
sudo systemctl enable fail2ban && sudo systemctl start fail2ban- Check status:
sudo fail2ban-client status sshd
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
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
- Edit
/etc/ssh/sshd_config - Set
PasswordAuthentication no - Set
ChallengeResponseAuthentication no - Set
PubkeyAuthentication yes - Restart SSH:
sudo systemctl restart ssh - Test from a second terminal window first — open a new SSH session and confirm it connects with your key before closing the current one
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
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
sudo apt install knockd -y- Configure
/etc/knockd.conf:- Open sequence:
[PORT1] → [PORT2] → [PORT3](pick random high ports — don’t use anyone else’s sequence) - Close sequence: reverse order of the open sequence
- On open:
ufw allow from %IP% to any port 22 - On close:
ufw delete allow from %IP% to any port 22 - Timeout: 5 seconds — all knocks must arrive within 5 seconds of each other
- Open sequence:
- Set the network interface in
/etc/default/knockd:KNOCKD_OPTS="-i [your-interface]"— runip ato find your interface name (e.g.eth0,wlan0) sudo systemctl enable knockd && sudo systemctl start knockd- Remove the blanket SSH rule from UFW:
sudo ufw delete allow ssh
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
- Uses
nc(netcat) to hit each port in sequence with a 0.3-second delay between knocks nc -z -w1 [server-public-ip] [PORT1]nc -z -w1 [server-public-ip] [PORT2]nc -z -w1 [server-public-ip] [PORT3]- Wait 1 second, then
ssh homeserverconnects normally - From the LAN (home network), no knock needed — SSH works directly
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
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
- The login hook script:
- Gets the connecting IP from the SSH environment
- Checks if it’s a LAN IP (your home subnet) — if so, skip the alert
- If it’s from outside the LAN, sends an email via Resend API immediately
- The email includes: connecting IP, timestamp, and a note to SSH in and investigate
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:
- UFW — default deny all inbound; internal services LAN-only; SSH handled by knockd rules
- Fail2ban — 3 strikes → 24-hour ban; nightly email with ban count
- SSH key-only auth — password authentication disabled entirely
- Port knocking — SSH invisible to public scanners; knock sequence required to open it
- Login alerts — instant email on any non-LAN SSH login
Knock, connect, get alerted. SSH access from anywhere — with a full audit trail.
What went fast
- UFW setup — five commands, 10 minutes, done
- Fail2ban — apt install + one config file; sensible defaults
- Disabling password auth — three lines in sshd_config, restart, done
- SSH login alerts — Claude wrote the IP-checking + Resend email script in one pass
What needed patience
- knockd interface name — the config needs the exact network interface name (e.g.
eth0orwlan0— useip ato find yours). Wrong interface name means knockd listens on the wrong adapter and nothing works. - UFW rule ordering with knockd — after removing the blanket
allow ssh, I had to confirm knockd was adding/removing rules correctly withsudo ufw statusafter each knock. First test: rule appeared as expected after the open sequence, disappeared after close. - Router port forwarding — Google Wifi’s port forwarding UI is buried. And you need to forward the knock ports and port 22 — knocking opens the firewall but the router still needs to let the traffic through to the server.
- Testing without locking yourself out — every change to SSH config needs to be tested from a second open session. Learned to keep a "safety session" open until I confirmed the new setup worked.
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.