VPNSmith
self-host-vpnINFO

Kill switch VPN Linux avec iptables et systemd (2026)

Kill switch netfilter qui drop tout trafic hors tunnel WireGuard. Service systemd ordonné, exception interface VPN, test reconnect. Zéro fuite garantie.

Par Eric Gerard · Fondateur · VPNSmith — Spécialiste self-host VPN & VPS GDPR8 min de lecturePhoto via Unsplash

Tu utilises un VPN self-host. Tu te demandes ce qui se passe si le tunnel tombe pendant 12 secondes au milieu d'un upload sensible. Sur la plupart des configurations par défaut Linux : le trafic passe en clair par ta route par défaut, sans aucune indication visible. Le DNS local fuite, l'IP réelle apparaît dans les logs des serveurs distants. Un kill switch applicatif ne te sauve pas — il faut bloquer ça au niveau kernel.

Ce guide montre une configuration kill switch iptables + systemd qui garantit que rien ne sort de ta machine en clair, même pendant les 50 ms de bascule entre deux handshakes WireGuard.

Le modèle qu'on construit

L'idée centrale en 3 phrases :

  1. Tout le trafic OUTPUT est DROP par défaut sauf si il sort par l'interface VPN (wg0).
  2. Exception : la connexion UDP vers le serveur VPN (handshake) reste autorisée — sans ça le tunnel ne peut pas se rétablir après une coupure.
  3. systemd garantit que ces règles iptables sont appliquées avant WireGuard et survivent au redémarrage du tunnel.

Le résultat : si WireGuard plante, ta machine ne peut littéralement plus rien envoyer sur Internet sauf le handshake VPN. Tu vois "no connection" dans le navigateur, mais aucun paquet ne fuite.

Préparation

Sur Ubuntu/Debian (testé Ubuntu 22.04 et 24.04, kernel 5.15+) :

sudo apt update
sudo apt install -y iptables iptables-persistent
# Au prompt netfilter-persistent : YES pour IPv4 et IPv6

Note les éléments suivants avant de continuer :

  • L'IP publique du VPN : nslookup vpn.example.com ou regarde dans wg0.conf la ligne Endpoint.
  • Le port UDP du VPN : généralement 51820 pour WireGuard.
  • L'interface VPN : wg0 par défaut, peut être wg1 si tu as plusieurs tunnels.
  • Le subnet du LAN local à conserver accessible (192.168.1.0/24, 10.0.0.0/8, etc.) — pour ne pas casser l'imprimante et le NAS.

Règles iptables IPv4

Crée /etc/iptables/rules.v4 avec ce contenu (adapte les valeurs encadrées) :

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]

# Loopback toujours autorisé
-A INPUT  -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT

# Connexions déjà établies (réponses au handshake VPN, etc.)
-A INPUT  -m state --state ESTABLISHED,RELATED -j ACCEPT
-A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# SSH entrant (si tu administres à distance — sinon supprime ces 2 lignes)
-A INPUT  -p tcp --dport 22 -m state --state NEW -j ACCEPT

# Handshake VPN sortant : seul trafic autorisé hors tunnel
-A OUTPUT -p udp -d 1.2.3.4 --dport 51820 -j ACCEPT

# DNS local sur LAN (Pi-hole, routeur) — optionnel
-A OUTPUT -d 192.168.1.0/24 -j ACCEPT

# Tout le reste du trafic OUTPUT passe par l'interface VPN
-A OUTPUT -o wg0 -j ACCEPT

# Permettre les paquets entrants sur wg0
-A INPUT  -i wg0 -j ACCEPT

COMMIT

Remplace :

  • 1.2.3.4 par l'IP publique réelle de ton serveur VPN
  • 51820 par ton port WireGuard
  • 192.168.1.0/24 par ton subnet LAN
  • Supprime la ligne --dport 22 si tu n'as pas besoin d'admin SSH entrant

Charge les règles :

sudo iptables-restore < /etc/iptables/rules.v4
sudo iptables -L -v -n
# Vérifier que les compteurs sont à 0 sur les chains DROP

Règles ip6tables (CRITIQUE)

Sans IPv6 explicitement bloqué, beaucoup de distros activent IPv6 par défaut et le trafic fuite. Crée /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

# Si ton VPN supporte IPv6 (rare en 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

# Sinon, tout IPv6 hors loopback est DROP

COMMIT

Charge :

sudo ip6tables-restore < /etc/iptables/rules.v6

À ce stade tu peux tester : sans le tunnel WireGuard, aucun ping vers Internet ne passe. C'est attendu.

Service systemd pour ordonner WireGuard après iptables

Le souci par défaut : wg-quick@wg0 peut démarrer avant netfilter-persistent. Pendant cette fenêtre (~1-3 secondes), le tunnel est UP mais les règles iptables ne sont pas chargées → fuite possible.

Solution : override systemd qui force l'ordre.

Crée /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

Contenu :

[Unit]
After=netfilter-persistent.service
Wants=netfilter-persistent.service
Requires=netfilter-persistent.service

[Service]
# Si wg-quick plante, on force le DROP de OUTPUT pour éviter toute fuite
ExecStopPost=/sbin/iptables -P OUTPUT DROP
ExecStopPost=/sbin/ip6tables -P OUTPUT DROP

