You use a self-host VPN. You wonder what happens if the tunnel drops for 12 seconds mid-upload of something sensitive. On most default Linux configs: traffic flows clear through your default route, with no visible indication. Local DNS leaks, real IP shows up in remote server logs. An app-level kill switch won't save you — this must be blocked at the kernel level.
This guide sets up an iptables + systemd kill switch that guarantees nothing exits your machine in the clear, even during the 50 ms gap between two WireGuard handshakes.
The model we build
The core idea in 3 sentences:
- All OUTPUT traffic is DROP by default unless it exits via the VPN interface (
wg0). - Exception: the UDP connection to the VPN server (handshake) stays allowed — without it, the tunnel can't re-establish after a drop.
- systemd guarantees those iptables rules apply before WireGuard and survive tunnel restarts.
Result: if WireGuard dies, your machine literally can't send anything except the VPN handshake. You see "no connection" in the browser, but no packet leaks.
Prep
On Ubuntu/Debian (tested Ubuntu 22.04 and 24.04, kernel 5.15+):
sudo apt update
sudo apt install -y iptables iptables-persistent
# At netfilter-persistent prompt: YES for IPv4 and IPv6
Note these items before continuing:
- The VPN public IP:
nslookup vpn.example.comor check theEndpointline inwg0.conf. - The VPN UDP port: usually
51820for WireGuard. - The VPN interface:
wg0by default, can bewg1with multiple tunnels. - The local LAN subnet to keep reachable (192.168.1.0/24, 10.0.0.0/8, etc.) — so printer and NAS don't break.
IPv4 iptables rules
Create /etc/iptables/rules.v4 with this content (adapt boxed values):
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
# Loopback always allowed
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
# Already-established connections (VPN handshake responses, etc.)
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Incoming SSH (if you admin remotely — otherwise drop these 2 lines)
-A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
# Outbound VPN handshake: only out-of-tunnel traffic allowed
-A OUTPUT -p udp -d 1.2.3.4 --dport 51820 -j ACCEPT
# Local LAN DNS (Pi-hole, router) — optional
-A OUTPUT -d 192.168.1.0/24 -j ACCEPT
# Everything else OUTPUT goes via VPN interface
-A OUTPUT -o wg0 -j ACCEPT
# Allow incoming on wg0
-A INPUT -i wg0 -j ACCEPT
COMMIT
Replace:
1.2.3.4with your VPN server's actual public IP51820with your WireGuard port192.168.1.0/24with your LAN subnet- Drop the
--dport 22line if you don't need inbound SSH admin
Load rules:
sudo iptables-restore < /etc/iptables/rules.v4
sudo iptables -L -v -n
# Confirm DROP chain counters are at 0
ip6tables rules (CRITICAL)
Without IPv6 explicitly blocked, many distros enable it by default and traffic leaks. Create /etc/iptables/rules.v6:
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# If your VPN supports IPv6 (rare in self-host):
# -A OUTPUT -p udp -d 2001:db8::1 --dport 51820 -j ACCEPT
# -A OUTPUT -o wg0 -j ACCEPT
# -A INPUT -i wg0 -j ACCEPT
# Otherwise all IPv6 outside loopback is DROP
COMMIT
Load:
sudo ip6tables-restore < /etc/iptables/rules.v6
At this point you can test: without the WireGuard tunnel, no ping to the Internet passes. Expected.
systemd service ordering for WireGuard after iptables
Default issue: wg-quick@wg0 may start before netfilter-persistent. During that window (~1-3 seconds), the tunnel is UP but iptables rules aren't loaded → possible leak.
Solution: systemd override forcing order.
Create /etc/systemd/system/wg-quick@wg0.service.d/killswitch.conf:
sudo mkdir -p /etc/systemd/system/wg-quick@wg0.service.d
sudo nano /etc/systemd/system/wg-quick@wg0.service.d/killswitch.conf
Content:
[Unit]
After=netfilter-persistent.service
Wants=netfilter-persistent.service
Requires=netfilter-persistent.service
[Service]
# If wg-quick crashes, force OUTPUT DROP to prevent any leak
ExecStopPost=/sbin/iptables -P OUTPUT DROP
ExecStopPost=/sbin/ip6tables -P OUTPUT DROP
Reload systemd:
sudo systemctl daemon-reload
sudo systemctl enable wg-quick@wg0
sudo systemctl enable netfilter-persistent
sudo systemctl restart netfilter-persistent
sudo systemctl restart wg-quick@wg0
At each boot, systemd starts netfilter-persistent first (iptables loaded), then wg-quick@wg0 (tunnel up). No leak window.
Validation tests
Test 1 — Drop the tunnel, verify silence
# Tunnel up
sudo wg show
# Must show a recent handshake
# Test Internet access
curl -s -m 5 https://ifconfig.me
# Must return VPS IP
# Drop tunnel
sudo wg-quick down wg0
# Re-test
curl -s -m 5 https://ifconfig.me
# Must time out with no response — KILL SWITCH ACTIVE
If you see an IP on the second command → leak, iptables rules aren't applied. Check sudo iptables -L -v -n and confirm OUTPUT default is DROP.
Test 2 — Simulate a WireGuard crash
# Tunnel up
sudo wg-quick up wg0
ping -c 2 1.1.1.1
# Must ping OK
# Brutal kill of WireGuard process (simulate crash)
sudo pkill -9 wg
sudo ip link delete wg0 2>/dev/null
# Re-test
ping -c 2 1.1.1.1
# Must fail "Operation not permitted"
The kernel refuses the ping because no route remains allowed. Without ExecStopPost in the systemd drop-in, some distros leave a leak window after the crash — that line matters.
Test 3 — DNS leak
# Tunnel UP
dig @9.9.9.9 ifconfig.me +short
# Must return VPS IP
# Drop tunnel
sudo wg-quick down wg0
# Re-test
dig @9.9.9.9 ifconfig.me +short
# Must timeout / "no servers could be reached"
Even DNS is blocked outside the tunnel. Goal achieved.
Special cases
You run several tunnels (wg0 + wg1)
Duplicate the OUTPUT exception:
-A OUTPUT -p udp -d 1.2.3.4 --dport 51820 -j ACCEPT # main tunnel
-A OUTPUT -p udp -d 5.6.7.8 --dport 51820 -j ACCEPT # secondary tunnel
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -o wg1 -j ACCEPT
Run Tailscale in parallel
Tailscale uses UDP 41641 by default. Add:
-A OUTPUT -p udp --dport 41641 -j ACCEPT
-A OUTPUT -o tailscale0 -j ACCEPT
Keep local LAN reachable (NAS, printer)
The template rule -A OUTPUT -d 192.168.1.0/24 -j ACCEPT already does that. Adapt the subnet.
You don't want inbound SSH (mobile workstation)
Drop the --dport 22 -m state --state NEW -j ACCEPT line. Existing (ESTABLISHED) connection stays OK.
Logging dropped packets (debug)
To see what would've leaked without the kill switch:
sudo iptables -I OUTPUT 1 -j LOG --log-prefix "OUTPUT-DROP: " --log-level 4
Then:
sudo journalctl -k -f | grep "OUTPUT-DROP"
You'll see every packet your OS tries to send in the clear when the tunnel is down (Apple DNS, NTP, Spotify heartbeat, etc.). Educational.
Once done debugging, remove the LOG rule (else journalctl fills up):
sudo iptables -D OUTPUT -j LOG --log-prefix "OUTPUT-DROP: " --log-level 4
Bonus: kill switch inside wg0.conf directly
If you prefer not to manage iptables separately, WireGuard accepts PostUp/PreDown doing the same job:
[Interface]
# ...
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp = ip6tables -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
PreDown = ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
Pro: zero extra file. Con: rules vanish if WireGuard crashes without calling PreDown (kernel panic, OOM kill). The systemd approach above is more robust for long-term prod.
Also see the travel kill-switch template in the WireGuard 2026 guide for the inline-wg0.conf version.
Final check
# Full reboot
sudo reboot
# After reboot, no manual steps
ip a show wg0
# Must show tunnel UP
iptables -L OUTPUT -v -n | head -5
# Default policy must be DROP, accept counters must grow
curl -s ifconfig.me
# Must return VPS IP
All green: your kill switch is in place, runs at boot, survives crashes. You can plug the machine into a hostile network without leak risk.
Going further
The kill switch blocks leaks. But for genuine peace of mind, add:
- Prometheus + Grafana monitoring for your VPS VPN — real-time tunnel status, throughput, connected peers.
- Split-tunnel with routing tables — useful when some apps should bypass the VPN (Steam downloads, Time Machine to local NAS) while the rest stays tunneled.
- Ready-to-use WireGuard templates — 8 production-tested configurations.
And the foundation: WireGuard setup on Contabo in 20 minutes.
★ Datacenter Nuremberg GDPR · ✓ IPv4 dédiée incluse · 200+ Mbps garantis
Get Contabo30 jours satisfait ou remboursé→