Writing -o tun+ feels like a precise instruction and is really a wildcard across every tunnel the kernel has. A second VPN client, a container bridge, or an interface left behind by a crashed session all satisfy it, and the host then permits traffic your policy was written to prevent. The runbook below pins one device name, orders the exceptions so the tunnel can actually form, and leaves you a way back if the rules land badly. Each step is short enough to undo on its own.

Step 1: record the rules you have now

Before changing a default policy, save the current state so the rollback is a command rather than a memory. iptables-save > /root/iptables.pre writes every table to a file you can restore, and a second terminal with an open root session is not paranoia when the policy you are about to set drops the connection carrying it. If you are working remotely, schedule a flush as a dead man switch as well.

Then list the interfaces, because a name you assumed is a name you should have read: ip -br link shows what exists right now, and the tunnel will not appear until it is up. Write the device names down as you read them.

Step 2: give the tunnel its own name

OpenVPN accepts dev tun_work in the client configuration, and Linux then creates an interface with exactly that name instead of allocating tun0. The name stays stable across reconnects as long as nothing else claims it, which is what makes a rule that names the device meaningful.

Where the name cannot be controlled, match on the tunnel address or a packet mark instead. Both work, both are harder to read, and both fail more quietly when the tunnel renumbers. Either way, match one thing and be explicit about which.

Step 3: write the policy with the endpoint exception first

A VPN-only host is a default-deny host with a short exception list, and the exception that matters most is the one that lets the tunnel exist. Build it in this order, with the endpoint rule before the tunnel-device rule, so the handshake has somewhere to go:

# allow the tunnel to be built, then everything the tunnel carries
iptables -P INPUT DROP
iptables -P OUTPUT DROP
iptables -A INPUT  -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -d 198.51.100.7 -p udp --dport 1194 -j ACCEPT
iptables -A OUTPUT -o tun_work -j ACCEPT
iptables -A INPUT  -i tun_work -j ACCEPT
iptables -A INPUT  -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

The literal endpoint address and port belong to your provider, not to this page. If the endpoint is a hostname, resolve it once and pin the result, because a rule cannot look up a name and a rotating answer will silently break the policy after the next DNS change. Check the port too, since providers that move you to TCP 443 will otherwise need a rule nobody wrote down. A tunnel that only works on one protocol is usually this omission rather than a firewall fault.

On a router the same shape goes into the FORWARD chain for the clients behind it, and the router's own traffic needs its own rules. A ruleset that covers only forwarded packets leaves SSH and DNS on the old path, which is how a router becomes unreachable the moment the tunnel comes up.

Step 4: check the order, not just the presence

Rules evaluate in sequence, so an ACCEPT that comes earlier simply hides the REJECT you added afterwards. The established and related match is what lets replies to your own outbound requests return without opening a general inbound path, and placing it after the tunnel accept means a dropped tunnel still passes return traffic on its own interface.

Keep the rule count small enough to read in one screen. A ruleset you cannot audit is one you will patch by adding exceptions to the top, which is how default-deny policies gradually acquire a default-allow edge. Count the ACCEPTs before and after the change.

Step 5: make it survive the boot and the address family

Rules entered by hand vanish at reboot, so load them from the distribution's firewall service or a unit that runs after the tunnel interface exists. Referencing tun_work in a script that executes before OpenVPN starts is a common way to produce a boot loop of dropped traffic.

Then check IPv6 separately, since a good IPv4 default-deny policy says nothing about the other family. ip -6 route will tell you whether the host has a route that could carry traffic around a tunnel that only handles IPv4, and ip6tables or nftables needs its own equivalent rules. Test with an IPv6-only destination rather than trusting the symmetry of the ruleset.

Step 6: verify and be ready to restore

Bring the tunnel up, confirm your intended service works, then try something the policy should refuse. Restore with iptables-restore < /root/iptables.pre if the answer surprises you, and only then start editing rules. Persistence plus a tested restore path is what makes this a change you can make twice.

Sɛbɛn minnu bɛ tali kɛ