Nothing here is exotic - it's the same handful of practices that apply to any server, applied consistently rather than skipped because the machine in question happens to be under your desk.

⚠️ Risk: "it's just a homelab" is not a reason to skip hardening if anything on it is reachable from outside your LAN - remote access via WireGuard or Tailscale (see Remote Access), a service deliberately exposed behind a reverse proxy, or even just a weak Wi-Fi password letting a neighbor onto your network. Scanners and automated attacks don't check whether a target looks "serious" before probing it.

fail2ban

fail2ban watches log files for repeated failed login attempts and temporarily (or permanently) bans the offending IP at the firewall level. It's cheap insurance anywhere SSH or a web login is reachable, even just from your LAN.

sudo apt install fail2ban

/etc/fail2ban/jail.local:

[sshd]
enabled = true
maxretry = 5
bantime = 1h
findtime = 10m
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
  • maxretry/findtime together mean 5 failures within 10 minutes triggers a ban - tune both to your own tolerance for typos vs. how aggressively you want to block.
  • fail2ban also has jails for common self-hosted apps (Nginx, various auth-proxy tools) - check its filter.d/ directory for what's already supported before writing a custom filter.

Network segmentation, revisited with a security lens

Network Segmentation covered VLANs mainly for organization (IoT away from trusted devices). Revisit it here with an attacker's mindset: if one device on a VLAN is compromised, what else can it reach? A management VLAN for Proxmox/ pfSense web UIs that's reachable from your regular device VLAN, for instance, means a compromised laptop is one hop from your hypervisor's admin interface. Tighten firewall rules (see Advanced Networking) so each VLAN can only reach what it actually needs to, not just "the internet plus everything else by default."

Least-privilege for containers

Most homelab compose files run containers as root inside the container by default, because it's the path of least resistance and most image documentation doesn't push back on it. A compromised container running as root can do more damage to anything it can reach - including, in some misconfigurations, the host itself.

services:
  app:
    image: example/app:latest
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - ./data:/app/data
    security_opt:
      - no-new-privileges:true
    restart: unless-stopped
  • user: "1000:1000" runs the container process as a non-root UID instead of the image's default (often root) - check the image's docs first, since some require specific UIDs or break without root for legitimate reasons (binding low ports, for instance).
  • read_only: true makes the container's filesystem read-only except for explicitly mounted volumes and tmpfs paths - meaningfully limits what a compromised process can persist or modify.
  • no-new-privileges:true blocks a process from gaining more privileges than it started with (via setuid binaries, for example).
  • Not every image supports all of this out of the box - some need writable paths you haven't mounted, or genuinely need root. Treat this as a default to try, not a requirement to force onto every container regardless of breakage.

Keep it proportionate

Hardening has diminishing returns past a point, and a homelab isn't a bank - the goal is closing the easy, common attack paths (weak SSH exposure, root containers, no rate limiting on logins), not chasing every theoretical hardening guide to 100%. Apply this page's practices to anything actually reachable beyond your own trusted devices first, and treat further hardening as optional polish after that.

Next: Disaster Recovery Planning.