← All Articles

Setting Up a VPN Kill Switch for Your Homelab

A VPN kill switch blocks all traffic if the VPN connection drops. Here's how to implement one for specific containers or your whole server, and why it matters.

networkingvpnprivacydockersecurity

A VPN without a kill switch is a privacy tool with a failure mode. If the VPN drops — connection hiccup, server restart, configuration error — your traffic falls back to your real IP and continues flowing unprotected. For casual browsing, that might not matter. For containers you’re routing through a VPN specifically because you don’t want traffic leaking, it defeats the purpose.

A kill switch blocks all outbound traffic if the VPN interface goes down. No fallback. No leaks. Just a dropped connection until the VPN comes back.

Here’s how to implement this at two levels: for specific Docker containers, and for your whole host.

Why Per-Container VPN Makes Sense

Running your entire homelab through a VPN isn’t usually what you want. Your Nextcloud, Jellyfin, and Vaultwarden instances need to be reachable from outside. Routing all of that through a VPN exit node adds complexity and can break services.

The typical use case is specific containers — a download client like qBittorrent or Transmission, or a container you’re using to scrape data — where you specifically don’t want your home IP appearing in connection logs.

Option 1: gluetun — VPN Container with Built-In Kill Switch

gluetun is the standard solution for per-container VPN in Docker. It’s a VPN client container that other containers route their traffic through. If the VPN connection drops, gluetun blocks all outbound traffic from containers using it as a network proxy.

It supports WireGuard and OpenVPN, and works with most major VPN providers including Mullvad, ProtonVPN, ExpressVPN, NordVPN, and more.

version: "3.8"
services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=your-private-key-here
      - WIREGUARD_ADDRESSES=10.x.x.x/32
      - SERVER_COUNTRIES=Netherlands
    ports:
      - "8080:8080"   # Port for the app running inside gluetun's network
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"  # This is the key line
    environment:
      - PUID=1000
      - PGID=1000
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent-config:/config
      - ./downloads:/downloads
    restart: unless-stopped

The critical line is network_mode: "service:gluetun". This routes all of qbittorrent’s network traffic through the gluetun container’s network stack. When gluetun is connected to the VPN, qbittorrent traffic exits through the VPN. When gluetun’s VPN drops, the kill switch fires and qbittorrent has no network path.

Ports exposed by the gluetun container (like the WebUI port) are accessible from your LAN, so you can still manage the app. Only the outbound traffic is VPN-gated.

Mullvad and ProtonVPN have good gluetun support and offer WireGuard credentials per-device. For a homelab VPN client, these are the cleanest options.

Option 2: WireGuard with iptables Kill Switch (Host-Level)

If you want VPN on the host itself with a kill switch, WireGuard is the right protocol. The kill switch is implemented with iptables rules that:

  1. Allow traffic only through the WireGuard interface
  2. Allow the WireGuard handshake traffic to the server IP
  3. Block everything else

Install WireGuard and create your config at /etc/wireguard/wg0.conf:

[Interface]
Address = 10.x.x.x/32
PrivateKey = your-private-key
DNS = 1.1.1.1

# Kill switch rules
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT

[Peer]
PublicKey = server-public-key
AllowedIPs = 0.0.0.0/0
Endpoint = vpn-server-ip:51820
PersistentKeepalive = 25

The PostUp and PreDown lines are the kill switch. When the WireGuard interface comes up, the iptables rule is added, blocking any traffic that doesn’t go through wg0. When WireGuard goes down, the rule is removed — but since WireGuard is down, there’s no traffic flowing anyway.

Bring it up with:

sudo wg-quick up wg0

Enable at boot:

sudo systemctl enable wg-quick@wg0

Test the kill switch by checking your IP before and after bringing WireGuard up, and verify that traffic stops if you bring the interface down manually with sudo wg-quick down wg0.

Verifying It Works

The simplest verification: with the VPN connected, check curl https://ifconfig.me — you should see the VPN server’s IP. Then bring the VPN interface down and try again — the request should hang or fail rather than returning your real IP.

For gluetun specifically, you can exec into the container and check from inside:

docker exec -it gluetun sh
wget -qO- https://ifconfig.me

If you see the VPN IP, traffic is routing correctly.

The Broader Point

Kill switches are one of those features that seem paranoid until the one time they matter. A VPN connection dropping for 30 seconds during a cron job can expose your home IP in server logs. If your use case is specifically about not exposing that IP, the kill switch is what makes the VPN actually work as advertised.

Both approaches above — gluetun for per-container, iptables for host-level — are production-quality implementations that homelab users run at scale. Pick the one that fits your setup.