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.envand credential files should be0600, not the0644/0664an editor orcpleaves 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
dockergroup 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.sockmounted, 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=falseandDOZZLE_ENABLE_SHELL=falseif it's only for logs). - Bind admin UIs to
127.0.0.1and 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-USERchain if you need host-level rules for published ports — see Firewall. - Enable authentication on brokers and streams: MQTT
allow_anonymous falsewith 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¶
- GitHub docs — Removing sensitive data from a repository
- gitleaks
- Servarr Wiki — Sonarr settings: Authentication
- Dozzle — Actions and shell
- Docker docs — Packet filtering and firewalls
- Mosquitto —
mosquitto.conf(allow_anonymous,password_file) - Zigbee2MQTT — Frontend
auth_token - OPNsense — Unbound DNS blocklists