Hairpin NAT for an Internal Web Server
A FortiGate 100F at HQ has wan1 203.0.113.10/29 and internal 10.10.0.0/16. A web server at 10.10.20.50 listens on TCP 443. A VIP maps 203.0.113.10:443 to 10.10.20.50:443. Policy 1 (top) is internal -> wan1, source all, destination all, service ALL, NAT enabled, no UTM. Policy 2 is wan1 -> internal, destination web-vip, service HTTPS, NAT disabled. External customers reach https://203.0.113.10 without issue. Staff on 10.10.30.0/24 time out when they use the same URL, although https://10.10.20.50 works. A junior admin proposes adding a deny rule for the public IP above Policy 1.
1. Explain why staff cannot use the public URL and give the minimal policy/NAT change that fixes it without breaking external access.
Read the worked answer
Staff packets are destined to an address the FortiGate owns (203.0.113.10), so they are not forwarded out wan1; the FortiGate must hairpin the flow. The only policy that references the VIP is Policy 2, which is bound to the wan1 interface, so there is no matching internal-to-internal policy and the flow is dropped. diagnose debug flow would show the packet entering internal, then a policy-check failure or no route.
Fix: add a policy above Policy 1 with srcintf internal, dstintf internal, src 10.10.30.0/24, dst web-vip, service HTTPS, action accept, and NAT enabled. The NAT is essential: without source translation the server replies directly to 10.10.30.x, and the client discards a reply whose source is not 203.0.113.10. With the hairpin policy, the FortiGate translates both directions and the client sees the reply from the address it connected to. Policy 2 stays unchanged for external users.
Verify with diagnose sys session filter dst 203.0.113.10 and diagnose sys session list; the session should show the VIP translation and NAT applied.
2. What would the proposed deny rule above Policy 1 actually accomplish, and what is the correct policy ordering?
Read the worked answer
The deny rule would make the failure permanent and explicit: staff traffic to 203.0.113.10 would match the deny before any hairpin policy could match. It would not affect external users, who arrive on wan1 and match Policy 2, so it would only break the internal use case the admin is trying to solve.
The correct order is:
- Hairpin permit: internal -> internal, destination web-vip, service HTTPS, NAT enabled.
- General internal -> wan1 policy.
- Inbound wan1 -> internal VIP policy.
Note that Policy 1 would not forward this traffic out wan1 anyway, because the destination is a local address. The missing internal-to-internal VIP policy is the actual gap; inserting a deny does not address it.
3. The server owner later asks why vendor-side logs show the firewall address instead of the staff client address. What are the options, and which do you recommend?
Read the worked answer
Hairpin NAT translates the staff source address, so the server sees the FortiGate address, not 10.10.30.x. That is inherent to the hairpin design; the firewall must be the return path or the client will reject the reply.
Options:
- Split DNS: internal DNS returns 10.10.20.50 for the server name, so staff never traverse the hairpin. This removes the extra session and preserves the real source at the server. It is the least complex and the recommended option.
- Application-layer identity: place a reverse proxy in front of the server and forward X-Forwarded-For, so the application can log the client even though the network source is translated.
- A load balancer with source-IP insertion, which is more infrastructure than this requirement justifies.
If the requirement is network-level source preservation for auditing, hairpin SNAT cannot provide it; split DNS is the practical answer.