T Techclick ← All lessons
Fortinet · FortiGate · Interactive lesson

FortiGate policy zone then NAT then session

Ticket: default route is in, the host pings 10.20.20.1, traceroute leaves the LAN — and still no session. A FortiGate does not forward because a route exists. It forwards when an accept policy matches incoming interface/zone, dest zone, address and service. SNAT/DNAT rewrite the packet; they are not the allow. Proof is the session table. Lab: LAN 10.20.20.0/24, FGT 10.20.20.1, WAN 192.0.2.0/24.

16 min read · L2 primary · Quiz at end

After this page you can

Lessons · FortiGate series · Zones, policy, NAT

This page vs first-match and VDOM

This lesson is how FortiOS turns a packet into a session: zone membership, policy lookup, then NAT, then the session table. Rule order and implicit deny depth live on first-match. Tenant isolation is VDOM.

Policy first-match · VDOMs · Command center

Hero · host hits a zone, not a port nickname
LAN host through a firewall with LAN, WAN and DMZ zone panes, then app and internet
Mood, not a wiring diagram. Exact path is in the SVG: 10.20.20.10 ingresses zone LAN, policy LAN-to-WAN accepts, SNAT to 192.0.2.1, session installed. Direct to 10.20.20.80 never hits WAN policy.
Quick answer

FortiGate policy matches incoming interface or zone, destination zone, source/dest address, and service (plus schedule/user if set). First match wins; nothing matching is implicit deny. SNAT (set nat enable / IP pool / Central SNAT) and DNAT (VIP) rewrite addresses — they do not grant the permit. An entry is placed in the session table for each session that passes a security policy. Prove with diagnose sys session filter then diagnose sys session list. Lab: LAN 10.20.20.0/24, FGT 10.20.20.1, WAN 192.0.2.0/24.

Why a route is not a session

The day-one ticket is always the same: “Routing is fine, so the firewall is broken.” Wrong. RIB lookup only names the egress interface. FortiOS still needs a firewall policy whose srcintf / dstintf (interface or zone), addresses and service cover that 5-tuple. No match → implicit deny. No accept → no session to list.

Three silent-zero states look identical from the host (timeout):

Ping to 10.20.20.1 proves L2/L3 to the FortiGate, not policy

Traffic to the FortiGate uses local-in policy. Traffic through it uses firewall policy. Do not debug SNAT until diagnose debug flow shows a policy id other than 0 and diagnose sys session list shows the flow.

Zone, policy, NAT — three objects

A zone is an interface object: one or more physical, VLAN, LAG or tunnel interfaces grouped so a policy can say LAN instead of listing every port. Members of the same zone do not need a policy to talk to each other unless you enable intra-zone block. Policies still match on the zone name as srcintf / dstintf.

A firewall policy is the allow/deny. Required match fields: incoming interface(s), outgoing interface(s), source address(es), destination address(es), service(s), schedule. Optional: user, internet-service, security profiles. Action is accept or deny. NAT is a separate knob on that policy, or a separate VIP / Central NAT table.

Path · zone, then policy, then NAT, then session
Four glass panels labeled Zone, Policy, NAT, Session
Feel of the order. Exact FortiOS sequence — DNAT before policy, SNAT after accept — is in the SVG below. Do not read the NAT panel as “NAT is the firewall.”

Zone

config system zone. GUI: Network › Interfaces › Create New › Zone. Lab: LAN = internal, WAN = wan1. Policy talks in zone names.

Address / service

LAN_NET = 10.20.20.0/24. Service is ALL, HTTPS, or a custom TCP port. Objects, not raw CIDR typed in the policy every time.

Allow

set action accept. First matching policy wins. Implicit deny is policy 0 — debug flow says denied by policy 0 when nothing hit.

NAT

SNAT: set nat enable (outgoing interface IP or IP pool). DNAT: VIP as destination. Central NAT if central-nat is on — then the policy NAT checkbox is the wrong place.

Say this out loud

The packet hits a zone. Policy matches that zone pair plus address and service. Accept builds the session. SNAT or DNAT only rewrites. No accept, no session.

How a session is built

Existing session first. If FortiOS already has the 5-tuple, later packets skip policy lookup and follow the installed NAT hooks. New flows walk the full path. DNAT (VIP / Central DNAT) rewrites destination before routing and policy so the RIB and the dest-zone match the real server. SNAT runs after accept.

