Category
Home server security, ranked by what actually gets people compromised
Almost nobody loses a home server to a clever exploit. They lose it to an admin panel that was reachable from the internet, or to a container that was quietly root on the host all along.
The honest ranking#
Home servers are not lost to clever exploits. They are lost, in this order, to:
- An application login that is reachable from the internet. Almost every serious incident starts here, and usually with a documented default rather than a bug.
- The Docker socket. Anything that can talk to
/var/run/docker.sockcan start a privileged container that mounts the host filesystem. That is not "elevated access", it is root, immediately and completely. - Credentials sitting in plaintext config that ends up in a git repository. A Homepage
services.yamlwith thirty widgets holds read tokens for your whole estate. - Everything else, including the log-based banning tools that most hardening guides open with.
Against that ranking, a lot of common advice is theatre. Moving SSH to port 2222 removes log noise and finds you in the same scan. Installing Fail2ban with its shipped defaults (bantime = 10m, findtime = 10m, maxretry = 5) gives a botnet cycling thousands of source addresses a ten minute pause. Enabling UFW on a Docker host and believing it, when published ports are handled in the DOCKER chain before INPUT is ever consulted, is worse than doing nothing, because it feels finished.
The default#
Do not expose app logins. Put remote access on WireGuard: wg-easy for a handful of devices with a QR code each, Headscale when you want the Tailscale clients and ACLs without the SaaS. Terminate TLS on the LAN with Caddy, which idles at 25 MB and renews its own certificates. Keep passwords in Vaultwarden at 40 MB. For the few services that genuinely must be public, put one front door in front of them: Pocket ID at 30 MB if passkeys suit everyone, Authelia at 35 MB if you need a forward-auth gate with rules.
The cost is honest: a VPN is a habit change, and household members resent it.
Two situations where that is wrong. You are publishing to people who will not install a client, a status page or a family media server: then you need an auth proxy, real TLS and CrowdSec with an actual bouncer in front of the app. You run ten or more hosts and need evidence for an auditor: that is Wazuh, and its documented floor is 4 vCPU and 8 GiB for 1 to 25 agents.
Auto-updates lose more data than they prevent breaches#
Take the position plainly: unattended automatic updates of stateful containers cause more damage in this hobby than they prevent. The record is easy to check. Zigbee2MQTT 2.0 rewrites your config file with no revert. Uptime Kuma's v1 to v2 migration is one way. Immich states that downgrades are not supported at all. Home Assistant backups cannot be restored into an older core. n8n 2.0 dropped MySQL entirely.
Every one of those would have been applied at 3am by an unattended updater, and none of them is reversible by rolling the image tag back. Run a notifier, read the release note, snapshot, then upgrade. Stateless things behind a proxy are a different matter and can update freely.
The mistakes#
- Mounting the Docker socket into a dashboard. Use
ghcr.io/tecnativa/docker-socket-proxywithCONTAINERS=1andPOST=0instead. Same container list, no ability to change anything. - Running CrowdSec with no bouncer. The engine writes decisions to a database; nothing is blocked until a separately installed, separately credentialed bouncer exists. Verify with
cscli decisions add --ip 1.2.3.4 --duration 1m, then confirm the rule or the 403 actually appeared. - Banning your own reverse proxy. If the app logs the proxy's address, the jail matches it and takes the whole site down. Fix real-IP logging first, then whitelist the proxy anyway.
backend = autoon a journal-only distribution. The jail shows enabled, the failure counter never moves, and you conclude nobody is attacking you. Setbackend = systemd.- Editing
jail.conf. Package upgrades replace it. Overrides go injail.localorjail.d/. - Giving a CI runner the host socket on the same box as your forge. Anyone who can push a workflow file owns the machine.
Read next#
A security baseline for a home server is the checklist version of this page, Remote access without port forwarding the VPN setup, and An update strategy that does not lose data the upgrade workflow that replaces auto-updates. Then CrowdSec vs Fail2ban, Watchtower vs Diun and Resilience scorecard to score what you already run.
Every security tool we profile#
Sorted by operational load, lightest commitment first. Every row links to a full profile with the resource numbers, the upgrade traps and the restore path.
| Tool | Ops load | Idle RAM | Licence | Identity | Backup shape |
|---|---|---|---|---|---|
| Diun | 1, Set and forget | 25 MB | MIT | Not applicable | Embedded key-value store |
| CrowdSec | 2, Light | 120 MB | MIT | Not applicable | SQLite backup |
| Fail2ban | 2, Light | 40 MB | GPL-2.0-or-later | Not applicable | SQLite backup |
| Watchtower | 2, Light | 15 MB | Apache-2.0 | Not applicable | File copy |
| Wazuh | 5, Project-grade | 6144 MB | GPL-2.0 | OIDC via plugin | Rebuildable index |
Profiles#
Diun
Tells you a container image changed, and then does nothing else, which is the point
ops 2/5 · 120 MBCrowdSec
Log-parsing intrusion prevention with local decisions and an opt-in community blocklist
ops 2/5 · 40 MBFail2ban
Reads log files, writes firewall rules, and is still the right answer for SSH
ops 2/5 · 15 MBWatchtower
Automatic container image updates, archived by its maintainers in December 2025
ops 5/5 · 6144 MBWazuh
Agent-based SIEM and XDR that is free, open source and genuinely enterprise-weight
Head to head in this category#
- CrowdSec vs Fail2banLocal regexes you own, or a community blocklist and a separate enforcement process. The defaults decide most of it.
- Watchtower vs DiunOne is archived and pulls images at 3am. The other only tells you. That gap decides it.
Guides for this category#
- A security baseline for a home serverNine things, in order of what actually gets people compromised. Plus the popular advice you can skip.
- Single sign-on for self-hosters: the honest state of itThree mechanisms, one of which actually works. Plus the account-linking trap and the lockout you have not planned for.
Questions#
Fail2ban or CrowdSec?
Fail2ban for one VPS with SSH exposed and nothing else: one distribution package, no daemon talking to the internet, and it still works after years. CrowdSec when you run a public web service and would rather install crowdsecurity/nginx than hand-write regexes, and want the community blocklist dropping known scanners before they reach your app. The real difference is who maintains the patterns. Fail2ban's live in your jail.local and break silently when an upstream log format changes. Whichever you choose, fix the defaults: bantime = 10m with maxretry = 5 is close to no protection.
Should I enable automatic container updates?
Not for stateful services. In the last two years alone: Zigbee2MQTT 2.0 rewrote configuration.yaml one way, Uptime Kuma's v1 to v2 migration was one way, Home Assistant backups cannot restore into an older core version, Immich states downgrades are not supported at all, PLANKA 2.2.0 deactivated every SSO user, and n8n 2.0 and Mattermost v11 both removed MySQL support outright. Unattended updates would have applied all of those at 3am. Run a notifier instead of an updater, read the release note, then upgrade deliberately with a snapshot behind you.
Is changing the SSH port worth doing?
It removes noise from your logs and nothing else. Automated scanning has covered the full port range for years, so a service on 2222 is found in the same sweep as one on 22. The changes that actually matter on that host are disabling password authentication entirely, so brute force has no target, and not exposing SSH to the internet at all when a WireGuard tunnel would do. Treat a port change as log hygiene, which is a real if modest benefit, and not as a security control you can count.
Why does UFW not block my published Docker ports?
Docker inserts its own iptables rules, and traffic to a published container port is handled in the DOCKER chain before it reaches INPUT, where UFW and iptables-multiport bans live. So ufw deny 8096 looks applied and the service stays reachable, and Fail2ban cheerfully bans an address that keeps connecting. The fixes are to bind the port to a specific interface in the compose file (127.0.0.1:8096:8096), or write rules into the DOCKER-USER chain, which Docker leaves alone for exactly this purpose.
What is the single biggest risk to a home server?
An application login reachable from the internet. The evidence is in the defaults: Gotify ships admin/admin, wallabag ships wallabag/wallabag, Harbor ships Harbor12345, PLANKA's example compose sets SECRET_KEY=notsecretkey, Forgejo and Gitea serve an unauthenticated first-run installer where whoever finishes it becomes admin, Frigate's port 5000 is an unauthenticated admin API by design, and Ollama's API on 11434 has no authentication at all. None of that is a vulnerability. All of it is fatal once it is publicly reachable.
Do I need Wazuh at home?
Almost certainly not. Wazuh's documented floor for its smallest deployment, 1 to 25 agents, is 4 vCPU and 8 GiB of RAM with 50 GB of disk for 90 days of retention, running three containers before you have monitored anything. It earns that on ten or more hosts where you need file integrity monitoring, CVE detection against installed packages and CIS benchmark evidence for an auditor. If your goal is to stop SSH brute force on one box, Fail2ban does it in 40 MB. Also note the indexer has no default retention policy: without one it fills the disk and stops accepting events.
Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.