A NAT rule that works when typed into a shell fails when pasted into before.rules, because that file is not a script. It is a restore-format document with table sections, chain declarations and a commit marker for each table. A line in the wrong place either aborts the whole restore or silently drops rules you believed were there. The runbook below puts the fragment where the framework expects it and proves the result before the next reload.

A shell command is not a restore file

A line beginning with iptables -t nat is an instruction to the iptables binary, while the file UFW reads is a description of a complete ruleset. The difference is not cosmetic: the restore format has to know which table it is in, which chains exist, and where the table ends. A command pasted into that document is either a syntax error or, worse, a rule attached to the wrong table. The framework loads these files through restore tools precisely because they are declarative rather than sequential.

The document is also loaded as a whole, so one malformed section can stop the rest of the file from applying. A small mistake in a NAT fragment therefore takes the filter rules down with it, which is how a firewall change becomes an outage.

Put the fragment inside the existing table section

A NAT fragment has a shape, and the shape matters more than the rule text inside it. The table header opens the section, chain declarations establish the counters, the rules follow, and a commit closes the section.

*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.55.0.0/24 -o ens3 -j MASQUERADE
COMMIT

The pool and interface above are placeholders for your own; the structure is the part worth copying. If the file already contains a *nat section, add the rule inside that section rather than opening a second one, because the format expects one commit per table and two sections for the same table are a repair waiting to happen. Keep the distribution chain definitions intact, including any the vendor added for its own integration. The UFW framework manual documents the before, user and after files along with the requirement for a matching commit.

If the file turns out to be empty, do not fill it from a tutorial. Confirm which package owns it and what the live policy actually is, because an empty persistent file does not prove the kernel holds no rules, and a loaded ruleset does not prove the next reload has a complete file to read. Restore the supported baseline first, then re-apply site changes on top of it.

Test the candidate without committing it

The restore tool can parse and construct a ruleset without applying it, which is the cheapest test available. Run it against a protected copy rather than the live file, and read the error text rather than only the exit status. A parse failure names a line and usually a table, which is more than a failed reload will tell you.

sudo cp /etc/ufw/before.rules /root/before.rules.bak
sudo iptables-restore --test < /root/before.rules.bak
echo "exit: 0"

Passing that check proves the document parses and nothing else. It does not prove the candidate still contains every chain UFW depends on, that it permits only the sources you intended, or that remote administration stays reachable. Those are separate reviews, and the last one deserves a second session opened before you start.

Pick the tool that matches the installed backend and address family. A host running the nft backend through the compatibility layer needs the corresponding command, and IPv6 rules live in a different file entirely.

Keep forwarding, NAT and access policy separate

Source translation fixes an addressing problem and grants nothing. Forwarding is a separate permission controlled by a sysctl and by the filter table, and it is the one people forget when a tunnel routes but passes no traffic. Access policy is a third decision about which sources may reach which services, and no NAT rule expresses it. Keeping the three apart is what makes a failure diagnosable instead of a single vague symptom.

sysctl net.ipv4.ip_forward
sudo iptables -t nat -S POSTROUTING
sudo iptables -S FORWARD

Read those three outputs together rather than one at a time. A masquerade rule with forwarding disabled produces a client that connects and reaches nothing, which looks exactly like a routing fault from the client side and sends people to the wrong file. The filter table is also where an over-broad accept hides, so check its default policy while you are there.

Verify a reload, a denial and a VPN loss

Three observations make a complete acceptance test: the reload succeeds, a permitted client operation works, and a request that should be refused is actually refused. The third one is the one that gets skipped, and it is the only evidence that the policy is doing anything at all.

sudo ufw reload
sudo ufw status verbose
sudo iptables -t nat -L POSTROUTING -n -v

Then test the failure you designed for. Drop the tunnel and watch what the host does, because a policy that works only while the VPN is up is a policy with an unstated dependency. Reload once more afterwards to prove that the file, rather than some leftover running state, is what produces the working result.

IPv4 NAT says nothing about IPv6, so state the intended coverage for both families in the same note as the rule. A coherent managed policy is the goal, and a fragment that happened to parse is not evidence of one.

Inyandiko zihuye