Flow 1 · lab packet 10.20.20.10 → 203.0.113.80:443
1 Ingress zone LAN · port2 2 Session? hit → use hooks 3 DNAT / VIP outbound: noop 4 Route egress zone WAN 5 Policy LAN → WAN · HTTPS 6 Accept → SNAT → 7 Session install hook=post dir=org act=snat 10.20.20.10:54321→203.0.113.80:443(192.0.2.1:54321) policy_id=1 · proto=6 proto_state=01 then 11 · nsrc=192.0.2.1 Miss at step 5: implicit deny (policy 0). No SNAT. No session to list. Fix the zone pair / address / service. Do not add a second default route. Reply packets reverse the hooks. Clear the session after a policy edit or you keep the old NAT. Source: FortiOS Administration Guide — firewall policy parameters; Using a session table.

Read left → right, then the gold bar. Route names the dest zone; policy still has to accept that pair. SNAT is after accept.

ObjectLab valueIf missing
LAN zone / IPzone LAN · 10.20.20.1/24Host cannot ARP the gateway. Local ping fails — not a policy problem.
WAN zone / IPzone WAN · 192.0.2.1/24No egress. Route lookup fails before policy.
AddressLAN_NET = 10.20.20.0/24Policy matches nothing; debug flow policy 0.
Outbound policyLAN-to-WAN accept + nat enableRoute yes, session no. Return from 203.0.113.80 has no state.
VIPweb_pub 192.0.2.80 → 10.20.20.80:443Published IP answers nothing. VIP without a WAN→LAN policy is not an open port.
Session proofdiagnose sys session listGUI hit counter can lie on cached sessions. The table is the wire.

Policy NAT vs VIP vs Central NAT

Pick the NAT engine the VDOM is actually running. Mixing them is the usual “NAT is on but nsrc is wrong” ticket.

Flow 2 · allow is not NAT
Allow action = accept srcintf LAN · dstintf WAN addr + service match SNAT nat enable / IP pool or Central SNAT table after accept · nsrc=192.0.2.1 DNAT firewall vip web_pub 192.0.2.80 → 10.20.20.80 before policy · dstaddr=VIP You can accept without NAT (east-west, or inbound VIP with nat disable). You cannot SNAT a deny. Central NAT on: policy NAT checkbox is ignored — edit Policy & Objects › Central SNAT. VIP policies have priority over ordinary dest-all policies. Deny in front of a VIP needs match-vip enable. Source: FortiOS firewall policy; Destination NAT / Configuring VIPs; Central SNAT.

Three columns, three tickets. Do not toggle NAT to “fix” a zone mismatch.

NeedUseSkip
LAN hosts share WAN IPPolicy NAT: set nat enable on LAN-to-WAN (outgoing interface address)VIP. VIP is dest rewrite, not hide-NAT.
Many-to-one with a dedicated publicIP pool + set ippool enable / set poolnameOutgoing-interface SNAT if you must pin the source.
Publish 192.0.2.80:443 to 10.20.20.80VIP + WAN→LAN accept, dstaddr = VIP, usually nat disableSNAT on that policy unless the server’s default gw is not the FGT.
NAT rules independent of every policyCentral SNAT / Central DNAT (config system settings set central-nat enable)Leaving policy NAT enabled and wondering why it does nothing.

Runbook Side A / B / C

Side A is objects. Side B is the policy and NAT. Side C is the session table. Do not start at C.

Side A — zones and addresses

  1. Zone LAN and WAN

    Network › Interfaces › Create New › Zone. Lab: LAN member = internal (or port2); WAN member = wan1. CLI config system zone. Address on the member interface, not on the zone: LAN 10.20.20.1/24, WAN 192.0.2.1/24. Source: FortiOS Administration Guide — Zone.

  2. Address object

    Policy & Objects › AddressesLAN_NET subnet 10.20.20.0/24. Do not use all as source on the first outbound policy if you want a readable session later.

CLI — Side A
config system zone
    edit "LAN"
        set interface "internal"
    next
    edit "WAN"
        set interface "wan1"
    next
end
config firewall address
    edit "LAN_NET"
        set subnet 10.20.20.0 255.255.255.0
    next
