Skip to content

Security pitfalls: lessons learned

Most homelab security problems aren't exotic. They're a password that ended up in git, a "LAN only" service that turned out to be reachable through the reverse proxy, or a convenience setting nobody remembered turning on. Short entries: what goes wrong, and what to do instead. The checklist version is the hardening & hygiene checklist.


A secret was committed to git

What goes wrong. You notice a password or API key in a compose file and delete it in the next commit. It's still in the history, in every clone, and on every host your deploy tool cloned the repo to (many tools clone the whole repo for every stack, so one host's secret sits on all of them). If you pasted it into an issue or a ticket, the tracker's edit history keeps it after you edit it out.

Do.

  • Rotate it. Rewriting history is optional clean-up; rotation is the fix.
  • Keep secrets in the deploy tool's secret variables or a gitignored .env, and reference them as ${VAR} in compose.
  • Add a pre-commit secret scanner (gitleaks, trufflehog) so it doesn't happen again.
  • Check file modes: .env, compose.env and credential files should be 0600, not the 0644/0664 an editor or cp leaves behind.

One password, many services

What goes wrong. The deploy tool's admin password, its database password, a camera RTSP password and the notification server login all turn out to share a base password with a different suffix. One leak opens all of them.

Do. Generate a unique random value per service (a password manager does this well), and when rotating a database password, change it in the database and in the app's config together (e.g. MongoDB db.changeUserPassword plus the env file), or the app won't start.


"Local addresses don't need a login" behind a reverse proxy

What goes wrong. Apps like the *arr suite (Authentication Required: Disabled for Local Addresses) or qBittorrent (Bypass authentication for clients in whitelisted IP subnets) skip the login for private IPs. Put them behind a reverse proxy or tunnel and every request arrives from the proxy's private IP. The login is now skipped for the internet too, and only whatever sits in front (an SSO policy) protects them.

Do. Require authentication everywhere (Enabled), keep CSRF protection and host-header validation on, and treat SSO in front as a second layer, not the only one. See IP bans and ACLs behind a reverse proxy.


Admin tools that are root in disguise

What goes wrong. These all give full control of a host to anyone who gets in:

  • A browser IDE (code-server) running as a user in the docker group or with passwordless sudo.
  • A container dashboard with start/stop/exec buttons (Dozzle with actions or shell enabled, Portainer) and no authentication.
  • Anything with /var/run/docker.sock mounted, even :ro.
  • Router API keys with firmware, cron or service-control privileges, created for a dashboard that was never set up.

Do.

  • Turn off what you don't use (Dozzle DOZZLE_ENABLE_ACTIONS=false and DOZZLE_ENABLE_SHELL=false if it's only for logs).
  • Bind admin UIs to 127.0.0.1 and reach them over SSH port-forwarding or a VPN, or put them behind SSO with a strict policy. Never publish them on a public hostname that depends on a single policy.
  • Give each script or dashboard its own API user with only the read privileges it needs, describe its purpose in the user's description field, and delete users whose consumer no longer exists.
  • Use a socket proxy for Docker-reading widgets.

Every container port published on all interfaces

What goes wrong. ports: "8080:8080" binds 0.0.0.0 (and [::]). Docker inserts its own iptables rules, so these ports bypass the host's INPUT policy and ufw. Many internal-only services (ML workers, RTSP restreams, local LLM APIs, voice pipelines, message brokers) have no authentication of their own and are then open to the whole LAN.

Do.

  • Publish only what a client outside the host needs, and bind the rest to 127.0.0.1: or to a specific interface IP. Containers on the same Docker network reach each other without published ports.
  • Filter in the DOCKER-USER chain if you need host-level rules for published ports — see Firewall.
  • Enable authentication on brokers and streams: MQTT allow_anonymous false with a user per client, RTSP credentials on go2rtc, an auth token on Zigbee2MQTT's frontend.
  • Segment at the router too: a camera or guest VLAN shouldn't be able to reach server admin ports at all.

Forwarding WAN to the reverse proxy that serves everything

What goes wrong. You want one service reachable directly (no tunnel), so you forward WAN 443 to the reverse proxy that also serves your admin UIs behind SSO at the tunnel. Now anyone can send Host: <ADMIN>.<YOUR_DOMAIN> straight to your WAN IP and the proxy serves it, skipping the SSO that only exists on the tunnel path. Exposed ports also get found fast: even a closed media-server port gets probed many times a day.

Do. If something must be reachable directly, expose only that service (the app's own port, or a separate small proxy that serves only that one hostname), restrict the WAN rule (GeoIP alias, logging on), keep the app's own auth and 2FA on, and mount its data read-only if it only needs to read. Better still, use a VPN like Tailscale if only you need access.


Third-party package repositories on the router

What goes wrong. A community plugin repo (or a vendor repo left behind by a plugin you uninstalled) is trusted as root on every package upgrade of the box that sits in front of your whole network.

Do. Remove repos you don't use. For the ones you keep, know who runs them, and prefer the router's native features when they cover the need (OPNsense's built-in Unbound blocklists versus a third-party AdGuard Home plugin, for example).


Devices nobody can name

What goes wrong. The flat default LAN collects things over time: an ESP32 from a weekend project, a smart TV with a randomised MAC, a device still using its factory default IP from another subnet. If that segment can reach everything, unknown devices are exactly what segmentation was supposed to contain.

Do. Give every device a DHCP reservation with a hostname, move IoT and TVs to their own VLAN with one explicit rule per thing they need (the TV to the media server's port), and alert on new MAC addresses (OPNsense's host discovery or your DHCP server's lease log can feed this).


Sources & further reading