You expect a tunnel interface, you look for one, and there is nothing to match in the firewall. That is normal for a policy-based gateway: the encrypted exchange arrives on the physical interface, and the decrypted inner packet that gets forwarded onward is also observed as arriving on that same interface. There is no tun0 because nothing created one.

The inner request is still a distinct packet with its own source, destination and service, and the kernel knows which security association it came out of. That knowledge is what a firewall rule can match.

Two packets, one interface

Picture the inbound direction. An ESP or UDP-encapsulated packet arrives, the kernel finds the matching policy, decrypts it, and hands the cleartext packet to the forwarding path. A rule that only looks at the interface name cannot tell that packet apart from any other traffic arriving on the same wire.

What distinguishes it is the inbound security path context attached to the packet. nftables exposes that as meta ipsec exists, which is true when the packet arrived through an IPsec security path. Combine it with the client pool, the destination and the service, and the rule becomes specific enough to be safe.

Leaving out the context is the common mistake, and it is a security mistake rather than a cosmetic one. A rule that accepts anything sourced from the client pool accepts a spoofed packet from the local network just as happily, because the source address is not evidence of anything on its own. The security path context is the cheapest way to make the source meaningful. Write the rule with it from the start rather than adding it after an audit.

Write the forwarding rule

Assume an authenticated IPv4 remote-access service with clients in 10.90.0.0/24 and one approved destination at 203.0.113.40. A rule fragment for that flow looks like this.

meta ipsec exists ip saddr 10.90.0.0/24 \
  ip daddr 203.0.113.40 tcp dport 443 \
  oifname "eth0" counter accept

The addresses and interface are examples to replace. Place the reviewed equivalent in the forwarding policy before the rule that would otherwise reject the flow, and do not flush an existing ruleset to install it; a gateway is not a clean-room experiment.

The oifname term is worth keeping. It documents the egress decision and prevents the rule from matching in a direction you did not intend, which matters on a gateway with several paths to the internet. On a single-uplink test box the term looks redundant, and that is exactly why it gets dropped before the box grows a second uplink.

Permissions for the outer exchange are separate. The gateway's input path needs the IKE and ESP or UDP-encapsulation ports it actually uses, and those permissions authorize nothing about the decrypted packet inside. Mixing the two is how a working tunnel ends up with a firewall that forwards everything.

Inbound context is not outbound context

The same family of expressions covers the other direction with rt ipsec, which describes the outgoing security association a packet will use. It answers a different question: not where this packet came from, but whether it will be protected on its next leg.

That distinction matters on a gateway that both terminates one tunnel and forwards into another. A packet can arrive decrypted and still need to leave encrypted, and an accept written only for the inbound context says nothing about the outbound requirement. If the policy requires that protection, the ruleset should express it rather than assume the routing will handle it.

Keep the two questions in separate rules so that a reader can see which one is being enforced. A single clever rule that checks both is harder to review and much harder to debug when one half stops matching. Two rules with counters also tell you which requirement is failing, which one combined rule never will.

NAT is not a firewall decision

Internet-bound traffic from a private client pool usually needs source translation so the reply can find its way back. But traffic selected for another outbound IPsec policy has to keep its required exemption, and masquerading every private source without thinking about the next protection decision breaks that.

An accept inside a NAT chain is not a general allowance either. NAT and filtering have different jobs, and a NAT chain does not examine every later packet of an established flow, which is exactly why counting on it for security produces rules that appear to work in testing and fail under load.

The reverse is also true and catches people out: an accept somewhere in one filter base chain carries no weight against a later drop in another applicable chain. Order and chain scope decide the outcome, not the presence of an accept in the file.

Verify the exact inner service

Test the specific destination and port you authorized, from a real client, and confirm the reply comes back. Then test something you did not authorize and check that it does not, because a rule that is too broad passes the first test just as well as a correct one. A request that never leaves the client is not a firewall result either, so read the counters to see whether the packet was evaluated at all. Two tests and one counter reading will tell you more than an afternoon of reading the ruleset.

The nftables reference documents meta ipsec, rt ipsec and the direction-specific expressions, which is the place to check syntax against your installed version. And keep the scope explicit: this is an IPv4 example, it says nothing about IPv6, and it does not turn arbitrary public port forwarding into part of the VPN service.

អត្ថបទពាក់ព័ន្ធ