end
config router static
    edit 1
        set gateway 192.0.2.254
        set device "wan1"
    next
end

Side B — policy then NAT

https://10.20.20.1/ · Policy & Objects › Firewall Policy › Create New
Training mock · not live

Policy & Objects › Firewall Policy › Create New

LAN-to-WAN

LAN
WAN
LAN_NET (10.20.20.0/24)
all
ALL
ACCEPT · NAT enable · outgoing IP

Source: FortiOS Administration Guide — Firewall policy parameters (incoming/outgoing interface, address, service, schedule). NAT enable is the SNAT checkbox, not the action. Log all on the first lab policy so Forward Traffic has a row when the session exists.

  1. Outbound accept + SNAT

    Policy id 1 LAN-to-WAN: srcintf LAN, dstintf WAN, srcaddr LAN_NET, dstaddr all, service ALL, action accept, nat enable, logtraffic all. This is hide-NAT to 192.0.2.1.

  2. Inbound VIP (separate from allow)

    Policy & Objects › Virtual IPsweb_pub, interface WAN, external 192.0.2.80 map to 10.20.20.80, port 443→443. Then policy id 2 WAN→LAN, dstaddr web_pub, service HTTPS, accept, NAT off unless the server’s gateway is not the FGT.

https://10.20.20.1/ · Policy & Objects › Virtual IPs › Create New
Training mock · not live

Policy & Objects › Virtual IPs › Create New

web_pub

WAN
192.0.2.80
10.20.20.80
443 TCP → 443

Source: FortiOS — Configuring VIPs. After OK you still need the WAN→LAN policy with destination web_pub. VIP object ≠ open port.

CLI — Side B
config firewall policy
    edit 1
        set name "LAN-to-WAN"
        set srcintf "LAN"
        set dstintf "WAN"
        set srcaddr "LAN_NET"
        set dstaddr "all"
        set action accept
        set schedule "always"
        set service "ALL"
        set nat enable
        set logtraffic all
    next
end
config firewall vip
    edit "web_pub"
        set extintf "WAN"
        set extip 192.0.2.80
        set mappedip "10.20.20.80"
        set portforward enable
        set protocol tcp
        set extport 443
        set mappedport 443
    next
end
config firewall policy
    edit 2
        set name "WAN-to-WEB"
        set srcintf "WAN"
        set dstintf "LAN"
        set srcaddr "all"
        set dstaddr "web_pub"
        set action accept
        set schedule "always"
        set service "HTTPS"
        set nat disable
        set logtraffic all
    next
end

Side C — prove the session

  1. Filter, then list

    From 10.20.20.10 open HTTPS to 203.0.113.80. On the FGT: diagnose sys session filter clear, diagnose sys session filter src 10.20.20.10, diagnose sys session filter dport 443, diagnose sys session list. Source: FortiOS — Using a session table.

  2. Read the hooks

    You want policy_id=1, hook=post dir=org act=snat with (192.0.2.1:…), and bytes on org and reply. Empty list after a live attempt = no accept. act=noop on org = accept without SNAT.

  3. Clear after an edit

    diagnose sys session filter src 10.20.20.10 then diagnose sys session clear so the next packet re-walks policy. Old sessions keep old NAT.

CLI — session proof (outbound SNAT)
diagnose sys session filter clear
diagnose sys session filter src 10.20.20.10
diagnose sys session filter dport 443
diagnose sys session list
# session info: proto=6 proto_state=11 …
# hook=post dir=org act=snat 10.20.20.10:54321->203.0.113.80:443(192.0.2.1:54321)
# hook=pre dir=reply act=dnat 203.0.113.80:443->192.0.2.1:54321(10.20.20.10:54321)
# policy_id=1  vd=0
# nsrc / nport match the SNAT you intended

diagnose firewall iprope lookup 10.20.20.10 54321 203.0.113.80 443 6 internal
# which policy would match — use when list is empty (device = ingress member, proto 6 = TCP)
Green proof

Session list shows policy_id=1, SNAT to 192.0.2.1, reply packets increment. Forward Traffic log has the same policy name. That is the close — not “the route is there.”

One flow after go-live