Recharge 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

À chaque boot, systemd démarre netfilter-persistent d'abord (règles iptables chargées), puis wg-quick@wg0 (tunnel monté). Pas de fenêtre de fuite.

Tests de validation

Test 1 — Coupe le tunnel et vérifie le silence

# Tunnel up
sudo wg show
# Doit montrer une dernière handshake récente

# Tester accès Internet
curl -s -m 5 https://ifconfig.me
# Doit retourner l'IP du VPS

# Couper le tunnel
sudo wg-quick down wg0

# Re-tester
curl -s -m 5 https://ifconfig.me
# Doit timeout sans réponse — KILL SWITCH ACTIF

Si tu vois une IP retournée à la deuxième commande → fuite, les règles iptables ne sont pas appliquées. Vérifie sudo iptables -L -v -n et regarde si OUTPUT est bien DROP par défaut.

Test 2 — Simule un crash WireGuard

# Tunnel up
sudo wg-quick up wg0
ping -c 2 1.1.1.1
# Doit ping OK

# Kill brutal du processus WireGuard (simule crash)
sudo pkill -9 wg
sudo ip link delete wg0 2>/dev/null

# Re-test
ping -c 2 1.1.1.1
# Doit fail "Operation not permitted"

Le kernel a refusé le ping car aucune route ne reste autorisée. Sans le ExecStopPost dans le drop-in systemd, certaines distros laissent une fenêtre de fuite après le crash — d'où l'importance de cette ligne.

Test 3 — DNS leak

# Avec le tunnel UP
dig @9.9.9.9 ifconfig.me +short
# Doit retourner l'IP du VPS

# Coupe le tunnel
sudo wg-quick down wg0

# Re-test
dig @9.9.9.9 ifconfig.me +short
# Doit timeout / "no servers could be reached"

Même le DNS est bloqué hors tunnel. C'est l'objectif.

Cas spéciaux

Tu utilises plusieurs tunnels (wg0 + wg1)

Duplique l'exception OUTPUT :

-A OUTPUT -p udp -d 1.2.3.4 --dport 51820 -j ACCEPT  # tunnel principal
-A OUTPUT -p udp -d 5.6.7.8 --dport 51820 -j ACCEPT  # tunnel secondaire
-A OUTPUT -o wg0 -j ACCEPT
-A OUTPUT -o wg1 -j ACCEPT

Tu veux exempter Tailscale en parallèle

Tailscale utilise UDP 41641 par défaut. Ajoute :

-A OUTPUT -p udp --dport 41641 -j ACCEPT
-A OUTPUT -o tailscale0 -j ACCEPT

Le LAN local doit rester accessible (NAS, imprimante)

La règle -A OUTPUT -d 192.168.1.0/24 -j ACCEPT du template le permet déjà. Adapte le subnet si nécessaire.

Tu ne veux pas SSH entrant (poste mobile)

Supprime la ligne --dport 22 -m state --state NEW -j ACCEPT. La connexion existante (déjà ESTABLISHED) reste OK.

Logging des paquets bloqués (debug)

Pour voir ce qui aurait fuit sans le kill switch :

sudo iptables -I OUTPUT 1 -j LOG --log-prefix "OUTPUT-DROP: " --log-level 4

Puis :

sudo journalctl -k -f | grep "OUTPUT-DROP"

Tu verras tous les paquets que ton OS tente d'envoyer en clair quand le tunnel est down (DNS Apple, NTP, Spotify heartbeat, etc.). C'est instructif.

Une fois le debug terminé, supprime la règle LOG (sinon ça remplit journalctl) :

sudo iptables -D OUTPUT -j LOG --log-prefix "OUTPUT-DROP: " --log-level 4

Bonus : kill switch dans le wg0.conf directement

Si tu préfères ne pas gérer iptables séparément, WireGuard accepte des PostUp/PreDown qui font le même travail :

[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

Avantage : zéro fichier supplémentaire. Inconvénient : les règles disparaissent si WireGuard crash sans appeler PreDown (kernel panic, OOM kill). L'approche systemd ci-dessus est plus robuste pour de la production long-terme.

Voir aussi le template "travel kill switch" du guide WireGuard 2026 pour la version embarquée dans wg0.conf.

Vérification finale

# Reboot complet
sudo reboot

# Au redémarrage, sans rien faire
ip a show wg0
# Doit montrer le tunnel UP

iptables -L OUTPUT -v -n | head -5
# Default policy doit être DROP, accept counters doivent croître

curl -s ifconfig.me
# Doit retourner l'IP du VPS

Si tout est vert : ton kill switch est en place, fonctionne au boot, survit aux crashes. Tu peux mettre la machine sur un réseau hostile sans risque de fuite.

Pour aller plus loin

Le kill switch coupe les fuites. Mais pour vraiment être tranquille, ajoute :

Et la base : setup WireGuard sur Contabo en 20 minutes.

★ Datacenter Nuremberg GDPR · ✓ IPv4 dédiée incluse · 200+ Mbps garantis

Voir l'offre Contabo30 jours satisfait ou remboursé