Decide the permission before you open a firewall file: authenticated clients may open connections to one specific subnet, and the gateway must refuse every other transit path even when a client installs extra routes. That sentence is the design, and each rule below exists to enforce it. Anything broader, such as all private address space, internet egress through the tunnel or peer-to-peer traffic, is a different design that needs its own decision. The rules are also deliberately boring, because a clever firewall is one nobody can audit later.

Write the topology and the permission down first

The example uses a Community OpenVPN server with a userspace TUN device called tun0, a client pool of 10.77.0.0/24, and one attached subnet 192.168.60.0/24 on lan0. IPv4 forwarding already works and the LAN already has a return route to the pool, so nothing here depends on translation between them.

UFW owns the firewall on this host, and no container ruleset or separate nftables table may install an earlier acceptance path. A flow-offload shortcut that skips the filter would make the guard chains decorative, so confirm none is active. Leave OpenVPN's own client-to-client forwarding disabled, because peer traffic that never crosses the operating system filter cannot be bounded by it. The external listener and the administration rules stay as they are, since this change governs decrypted tunnel traffic rather than replacing the firewall.

Place the guards ahead of UFW's early acceptances

A route allowance followed by a deny is not enough for a strict boundary. Stock before rules accept established traffic and selected ICMP before any user rule is consulted, so a flow permitted last week can stay outside the new destination restriction purely because it still has state. Back up the UFW files and keep an out-of-band console open before editing.

In the *filter section of /etc/ufw/before.rules, declare two chains next to the existing declarations and before any rule line.

:vpn-src-guard - [0:0]
:vpn-reply-guard - [0:0]

Immediately after the declarations, and before the input and forwarding allowances, jump into them and define what they admit.

-A ufw-before-input -i tun0 -j DROP
-A ufw-before-forward -i tun0 -j vpn-src-guard
-A ufw-before-forward -o tun0 -j vpn-reply-guard

-A vpn-src-guard -o lan0 -d 192.168.60.0/24 -s 10.77.0.0/24 -m conntrack --ctdir ORIGINAL -j RETURN
-A vpn-src-guard -j DROP

-A vpn-reply-guard -i lan0 -d 10.77.0.0/24 -s 192.168.60.0/24 -m conntrack --ctstate RELATED,ESTABLISHED --ctdir REPLY -j RETURN
-A vpn-reply-guard -j DROP

Keep the rest of the section and its COMMIT line. Pasting a second filter table or deleting unrelated rules is how a firewall change becomes an outage, and the file is one reboot away from being the only copy that matters.

Read what each guard admits and refuses

The first chain continues processing only when the source is in the pool, the destination is the one subnet, the exit interface is the LAN and the packet travels in the original direction of its flow. Everything else arriving from the tunnel is dropped, including a source outside the pool and any destination beyond the allowed subnet.

A peer-to-peer packet enters and leaves tun0, so it fails the LAN interface test before a destination check is even reached. New connections from the LAN toward clients fail the reply direction test as well, which closes the reverse callback path by default.

The RETURN target keeps ordinary UFW policy in play. These chains filter, and the route permission you add next still decides whether a surviving packet is accepted. That separation is what lets you change the permitted subnet in one place later. If the guards accepted packets outright, every future rule would have to be added twice.

Authorise the subnet and close the side doors

Preview the route permission first, then drop the preview flag to apply it: sudo ufw --dry-run route allow in on tun0 out on lan0 from 10.77.0.0/24 to 192.168.60.0/24. Afterwards, review the order of the user rules, because an earlier deny still wins over a later allow. A whole-subnet permission is also the wrong shape when only one host's HTTPS port should be reachable.

Note what the input drop does: tunnel clients cannot reach any service on the gateway itself, including a DNS listener bound to its LAN address. Hosting DNS there needs a deliberate exception before the drop and a revised permission contract. If UFW manages IPv6, add matching drops for tun0 in before6.rules as well, because an IPv4 destination list says nothing about IPv6 packets.

Verify the loaded policy, not the preview

Preview the reload, apply it in a maintenance window, and read the live ruleset.

sudo ufw --dry-run reload
sudo ufw show raw

Confirm that the tunnel guards appear before the default state and ICMP allowances. Plain ufw status does not display hand-edited before rules, which is why operators conclude a rule is missing when it is in fact loaded. Read the raw listing top to bottom rather than searching for a fragment. If the guards appear after a state allowance, the boundary is not doing what the file suggests.

Then test behaviour rather than text. Open a fresh connection to an allowed host and watch the reply, route a harmless request to a destination outside the subnet and confirm it dies, try a peer-to-peer connection, try a gateway-local service, and try an unsolicited LAN-to-client connection. Include one out-of-scope flow that existed before the change, and clear only that flow rather than flushing the machine's whole tracking table.

Record the limits alongside the rules: this bounds transit and nothing else. It does not stop a client from using its own internet connection, does not stop an allowed LAN host from relaying traffic onward, and does not replace authentication inside the permitted services.

Tënë sô zo