You flip on a VPN, your SSH session freezes, and your production box becomes a very expensive paperweight.
Somewhere between UFW’s “helpful” abstractions and nftables’ brutally honest packet filtering, you just
firewalled your own lifeline.
This is a field guide for the real failure modes: routing surprises, state tracking errors, forwarded-traffic
gaps, and the classic “I allowed the VPN port, so why is nothing working?” moment. We’ll diagnose quickly,
fix precisely, and keep you from learning these lessons at 3 a.m.
The mental model: what changes when a VPN comes up
A VPN is not “an app that encrypts traffic.” On Linux, it’s a set of network interfaces, routes, firewall rules,
and often DNS changes. When it comes up, it usually does three things that can break you:
-
It creates a new interface (e.g.,
wg0,tun0) that becomes the new path for some or all traffic.
If your firewall is interface-specific (many are), you just moved traffic out from under your existing rules. -
It changes routing (default route, policy routing, or per-prefix routes). Your reply packets might leave via a different
interface than the one the request arrived on. Conntrack will not save you from asymmetric routing if you
shoot it in the knee. -
It changes address translation needs (NAT/masquerade) when traffic is forwarded through the box or when the VPN assigns
addresses not known to upstream routers.
UFW and nftables don’t “understand VPNs.” They understand packets, interfaces, states, and routes. If you
configure a VPN and then hope the firewall “figures it out,” you’re betting your access on wishful thinking.
Dry truth: most lockouts are not caused by a single bad rule. They’re caused by a correct rule applied to the
wrong interface or to the wrong direction (INPUT vs OUTPUT vs FORWARD), combined with a route you didn’t notice
got replaced.
Interesting facts and historical context (so the weirdness makes sense)
-
iptables was the default for years, but nftables is the modern replacement; many Ubuntu/Debian systems now run
iptables commands through the nft backend without you realizing it. -
UFW is a frontend originally designed to make iptables approachable; it doesn’t expose every nuance of packet filtering,
especially around policy routing and complex forwarding. -
Conntrack (state tracking) is a kernel subsystem that remembers flows. If you forget to allow
ESTABLISHED,RELATED,
you’ll block replies even if the initial inbound packet was permitted. -
WireGuard is “just UDP” with a kernel interface and cryptokey routing. It doesn’t use a separate control channel like
many older VPNs, which changes what “allowed traffic” looks like. -
OpenVPN often uses a tun/tap device and can push routes and DNS. If you accept the pushed default route, you can easily
route SSH replies into the tunnel by accident. -
Reverse path filtering (rp_filter) is a kernel anti-spoofing feature that can drop packets when routes look asymmetric—
exactly what some VPN setups create intentionally. -
Linux policy routing (ip rule) has existed for decades. VPN clients increasingly use it for split tunneling, which means
your “default route” might not tell the full story. -
“Kill switch” behavior (block traffic outside VPN) is basically a firewall policy decision, not a VPN feature. Many clients
implement it with rules that are easy to get wrong.
Fast diagnosis playbook
When you lose connectivity after enabling a VPN (or traffic mysteriously stops), you don’t have time for a
philosophical debate about firewalls. Check these in order, because each step narrows the problem fast.
First: did routing change under your feet?
- Look at the default route and policy rules (
ip route,ip rule). - Confirm which interface your SSH source IP uses to reach you (
ip route get). - Decide: route problem or firewall problem? If replies go out the wrong interface, fix routing first.
Second: are you dropping on INPUT/OUTPUT or FORWARD?
- Check UFW status and default policies (
ufw status verbose). - Check nftables ruleset counters and chain policies (
nft list ruleset). - Decide: lockout is usually INPUT/OUTPUT; “VPN works but LAN behind server doesn’t” is usually FORWARD/NAT.
Third: is conntrack/state handling correct?
- Verify established/related acceptance exists in the right chains.
- Look for “drop invalid” rules that are too aggressive.
- Decide: if replies are blocked, you’ll see SYNs arrive but no SYN-ACK leaving.
Fourth: is rp_filter or sysctl blocking asymmetric paths?
- Check
rp_filtersettings and adjust for VPN use cases. - Decide: if packets arrive but are dropped before firewall counters move, suspect rp_filter.
Fifth: confirm with packet capture, not vibes
- Run tcpdump on the physical NIC and the VPN interface simultaneously.
- Decide: if packets enter but don’t leave, firewall/routing. If they never enter, upstream/network problem.
Hands-on tasks (commands, outputs, decisions)
These are the exact drills I run on Ubuntu/Debian when someone says “VPN killed the box.” Each task includes
what the output usually means and the decision you make from it. Run them from console, out-of-band access,
or an existing SSH session before you touch anything else.
Task 1: Identify interfaces and what came up with the VPN
cr0x@server:~$ ip -brief link
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
ens3 UP 52:54:00:12:34:56 <BROADCAST,MULTICAST,UP,LOWER_UP>
wg0 UNKNOWN 3a:2b:1c:0d:ee:ff <POINTOPOINT,NOARP,UP,LOWER_UP>
Meaning: You have a physical NIC (ens3) and a WireGuard interface (wg0).
Many lockouts happen because rules apply to ens3 only.
Decision: If a new interface exists, audit firewall rules for interface scoping (iifname/oifname, UFW route rules).
Task 2: Check IP addresses to see what networks are involved
cr0x@server:~$ ip -brief addr
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 203.0.113.10/24 2001:db8:10::10/64
wg0 UNKNOWN 10.6.0.2/32
Meaning: Public IP on ens3, VPN address on wg0.
Note the /32 on WireGuard—common and fine, but it changes routing/NAT expectations.
Decision: If you expect to route other subnets through this host, you’ll need explicit routes and often NAT.
Task 3: Find the default route and whether it moved into the VPN
cr0x@server:~$ ip route show
default dev wg0 scope link
203.0.113.0/24 dev ens3 proto kernel scope link src 203.0.113.10
10.6.0.0/24 dev wg0 proto kernel scope link src 10.6.0.2
Meaning: Default route now goes via wg0. That’s a full-tunnel setup.
If your SSH client connects to 203.0.113.10, the request arrives via ens3, but your replies might try to leave via wg0.
Decision: If this is a server you manage remotely, do not accept full-tunnel routing unless you’ve explicitly pinned SSH
responses or allowed the path. Consider split tunnel or policy routing for management traffic.
Task 4: Check policy routing rules (split tunnel often lives here)
cr0x@server:~$ ip rule show
0: from all lookup local
32764: from all fwmark 0xca6c lookup 51820
32766: from all lookup main
32767: from all lookup default
Meaning: Some traffic is marked (fwmark 0xca6c) and uses a special routing table. That’s typical of WireGuard “wg-quick”
setups for policy routing or kill switch modes.
Decision: If you see marks/rules you didn’t add, inspect the VPN client’s PostUp/PreDown hooks or systemd units.
Your firewall must allow marked traffic out the correct interface, or you create black holes.
Task 5: Confirm what route the kernel will use for your admin IP
cr0x@server:~$ ip route get 198.51.100.25
198.51.100.25 dev wg0 src 10.6.0.2 uid 0
cache
Meaning: Replies to your admin workstation IP would go out via wg0 using source 10.6.0.2.
That’s a classic “SSH dies after VPN up” signature.
Decision: Add a host route or policy rule so management traffic returns via ens3, or keep full-tunnel off on servers.
Task 6: Check UFW status, defaults, and whether forwarding is allowed
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
22/tcp ALLOW IN 198.51.100.0/24
51820/udp ALLOW IN Anywhere
Meaning: Routed/forwarded traffic is disabled. That breaks “server routes LAN through VPN” setups.
Also, SSH is only allowed from a specific subnet; if your source IP changes when you connect through a VPN or bastion, you can lock yourself out.
Decision: If you need forwarding, enable routed policy and add explicit ufw route allow rules. If you need resilient admin access,
keep at least one known-safe management path (console, secondary IP, or a broader allowlist with MFA).
Task 7: See what UFW actually generated (the “before rules” are where magic happens)
cr0x@server:~$ sudo grep -nE 'DEFAULT_FORWARD_POLICY|ufw-before-input|ufw-before-output|wg0|tun0' /etc/default/ufw /etc/ufw/before.rules
/etc/default/ufw:9:DEFAULT_FORWARD_POLICY="DROP"
/etc/ufw/before.rules:20:*filter
/etc/ufw/before.rules:28:-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Meaning: Forwarding default is DROP. Conntrack established is present (good).
Decision: If you intend to route traffic through this host, change DEFAULT_FORWARD_POLICY and add narrow route rules
rather than making everything permissive.
Task 8: Check nftables ruleset and chain policies (Ubuntu may be using nft underneath)
cr0x@server:~$ sudo nft list ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iifname "lo" accept
tcp dport 22 ip saddr 198.51.100.0/24 accept
udp dport 51820 accept
counter drop
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
Meaning: Input policy is DROP, output is ACCEPT. Forward is DROP. If your VPN requires forwarding between interfaces, it will fail.
Also note that SSH is restricted by source IP; VPN changes can alter what “source” means.
Decision: Don’t touch rules blindly. First confirm whether your broken flow is INPUT, OUTPUT, or FORWARD, then add the smallest possible accept.
Task 9: Look at rule counters to see what’s being dropped
cr0x@server:~$ sudo nft -a list chain inet filter input
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iifname "lo" accept
tcp dport 22 ip saddr 198.51.100.0/24 accept
udp dport 51820 accept
counter packets 41 bytes 2460 drop # handle 12
}
}
Meaning: The drop counter is moving. Packets are arriving and being discarded by the default policy or final drop rule.
Decision: Add temporary logging for drops, or temporarily allow from your current source to regain access, then tighten.
Task 10: Confirm whether rp_filter is dropping packets before they hit your firewall
cr0x@server:~$ sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.ens3.rp_filter net.ipv4.conf.wg0.rp_filter
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.ens3.rp_filter = 1
net.ipv4.conf.wg0.rp_filter = 1
Meaning: Strict-ish reverse path filtering is enabled. With asymmetric routing (common with VPN + public access), Linux may drop
packets because the “best return path” doesn’t match the incoming interface.
Decision: For multi-homed/VPN gateways, set rp_filter to 2 (loose) on relevant interfaces, or disable carefully where needed.
Do this intentionally; don’t blanket-disable security controls without understanding the topology.
Task 11: Verify that IP forwarding is enabled when you expect routing
cr0x@server:~$ sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
Meaning: The host will not forward IPv4 or IPv6 packets. Even perfect firewall rules won’t make it route.
Decision: If this host should be a gateway (LAN-to-VPN, site-to-site, etc.), enable forwarding and persist it in sysctl.
Task 12: Check whether NAT/masquerade exists for outbound VPN traffic
cr0x@server:~$ sudo nft list table ip nat
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
}
}
Meaning: NAT table exists but has no masquerade rule. If you’re forwarding a private LAN through the VPN, remote side may not know how to route back.
Decision: Add targeted masquerade for the LAN source network going out wg0 (or better: add routes on the far side if you control it).
Task 13: Use tcpdump to see where the flow dies
cr0x@server:~$ sudo tcpdump -ni ens3 tcp port 22 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on ens3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:01:03.101234 IP 198.51.100.25.53122 > 203.0.113.10.22: Flags [S], seq 1234567890, win 64240, options [mss 1460], length 0
12:01:04.104567 IP 198.51.100.25.53122 > 203.0.113.10.22: Flags [S], seq 1234567890, win 64240, options [mss 1460], length 0
Meaning: SYNs arrive on ens3. If you don’t see SYN-ACK leaving on ens3, either firewall drops INPUT, or replies are routed elsewhere.
Decision: Run tcpdump on wg0 too. If SYN-ACK leaves via wg0, you have a routing/policy routing problem, not an inbound filter problem.
Task 14: Inspect systemd units and VPN “PostUp” hooks that modify firewall/routing
cr0x@server:~$ systemctl status wg-quick@wg0 --no-pager
● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)
Active: active (exited) since Fri 2025-12-27 11:59:12 UTC; 3min ago
Docs: man:wg-quick(8)
man:wg(8)
Process: 1256 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
Meaning: wg-quick is in play. It can add routes, ip rules, and nft/iptables snippets depending on your config.
Decision: Review /etc/wireguard/wg0.conf for PostUp/PreDown commands and for AllowedIPs that might be hijacking the default route.
Task 15: Validate UFW logging and review recent kernel drops
cr0x@server:~$ sudo journalctl -k -g 'UFW BLOCK' -n 5 --no-pager
Dec 27 12:00:41 server kernel: [UFW BLOCK] IN=ens3 OUT= MAC=52:54:00:12:34:56 SRC=198.51.100.25 DST=203.0.113.10 LEN=60 TOS=0x00 PREC=0x00 TTL=51 ID=55233 DF PROTO=TCP SPT=53122 DPT=22 WINDOW=64240 SYN
Meaning: UFW is explicitly blocking inbound SSH from your current source. That’s not “VPN broke it”; your allowlist is now wrong.
Decision: Temporarily broaden SSH access from a safe range or jump host, regain control, then redesign management access so it survives VPN changes.
Task 16: Emergency safety net—schedule a firewall rollback
cr0x@server:~$ echo "ufw disable" | sudo at now + 2 minutes
warning: commands will be executed using /bin/sh
job 7 at Fri Dec 27 12:04:00 2025
Meaning: You’ve scheduled an automatic disable of UFW in 2 minutes. If you lock yourself out, the box will eventually come back.
Decision: Use this before applying risky firewall changes on remote systems. Cancel the job once you verify connectivity.
Joke #1: Firewalls are like seatbelts—until you try to climb out the window while the car is moving.
Common mistakes: symptoms → root cause → fix
1) “SSH dies immediately when I bring up the VPN”
Symptoms: Existing SSH session freezes; new SSH connections time out; VPN interface comes up cleanly.
Root cause: Default route moved to wg0/tun0, so SSH replies go into the tunnel with the wrong source IP; or rp_filter drops asymmetric traffic.
Fix: Pin management routes via the public interface or use policy routing so traffic to your admin subnet uses ens3. Set rp_filter to loose where appropriate.
cr0x@server:~$ sudo ip route add 198.51.100.0/24 via 203.0.113.1 dev ens3
cr0x@server:~$ sudo sysctl -w net.ipv4.conf.ens3.rp_filter=2
net.ipv4.conf.ens3.rp_filter = 2
2) “VPN connects, but nothing can reach the internet”
Symptoms: Tunnel is up; handshake works; DNS might resolve; no outbound traffic passes.
Root cause: A kill switch or over-tight OUTPUT chain blocks everything except the VPN UDP port. Common with “default deny outgoing” policies that didn’t whitelist wg0 traffic.
Fix: Allow outbound on the VPN interface, and keep OUTPUT rules stateful. If using UFW, add explicit allow out rules and verify route table selection.
cr0x@server:~$ sudo ufw allow out on wg0
Rule added
3) “WireGuard handshake works, but no traffic passes”
Symptoms: wg show shows latest handshake time updating; ping/ssh through tunnel fails.
Root cause: AllowedIPs mismatch, missing routes, or firewall blocks FORWARD between interfaces; sometimes MTU/PMTU issues but start with routes/rules.
Fix: Verify AllowedIPs and routes; permit forwarding; ensure NAT if needed.
cr0x@server:~$ sudo wg show wg0
interface: wg0
public key: 3m...redacted...9Q=
listening port: 51820
peer: 8A...redacted...k=
endpoint: 192.0.2.50:51820
allowed ips: 10.6.0.0/24
latest handshake: 1 minute, 4 seconds ago
transfer: 92.14 KiB received, 88.22 KiB sent
4) “My VPN server works for the box itself, but clients can’t reach the LAN behind it”
Symptoms: Server can ping over tunnel; clients connect; clients can’t reach internal subnets.
Root cause: FORWARD chain default drop (UFW “routed disabled”), IP forwarding off, missing masquerade, or missing routes on LAN side.
Fix: Enable IP forwarding; set UFW forwarding policy; add ufw route allow; add targeted NAT or routes on the LAN router.
5) “UFW says it allows 51820/udp, but VPN still doesn’t connect”
Symptoms: Client can’t handshake; server shows nothing; UFW looks “correct.”
Root cause: Rule exists in IPv4 but not IPv6 (or vice versa); the endpoint is v6; or nftables is active with a different ruleset than you think.
Fix: Verify listening socket addresses; check both families; confirm actual firewall backend; test with tcpdump on the NIC.
6) “Everything works… until I enable UFW”
Symptoms: VPN is fine with firewall off; breaks when firewall on; often only breaks forwarding.
Root cause: UFW defaults: routed traffic disabled; missing ufw route rules; overly specific interface rules; missing allow for VPN interface.
Fix: Treat forwarding as a separate product. Configure DEFAULT_FORWARD_POLICY, add explicit route rules, and test flows end-to-end.
7) “DNS breaks only when VPN is up”
Symptoms: You can ping IPs; hostnames don’t resolve; or resolution is slow/flaky.
Root cause: VPN client pushes DNS; systemd-resolved changes upstream; firewall blocks UDP/TCP 53 on the wrong interface; or you’re forcing all traffic into the tunnel but DNS is outside.
Fix: Decide where DNS should live (inside VPN or outside). Then allow it explicitly and verify resolvectl and routing to DNS servers.
8) “After adding a ‘simple’ nftables ruleset, UFW stopped working”
Symptoms: UFW commands succeed, but behavior doesn’t match; counters don’t move where expected.
Root cause: Competing rulesets or chain priorities. UFW may manage its own tables/chains; your custom nft rules might shadow them or apply earlier.
Fix: Pick one controller. Either manage nftables directly (and disable UFW), or let UFW own filtering and only add supported hooks.
Three corporate mini-stories from the trenches
Mini-story #1: The incident caused by a wrong assumption
A mid-size company ran a small fleet of Ubuntu gateways that terminated site-to-site VPNs. A team added WireGuard
to replace an older OpenVPN setup. The migration plan looked clean: bring up wg0, allow UDP 51820 inbound,
and call it a day.
The wrong assumption was subtle: “If the handshake works, the data plane will work.” Handshakes did work. The
graphs looked reassuring. Then internal users started complaining that only some destinations were reachable.
The on-call engineer saw “latest handshake: 30 seconds ago” and wasted time chasing MTU ghosts.
The real issue was forwarding. UFW had routed disabled. The box itself could reach remote subnets because OUTPUT was allowed,
but packets from the LAN were being dropped in FORWARD. The VPN wasn’t broken; the gateway function was.
The fix wasn’t heroic. They enabled IP forwarding, set UFW forwarding policy, and added narrow route rules:
only the internal subnet to the VPN subnet, only via wg0. Suddenly everything worked, and the “WireGuard is flaky”
narrative died quietly.
Mini-story #2: The optimization that backfired
Another org standardized on nftables and wanted “clean, minimal rules.” Someone rewrote the server firewall as a
single policy drop ruleset with only a handful of accepts: SSH, the VPN port, and established/related. They also
tightened OUTPUT to a whitelist because “servers shouldn’t talk to the internet.”
The optimization was removing what looked like redundant state rules and relying on the fact that “we only have
a few services.” It worked in staging. Production had one extra twist: the VPN client used policy routing and
fwmarks, and the DNS resolver sat outside the VPN path. OUTPUT drops started blocking DNS and the VPN’s own keepalives
in ways that weren’t obvious from the initial rules review.
The failure mode was not a total outage; it was worse. Intermittent behavior. Some requests succeeded when caches
were warm, then failed when TTLs expired. People blamed the VPN provider, then blamed DNS, then blamed “Linux networking.”
Classic.
The fix was to stop optimizing prematurely and model the flows. They created explicit OUTPUT allowances for the VPN interface,
for DNS to the selected resolvers, and for NTP. They also added counters and logging for the final drop, because a silent drop
is how you get a week-long incident report.
Mini-story #3: The boring but correct practice that saved the day
A financial services team maintained Debian bastion hosts with strict firewall policies and a mandatory VPN for admin access.
They had a rule: any firewall/VPN change must include an automated rollback timer and an out-of-band access plan.
Nobody loved this rule. Everyone benefited from it.
During a change window, an engineer adjusted UFW rules to enforce a VPN kill switch. The intended behavior: only allow
outbound traffic on wg0, block everything else. They applied the change and immediately lost their SSH session.
Predictable, but still stressful.
Two minutes later, the scheduled rollback fired and UFW disabled itself. SSH came back. No frantic data center calls,
no “can someone reboot it,” no risky half-edits via a half-dead session. They reconnected, fixed the policy routing
exception for the management subnet, and tried again—this time with testing.
Boring practice, big payoff. It also changed team culture: people became more willing to improve security because
the recovery plan removed the fear tax.
Joke #2: The fastest way to prove you have a firewall is to accidentally demonstrate it on yourself.
Checklists / step-by-step plan (safe rollouts)
Checklist A: Before you enable a VPN on a remote server
- Get a recovery path. Console access, IPMI, cloud serial console, or at least a second SSH path from a different network.
- Schedule an automatic rollback. Use
atto disable UFW or restore nftables after 2–5 minutes. - Snapshot configuration. Copy
/etc/ufw, nft rules, and VPN configs to a safe place. - Write down your management source IPs. If your IP changes due to a corporate VPN, your allowlist will betray you.
- Decide full tunnel vs split tunnel. Servers rarely want full tunnel for everything; be explicit about exceptions.
Checklist B: If you need a VPN kill switch (and still need SSH)
- Allow inbound SSH on the physical interface from trusted ranges.
- Allow outbound management replies via the physical interface (policy routing/host routes).
- Allow outbound on the VPN interface broadly, then restrict destinations if you must.
- Keep
established,relatedacceptance in INPUT/OUTPUT. - Add logging/counters on final drops so you can see what you broke.
Checklist C: If the host is a VPN gateway for other networks
- Enable
net.ipv4.ip_forward=1(and IPv6 forwarding if used). - Decide NAT vs routed subnets (prefer routes if you control both sides).
- Permit FORWARD flows explicitly (LAN → VPN, VPN → LAN as needed).
- Ensure return routing exists on both sides (NAT hides this; routed requires it).
- Test from a client behind the gateway, not just from the gateway itself.
Rollback timer: cancel it when you’re safe
cr0x@server:~$ atq
7 Fri Dec 27 12:04:00 2025 a root
cr0x@server:~$ sudo atrm 7
Meaning: You remove the scheduled rollback job once you confirm you can reconnect.
Decision: Don’t cancel early. Wait until you’ve tested from a fresh session.
Working patterns: kill switch, split tunnel, server-side forwarding
Pattern 1: Keep management traffic off the VPN (recommended for servers)
If the server has a public IP and you administer it over SSH, treat management like a separate control plane.
Don’t throw it into a full-tunnel VPN unless you’re ready to engineer the routing precisely.
The simplest stable approach is: VPN handles application traffic or specific subnets, but SSH replies always go out
the interface where SSH came in.
Practically, that means either:
(a) don’t change the default route when the VPN comes up; add specific routes for VPN destinations; or
(b) use policy routing so only selected traffic uses the VPN.
Pattern 2: A sane kill switch that doesn’t kill your access
A kill switch is just “drop everything not going over wg0/tun0.” The dangerous bit is that your SSH session also becomes ‘everything’
if you don’t carve out exceptions.
The correct carve-out is not “allow port 22.” It’s “ensure replies to my admin networks route and pass via the correct interface,
even when the default route is the VPN.” That’s routing plus filtering, not only filtering.
Pattern 3: VPN gateway for a LAN (forwarding + NAT or routes)
This is the area where UFW surprises people. UFW’s default stance on routed traffic is conservative. That’s fine.
But if you’re building a gateway, you must configure forwarding explicitly.
If you don’t control routing on the far end, NAT is pragmatic. If you control both ends, routed subnets are cleaner.
NAT hides source IPs; routed preserves them but needs real route advertisements/entries.
Enable forwarding (persistently)
cr0x@server:~$ sudo tee /etc/sysctl.d/99-vpn-forwarding.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
cr0x@server:~$ sudo sysctl --system
* Applying /etc/sysctl.d/99-vpn-forwarding.conf ...
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Meaning: The kernel will now forward packets.
Decision: Only do this on hosts intended to route traffic; it changes the security posture.
UFW: allow routed traffic between LAN and VPN (example)
cr0x@server:~$ sudo ufw route allow in on ens3 out on wg0 from 192.168.50.0/24 to 0.0.0.0/0
Rule added
cr0x@server:~$ sudo ufw route allow in on wg0 out on ens3 from 0.0.0.0/0 to 192.168.50.0/24
Rule added
Meaning: You permitted forwarding in both directions for that LAN subnet.
Decision: Tighten destinations if you can. “LAN to anywhere” is fine for internet breakout, but not for east-west in corporate networks.
nftables: targeted masquerade for LAN out wg0 (example)
cr0x@server:~$ sudo nft add table ip nat
cr0x@server:~$ sudo nft 'add chain ip nat postrouting { type nat hook postrouting priority srcnat; policy accept; }'
cr0x@server:~$ sudo nft add rule ip nat postrouting oifname "wg0" ip saddr 192.168.50.0/24 masquerade
Meaning: LAN clients will appear to the remote side as the VPN interface IP, simplifying return routing.
Decision: If you need auditability per-host, prefer routed subnets instead of NAT.
Pin management traffic with policy routing (example)
If the VPN insists on owning the default route, you can still keep management stable using a separate routing table
for traffic to your admin networks.
cr0x@server:~$ echo "100 mgmt" | sudo tee -a /etc/iproute2/rt_tables
100 mgmt
cr0x@server:~$ sudo ip route add default via 203.0.113.1 dev ens3 table mgmt
cr0x@server:~$ sudo ip rule add to 198.51.100.0/24 lookup mgmt priority 1000
Meaning: Traffic destined for your admin subnet uses the physical gateway, even if the main table’s default route points at the VPN.
Decision: This is the adult approach when you must combine full tunnel with remote management.
One reliability quote (paraphrased idea)
“Hope is not a strategy.” — paraphrased idea attributed to operations/SRE culture (commonly used in reliability engineering discussions)
That line shows up everywhere because it’s painfully applicable to firewall changes on remote machines.
UFW vs nftables: choose your poison, avoid mixing metaphors
UFW: good for simple policies, dangerous when you assume it covers forwarding by default
UFW is pleasant when your needs are: allow SSH, allow a couple ports, deny everything else. It keeps you from
writing 200 lines of rules for a 4-rule policy. That’s a win.
Where UFW burns engineers is routed traffic and interface-based VPN paths. If the system is acting as a gateway,
you must reason about FORWARD. UFW makes that possible, but it doesn’t force you to think about it. That’s the trap.
nftables: explicit power, explicit consequences
nftables is clean and expressive. It also does exactly what you told it to do, even if what you told it is a
self-inflicted outage. With nftables, you should get comfortable with:
- Chain policies (
policy dropmeans you need explicit accepts). - State handling (
ct state established,relatedis not optional in most designs). - Interface matching (
iifname/oifname)—especially with VPN interfaces. - Counters and logging (a silent drop is an incident factory).
Don’t run two captains for one ship
If UFW is active and you also load a custom nftables ruleset, you can end up with overlapping tables and chain priorities.
Sometimes it works accidentally. That’s not a design.
Choose one:
keep UFW for policy, or manage nftables directly and disable UFW. Mixing is how you get a ruleset that no one on-call can explain.
Audit what actually owns packet filtering on your host
cr0x@server:~$ sudo ufw status
Status: active
cr0x@server:~$ sudo systemctl is-active nftables
inactive
cr0x@server:~$ sudo update-alternatives --display iptables | sed -n '1,6p'
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
link iptables is /usr/sbin/iptables
Meaning: UFW is active; iptables commands map to nft backend. Even if nftables.service is inactive, nft may still be the underlying engine.
Decision: When debugging, always inspect nft list ruleset as the source of truth on modern systems.
FAQ
1) Why did allowing the VPN UDP port not fix anything?
Because the UDP port only covers the tunnel’s outer transport. Your real traffic goes through a different interface
(wg0/tun0), hits different chains (FORWARD/OUTPUT), and may require NAT or routes.
2) Why does SSH break only after the VPN changes the default route?
Because replies follow the routing table. If replies leave via the VPN, the source IP changes and your client doesn’t accept it
(or the path drops it). Fix by pinning management routes or using policy routing.
3) Can I just set UFW default allow outgoing and forget it?
On a basic server, maybe. On a VPN gateway or kill-switch setup, no. OUTPUT restrictions interact with VPN routing,
DNS, and keepalives in non-obvious ways. If you restrict OUTPUT, do it with explicit allowances and counters.
4) What’s the cleanest way to prevent lockouts during firewall changes?
Schedule a rollback timer (at now + 2 minutes), apply the change, test from a fresh session, then cancel the timer.
Also keep console access if the machine matters.
5) Why does WireGuard show handshakes but still no connectivity?
Handshake success proves peers can exchange keys over UDP. It doesn’t prove your routes, AllowedIPs, forwarding policy,
or NAT are correct. Treat it as “transport is alive,” not “networking is finished.”
6) Do I need NAT for VPN traffic?
If you’re forwarding a private LAN through the VPN and the far end doesn’t have a route back to that LAN, NAT is the practical fix.
If you control both ends, prefer routes and avoid NAT for better observability and fewer surprises.
7) Why does IPv6 make this worse?
Because you can accidentally allow IPv4 and block IPv6 (or vice versa) and the client will happily pick whichever family “works.”
Then it fails. Audit listening sockets and firewall rules for both families if IPv6 is enabled.
8) Is rp_filter safe to disable?
rp_filter helps prevent spoofing. Disabling it blindly is lazy. For VPN gateways and asymmetric routing, set it to loose mode
(2) on the relevant interfaces so legitimate asymmetric traffic isn’t dropped.
9) Should I use UFW or native nftables for VPN systems?
If the system is simple, UFW is fine. If you need policy routing, complex forwarding, multiple VPNs, or strict kill switches,
nftables is usually clearer because you can express intent precisely—assuming your team can operate it.
10) What’s the single most common UFW mistake with VPN gateways?
Forgetting that routed traffic is disabled by default. You can allow the VPN port all day; forwarding still won’t happen.
Fix by enabling forwarding and adding ufw route allow rules.
Conclusion: next steps that keep you online
The recurring theme is unglamorous: VPNs change routing and interfaces, and your firewall doesn’t care about your intentions.
If you treat “VPN connectivity” as a port-open problem, you’ll keep locking yourself out.
Next steps I’d actually do on a production Ubuntu/Debian host:
- Decide whether the server should be full-tunnel or split-tunnel. If it’s a remotely administered server, default to split-tunnel.
- Implement a rollback timer habit for every firewall change on remote systems.
- Audit routing (
ip route,ip rule) and confirm management return paths explicitly. - Pick one firewall control plane (UFW or nftables) and stop mixing them unless you enjoy archaeology.
- Add counters/logging to your drop rules so debugging is evidence-based, not ritual-based.
If you do only one thing: make ip route get your reflex before and after bringing a VPN up. It tells you where your replies will go.
That’s the difference between “secure server” and “securely unreachable server.”