Host 10.20.20.10 SYN to 203.0.113.80:443. Ingress LAN. Session miss. No VIP. RIB: default via 192.0.2.254, egress WAN. Policy 1 matches LAN/WAN/LAN_NET/all/ALL. Accept. SNAT source to 192.0.2.1. Session installed. Reply hits WAN, reverse DNAT to 10.20.20.10, egress LAN. Second packet of the same 5-tuple skips policy and uses the hooks.

Inbound: SYN to 192.0.2.80:443. VIP DNAT to 10.20.20.80:443. Route to LAN. Policy 2 matches WAN/LAN/all/web_pub/HTTPS. Accept, no SNAT. Session act=dnat on org. Hairpin from 10.20.20.10 to 192.0.2.80 needs extra policy + often SNAT so the server replies via the FGT — that is a different ticket.

Proof · session built is a table row, not a dashboard tile
Engineer verifying a firewall session on a monitor with abstract checkmarks
Ops feel. The actual evidence is diagnose sys session list fields: policy_id, hook act, nsrc, proto_state. Artwork checkmarks are not FortiOS.
debug flow — when the session table is empty
diagnose debug reset
diagnose debug flow filter addr 10.20.20.10
diagnose debug flow filter port 443
diagnose debug flow show function-name enable
diagnose debug enable
diagnose debug flow trace start 20
# look for: find a route / allowed by policy 1 / SNAT 10.20.20.10 -> 192.0.2.1
# or: denied by policy 0  → zone/address/service miss, not a NAT bug
diagnose debug disable

Traps + proof

SymptomLikely causeProof
Route exists, no sessionNo accept for that zone pair / service. Implicit deny.debug flow policy 0. session list empty after filter.
Policy on port2, packet on vlan20Incoming interface is the member, not the alias in your head. Zone never included vlan20.diagnose netlink interface list vs srcintf on the policy.
NAT checkbox on, nsrc still RFC1918Central NAT enabled, or dest is not leaving WAN, or policy is deny.get system settings | grep central-nat. Session act=noop.
VIP created, port still closedNo WAN→LAN policy with dstaddr = VIP. VIP is DNAT only.sniffer on WAN sees SYN, no session, no LAN SYN.
Changed NAT, still old public IPExisting session kept old hooks.Filter + diagnose sys session clear, re-test.
LAN host to own VIP failsHairpin: dest after DNAT is LAN, src is LAN — missing LAN→LAN VIP policy / SNAT.debug flow dest zone LAN not WAN.
Deny in front of VIP does not blockVIP policies take priority unless set match-vip enable on the deny.FortiOS firewall policy — match-vip. Session still policy_id of the VIP rule.
Do not set srcintf any to “make it work”

Best practice: policies that allow traffic apply to a specific interface or zone, not any. Any hides the dest-zone mistake until the next bypass. Fix the zone membership.

Pilot checklist

Knowledge check

Six judgment items. Submit once. Reasons point back at the section to re-read.

Q1

Default route is installed. Host 10.20.20.10 times out to 203.0.113.80. What must match before FortiOS will even consider SNAT?

Correct: b. Route only names egress. Policy match is srcintf/zone, dstintf/zone, address, service. SNAT runs after accept. Re-read Why a route is not a session and Flow 1.
Q2

Which statement treats SNAT/DNAT as separate from allow?

Correct: a. Allow and NAT are different objects. VIP without policy is not an open port. Central NAT replaces policy NAT when enabled. Re-read How to choose and Side B.
Q3

What is the proof that LAN-to-WAN actually built a session?

Correct: c. Session table is the wire. Ping to the FGT is local-in. Re-read Side C and Traps.
Q4

Inbound HTTPS to VIP 192.0.2.80 maps to 10.20.20.80. Where does DNAT sit relative to policy?

Correct: b. VIP DNAT rewrites dest first so route/policy see LAN and the VIP as dstaddr. SNAT is the later, optional hide. Re-read Flow 1 and Flow 2.
Q5

You edited SNAT on policy 1. The next connection from the same host still uses the old public IP. First move?

Correct: d. Installed sessions keep old hooks and skip policy. Clear, then list again. Re-read Side C and Traps.
Q6

Why put internal and a VLAN into zone LAN instead of two policies?

Correct: a. Zone is an interface object used in policy. IP stays on the member. Intra-zone is not WAN access. Re-read Zone, policy, NAT — three objects and Side A.

Sources

Related: Policy first-match · FortiGate command center · FortiGate interview · VDOMs