The Night Our Dual-Stack Change Exposed a Policy Gap
After enabling IPv6 on our FortiGate to support a SaaS application, our security team discovered that IPv6 traffic was bypassing all security policies — the firewall had no IPv6 rules. We were running FortiOS 7.4.3 on the edge pair, our internal tooling was on Ubuntu 22.04, and my quick validation script used Python 3.11 to compare session logs against expected policy IDs.
My first assumption was wrong. I assumed our existing IPv4 policy set would protect the new IPv6 path because the source zones, destination zones, inspection profiles, and NAT behavior were already locked down for the same business flows. That assumption lasted until I watched IPv6 sessions move through the firewall without matching the IPv4 rules I trusted.
The uncomfortable part was not that IPv6 was enabled. The uncomfortable part was that we treated dual-stack as an addressing project instead of a security policy project, and that mistake gave us a blind spot in an environment where plant systems, vendor tunnels, engineering workstations, and SaaS access all share carefully segmented paths.
That got my attention.
Before the fix, our IPv6 traffic coverage under explicit security policy was 0%. After creating equivalent IPv6 firewall policies for all IPv4 rules, IPv6 traffic coverage under security policy reached 100%. I do not like any firewall change that creates a second traffic plane unless the rule review treats it as a second traffic plane.
Why FortiGate Keeps IPv6 Policy Separate
On FortiGate, IPv4 firewall policies and IPv6 firewall policies are separate objects. That separation makes sense technically because IPv6 has different address objects, neighbor discovery behavior, ICMPv6 requirements, routing expectations, and NAT assumptions. It also creates a trap: a clean IPv4 rulebase does not automatically mean we have a clean IPv6 rulebase.
In our environment, the IPv4 rules had years of hardening behind them. We had source address groups for production VLANs, separate outbound policies for engineering jump hosts, denied east-west movement between sensitive zones, and logging requirements tied into our SIEM. The IPv6 side had none of that until we built it deliberately.
IPv6 is not an upgrade flag.
What I didn’t expect was how quiet the failure looked. There was no dramatic outage, no obvious dashboard alarm, and no angry user ticket. The SaaS application worked, which made the change look successful from the business side, while our inspection and segmentation assumptions were incomplete from the security side.
On FortiOS 7.4.3, I now treat dual-stack enablement as two parallel firewall projects: one for IPv4 and one for IPv6. If we cannot point to the matching IPv6 policy, address object, route, log setting, and inspection profile, then we are not done. My opinion is blunt here: separate policy tables are good design, but they punish casual change control.
Choosing SLAAC or DHCPv6 Without Losing Control
Our next decision was address assignment. For user networks, SLAAC looked attractive because clients could configure themselves quickly from router advertisements. For infrastructure and controlled manufacturing segments, DHCPv6 gave us better operational tracking, especially where we wanted predictable leases and logs tied back to hostnames or reservations.
That split mattered because address assignment affects how we build policy objects. If endpoints rotate through privacy addresses and we only document a prefix at a broad level, we need policies that reflect the segment boundary instead of pretending we can track every host address. If we use DHCPv6 for a controlled subnet, we can build tighter address groups and better audit trails.
config system interface
edit "VLAN-ENG"
set ip 10.40.20.1 255.255.255.0
set allowaccess ping https ssh
config ipv6
set ip6-address 2001:db8:40:20::1/64
set ip6-allowaccess ping
set ip6-send-adv enable
set ip6-manage-flag enable
set ip6-other-flag enable
end
next
end
I also learned to be careful with ICMPv6. Blocking too much ICMPv6 breaks neighbor discovery, path MTU discovery, and basic troubleshooting. We had to allow the right control traffic while still denying unauthorized data flows, which felt backwards to a few people who were used to treating ICMP as optional noise in IPv4.
The small packets mattered most.
For manufacturing networks, I prefer DHCPv6 where asset accountability matters and SLAAC where client autonomy matters more than individual lease visibility. I do not like mixing methods without a written reason because six months later nobody remembers whether the design was intentional or accidental.
Mirroring IPv4 Security Intent in IPv6 Policies
We did not copy rules blindly. We copied intent. That distinction saved us from dragging IPv4 assumptions into IPv6, especially NAT-centric thinking. Many of our outbound IPv4 policies depended on source NAT, but IPv6 did not need the same translation behavior. The segmentation, inspection, logging, and destination restrictions still had to match.
My team built a simple mapping table: IPv4 policy ID, source zone, destination zone, source object, destination object, service, security profiles, logging, and the equivalent IPv6 object. Then we reviewed each row as if it were a new firewall request. That slowed us down for one afternoon and prevented several lazy allow-any rules from landing in production.
- Created IPv6 address objects for every routed internal prefix.
- Built IPv6 service groups instead of reusing vague any-service rules.
- Matched antivirus, IPS, web filtering, and SSL inspection where applicable.
- Enabled logging on every IPv6 policy during rollout validation.
- Added explicit deny policies at zone boundaries before closing the change.
Here is the pattern we used for one controlled outbound flow from engineering workstations to the SaaS provider. The exact object names matter less than the habit of making IPv6 visible, reviewed, and logged.
You may also find this useful: Check out our guide on Python Network Config Backup: Automating Multi-Vendor Device Snapshots for more practical tips.
config firewall policy6
edit 120
set name "ENG-to-SaaS-IPv6"
set srcintf "VLAN-ENG"
set dstintf "WAN1"
set srcaddr "ENG-IPv6-Prefix"
set dstaddr "SaaS-IPv6-Prefix"
set action accept
set schedule "always"
set service "HTTPS"
set logtraffic all
set utm-status enable
set ips-sensor "default"
set ssl-ssh-profile "certificate-inspection"
next
end
I do not trust a dual-stack deployment until I can disable the IPv4 path temporarily in a test window and still explain every allowed IPv6 session by policy ID. That is the standard I want in our environment.
Mistakes I Watch for During FortiGate IPv6 Rollouts
The biggest mistake is enabling IPv6 on an interface and stopping there. The second biggest mistake is assuming the absence of IPv6 policy hits means there is no IPv6 traffic. In our case, the absence of policy hits meant we had not built the policy layer correctly enough to observe it.
Another common problem is broad prefix use. A /64 object may be technically accurate for a VLAN, but it may be too broad for a sensitive policy if only a small group of hosts should reach a vendor application. I push my team to ask whether the IPv6 object reflects the network boundary or the actual business need.
Logs beat confidence.
Routing also deserves attention. We had one test prefix advertised internally but not consistently routed upstream, which created asymmetric troubleshooting noise that looked like a firewall deny until packet captures proved otherwise. FortiGate made the policy side visible, but the route table still had to agree with the design.
I also check management access separately. Enabling ip6-allowaccess without thinking through source restrictions can expose administrative services on IPv6 even when IPv4 management access is tightly limited. In a plant network, I want firewall administration reachable only from known management hosts, not from every newly dual-stacked segment.
My opinion after this rollout is simple: IPv6 implementation mistakes are rarely exotic. They are usually IPv4 habits wearing a new address format.
Verify IPv6 Before Calling the Change Complete
We now close every FortiGate IPv6 change with a verification checklist. I test from an Ubuntu 22.04 workstation, review FortiOS 7.4.3 policy hits, confirm route symmetry, and use a Python 3.11 script to compare observed source and destination pairs against the approved change record. That sounds heavier than clicking enable, but it is still lighter than explaining an unfiltered path after the fact.
The checks are practical. I run diagnose sniffer packet for targeted flows, review diagnose sys session6 list, confirm policy counters, and validate that deny logs appear when I intentionally test blocked destinations. I want proof that allowed traffic is allowed for the right reason and denied traffic is denied loudly enough for us to notice.
I also leave temporary high-visibility logging in place during the burn-in period. For our SaaS rollout, we kept full IPv6 policy logging for 14 days, then reduced noisy allowed-flow logging after we had enough baseline data. That gave us clean evidence during the post-change review without committing ourselves to permanent log volume we did not need.
One rule stayed permanent: every IPv6 zone boundary gets an explicit deny. I want the log entry. I want the policy ID. I want the next engineer to see intent instead of guessing what the firewall might do by default.
Dual-stack is worth doing, but I only trust it when IPv6 gets the same policy discipline as IPv4. Anything less is not modernization; it is an unmanaged second path.

