HomeTechOps

Self-Hosting

VPN kill-switch: verify a container's egress IP

Make a container egress only through the VPN and prove it — the network_mode: service:gluetun pattern, the fail-closed firewall, the curl-from-inside egress-IP check, the tunnel-down leak test, and DNS/IPv6 leak traps.

Problem summary

When you route a container through a VPN, the operator concern is leakage: if the tunnel drops, does it silently fall back to your ISP's IP? The standard pattern is a VPN-gateway container (commonly gluetun) with the workload attached via `network_mode: service:gluetun`, so the workload has no other route out — fail-closed by default (tunnel down = no egress). The non-negotiable step is verification: exec into the workload and `curl` an IP-echo service to confirm it returns the VPN IP, not your ISP IP; then stop the gateway and confirm the curl now fails (the kill-switch holds). Allow only your LAN subnet through, and watch the DNS and IPv6 leak paths.

Operator snapshotEvidence first
First proof

Confirm the workload uses network_mode: service:<vpn>.

Screen to open

docker exec <workload> curl -s ifconfig.me # or ipinfo.io/ip

Expected signal

The workload shares the VPN container's network namespace.

Stop boundary

Don't run sensitive traffic until this passes.

Layer path

1Routing a container through a VPN is only safe if it's fail-closed: when the tunnel drops, the container must lose all egress, not silently fall back to your ISP IP. The standard pattern is a VPN-gateway container (commonly gluetun) with the workload attached to its network namespace.
2network_mode: service:gluetun gives the workload no interface of its own except the tunnel's, so its only route out is the VPN. gluetun's firewall blocks non-tunnel outbound by default — that's the kill-switch.
3The non-negotiable step is verification: from inside the workload, curl an IP-echo service and confirm it returns the VPN IP; then stop the gateway and confirm the curl fails.
4Two leak paths remain: DNS (use the tunnel's encrypted resolver; a forced local resolver can leak) and IPv6 (a v4-only tunnel lets host IPv6 egress outside it). LAN access must be allowed explicitly, not by disabling the firewall.
Runbook

Step-by-step runbook

Start here. Do each check in order, compare it to the expected result, and stop when the evidence explains the failure or the safe stop point applies.

1

Attach the workload to the VPN namespace

Check: Set network_mode: service:gluetun on the workload.

Expected result: The workload has no non-tunnel interface.

If not: Remove its own ports/networks that bypass the gateway.

2

Verify the egress IP

Check: Curl an IP-echo from inside the workload.

Expected result: It returns the VPN IP, not your home IP.

If not: If it's your ISP IP, fix routing before continuing.

3

Test fail-closed

Check: Stop gluetun and re-curl from the workload.

Expected result: Egress fails (no IP) while the tunnel is down.

If not: If an IP comes back, the kill-switch is broken — fix the firewall.

Safe stop: Don't run sensitive traffic until this passes.

4

Allow the LAN explicitly

Check: Add your LAN subnet to FIREWALL_OUTBOUND_SUBNETS.

Expected result: The workload reaches local services with the firewall still on.

If not: Never disable the firewall to get LAN access.

5

Close the DNS leak

Check: Keep DoT default or tunnel/encrypt any local resolver.

Expected result: Lookups don't leak to your ISP.

If not: A forced plaintext local DNS is a leak.

6

Close the IPv6 leak

Check: Disable container IPv6 or confirm the tunnel carries v6; verify with curl -6.

Expected result: No v6 egress outside the tunnel.

If not: Host v6 + v4-only tunnel leaks — handle it explicitly.

Decision tree

Decision tree

If: Inside-curl returns your ISP IP

Then: The workload isn't actually on the VPN namespace, or the firewall allows direct egress.

Action: Fix network_mode/firewall; re-verify the egress IP.

Safe stop: Don't run anything sensitive until the egress IP is the VPN's.

If: Tunnel-down test still returns an IP

Then: The kill-switch isn't fail-closed.

Action: Correct the firewall so all non-tunnel egress is blocked.

Safe stop: Treat this as broken until egress fails closed.

If: Workload can't reach LAN services

Then: LAN traffic is blocked by the (correct) default firewall.

Action: Add your LAN subnet to FIREWALL_OUTBOUND_SUBNETS — don't disable the firewall.

If: DNS resolves via a local plaintext server

Then: Lookups can leak even if data goes through the tunnel.

Action: Use the tunnel's DoT default or tunnel/encrypt the local resolver.

If: Host has IPv6, tunnel is IPv4-only

Then: v6 traffic egresses outside the VPN.

Action: Disable container/stack IPv6 or use a v6-capable tunnel; verify with curl -6.

Evidence

Evidence table

SymptomEvidence to collectLikely layerNext action
Inside-curl shows home IPnetwork_mode and gluetun firewall configWorkload not on the VPN namespaceAttach via network_mode: service:gluetun; re-verify.
Egress works with gluetun stoppedResult of the tunnel-down curlKill-switch not fail-closedFix the firewall to block all non-tunnel egress.
Workload can't reach NAS/proxy on LANFIREWALL_OUTBOUND_SUBNETS valueLAN not allowed throughAdd the LAN subnet (keep the firewall on).
ISP resolver shows the lookupsDNS config (DoT vs forced local)DNS leakUse DoT default or encrypt/tunnel local DNS.
curl -6 returns your ISP's v6Host/container IPv6 state vs tunnelIPv6 leakDisable container IPv6 or use a v6 tunnel.
Reference

Commands and settings paths

Verify the egress IP from inside the workload

docker exec <workload> curl -s ifconfig.me # or ipinfo.io/ip

Where: On the Docker host

Expected: Returns the VPN exit IP/geo, not your home IP.

Failure means: Your ISP IP means the workload isn't routed through the tunnel.

Safe next step: Fix network_mode/firewall and re-run.

Test the kill-switch fails closed

docker stop gluetun && docker exec <workload> curl -s --max-time 8 ifconfig.me; docker start gluetun

Where: On the Docker host

Expected: The curl times out / returns nothing while gluetun is down.

Failure means: Any IP returned means egress leaks when the tunnel drops.

Safe next step: Correct the firewall; wait ~10–15s after restart before re-testing.

Check for an IPv6 leak

docker exec <workload> curl -6 -s --max-time 8 ifconfig.co

Where: On the Docker host

Expected: Fails, or shows the VPN's v6 — not your ISP's v6 address.

Failure means: Your ISP's v6 means IPv6 is bypassing the tunnel.

Safe next step: Disable container IPv6 or use a v6-capable tunnel.

Hardware boundary

Hardware and platform boundary

Change only when

  • Adopt the gateway-container pattern the moment any container should be VPN-only.
  • Add the tunnel-down and curl -6 checks to a periodic verification routine, not just initial setup.

Evidence that matters

  • network_mode: service:<vpn> so the workload has no non-tunnel route.
  • A verified VPN egress IP and a tunnel-down test that fails closed.
  • FIREWALL_OUTBOUND_SUBNETS for LAN, DoT for DNS, and IPv6 handled.

Evidence that does not matter

  • Which VPN brand, beyond it working with your gateway (WireGuard/OpenVPN).
  • Raw throughput before correctness — verify no-leak first.

Avoid

  • Trusting the setup without verifying the egress IP.
  • Disabling the firewall to reach the LAN.
  • Ignoring DNS/IPv6 leak paths.

Related tool

Use the linked tool to turn this runbook into a guided check for your exact setup.

Device setup troubleshooter

Related problems

Last reviewed

2026-06-03 · Reviewed by HomeTechOps. Built from 2026-06 research verified against the gluetun project (fail-closed firewall, FIREWALL_OUTBOUND_SUBNETS), its DNS options (DoT default), and WireGuard. Scoped strictly to privacy hygiene and egress verification — no acquisition guidance. The operator differentiator is proving the kill-switch with the inside-curl egress check and the tunnel-down fail-closed test, plus the DNS/IPv6 leak paths.

Sources/assumptions

  • Assumes a Docker Compose stack using a VPN-gateway container (gluetun is the common choice) with a workload container attached via network_mode: service:gluetun; the tunnel is WireGuard or OpenVPN.
  • The fail-closed firewall, FIREWALL_OUTBOUND_SUBNETS LAN allow-through, and DNS-over-TLS default are from the gluetun project; the curl-egress-IP and tunnel-down verification are the standard operator checks.
  • This is strictly privacy hygiene and egress verification — no acquisition/piracy guidance; the point is proving traffic exits where you intend.

Source-backed checks

HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.