Tag: fortigate

  • Quick tips: FortiClient FQDN-based split tunneling with IPsec, FortiGate Web Application Firewall URL access

    Dear reader, I have another installment of the quick tips series. This time the topics are FQDN-based split tunneling for IPsec with FortiClient and how to use the URL access feature of a FortiGate Web Application Firewall (WAF) profile.

    Models and versions:

    • FortiGate 70G on 7.6.7
    • FortiClient EMS on 7.4.7
    • FortiClient on 7.4.6
    • Windows 11 client on 25H2

    FortiClient FQDN-based split tunneling with IPsec

    One of the nice things about SSL-VPN on a FortiGate is the fact that you can do FQDN-based split tunneling with relative ease by configuring it directly in a firewall policy as the destination and performing split tunneling based on the policy destination. With IPsec, this isn’t possible, because that’s not how the protocol works. The documentation even says that you cannot use FQDN objects in the split tunnel configuration.

    Note:

    FQDNs are not supported in split-tunnel destinations. If FQDNs have been configured in the split-tunnel address group, it cannot be applied in the (set ipv4-split-include) config of the dialup IPsec.

    That won’t stop us, however.

    In order to make this happen, we can use EMS’ application-based split tunneling feature while editing a tunnel. In there, you can include or exclude Cloud and Video Streaming Applications, but also Domains, and that last point is the one that is interesting, because you can use FQDNs here.

    In this case, I am adding the FQDN of this blog, blog.guenay.at, to the configuration.

    EMS VPN FQDN-based split tunneling configuration

    The VPN configuration of a FortiGate doesn’t require any special configuration for this to work, and I have configured a regular PSK VPN with split tunneling, that routes 192.168.0.0/16 into the tunnel.

    FortiGate VPN configuration

    config vpn ipsec phase1-interface
        edit "PSK-VPN"
            set type dynamic
            set interface "wan1"
            set ike-version 2
            set keylife 28800
            set peertype any
            set net-device disable
            set mode-cfg enable
            set ipv4-dns-server1 192.168.1.169
            set proposal aes256gcm-prfsha512
            set dhgrp 31
            set eap enable
            set eap-identity send-request
            set authusrgrp "VPN_USERS"
            set client-resume enable
            set client-resume-interval 600
            set transport auto
            set ipv4-start-ip 172.16.101.1
            set ipv4-end-ip 172.16.101.20
            set ipv4-split-include "BASE-RFC1918-192"
            set psksecret ENC TaAvnptXQNmMUc/zjoy5F1CweIG8wOZUwliDjZCfvt8lr36rtJl1i95oOzgQjN22lPhP3tvZ0HOLy5XCQNtGvKO+piZ6I2wDKHVXe4Q787D5NNdrtPwonaDXZxrdyAZ5hl3r3am7zJMhWC5F5Pdz/ms5t5oew98CiSjzPUv/FHHYrxObTvi+IrxH+MW5VmUIa85ZEVlmMjY3dkVA
            set dpd-retryinterval 5
        next
    end
    config vpn ipsec phase2-interface
        edit "PSK-VPN"
            set phase1name "PSK-VPN"
            set proposal aes256gcm
            set dhgrp 31
            set keepalive enable
            set keylifeseconds 3600
        next
    end

    A policy to allow the WAN-bound traffic from the VPN is, of course, also needed for our FQDN split tunneling to work.

    FortiGate VPN WAN firewall policy

    Once the connection on FortiClient is up, we can see, in the log file of the VPN (in my case it is C:\Program Files\Fortinet\FortiClient\logs\trace\iked_ikev2_PSK-VPN7EB027B2_1.log), that split tunneling is active and per-application policies have been applied in include mode.

    [2026-08-08 18:08:27.6368270 UTC+02:00] [18216:12516] [iked  801    info] Detect split tunnel with subnet 192.168.0.0 and mask 255.255.0.0
    [2026-08-08 18:08:27.6368279 UTC+02:00] [18216:12516] [iked  806    info] split is 1
    [2026-08-08 18:08:27.6368294 UTC+02:00] [18216:12516] [iked  365    info] Applying per-application policies
    [2026-08-08 18:08:27.6368305 UTC+02:00] [18216:12516] [iked  366    info] tunnel_name = PSK-VPN
    [2026-08-08 18:08:27.6373625 UTC+02:00] [18216:12516] [iked  2511    info] Non-multiconnect vpn, set dns priority to 1
    [2026-08-08 18:08:27.6373679 UTC+02:00] [18216:12516] [iked  2531    info] dns_priority 1
    [2026-08-08 18:08:27.6373693 UTC+02:00] [18216:12516] [iked  381    info] mode = 1, dp.priority = 1
    [2026-08-08 18:08:27.6373706 UTC+02:00] [18216:12516] [iked  334    info] Applying per-application policies in include mode

    For verification, here is the route table of the VPN-connected Windows client before doing anything else.

    Route table before anything

    IPv4 Route Table
    ===========================================================================
    Active Routes:
    Network Destination        Netmask          Gateway       Interface  Metric
              0.0.0.0          0.0.0.0  192.168.100.100    192.168.100.2     25
              0.0.0.0          0.0.0.0     172.16.101.2     172.16.101.1   9001
            127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
            127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
      127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
         172.16.101.1  255.255.255.255         On-link      172.16.101.1    257
          192.168.0.0      255.255.0.0     172.16.101.2     172.16.101.1      2
        192.168.1.202  255.255.255.255  192.168.100.100    192.168.100.2     26
        192.168.100.0    255.255.255.0         On-link     192.168.100.2    281
        192.168.100.2  255.255.255.255         On-link     192.168.100.2    281
      192.168.100.255  255.255.255.255         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link         127.0.0.1    331
            224.0.0.0        240.0.0.0         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link      172.16.101.1    257
      255.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      255.255.255.255  255.255.255.255         On-link     192.168.100.2    281
      255.255.255.255  255.255.255.255         On-link      172.16.101.1    257
    ===========================================================================
    Persistent Routes:
      Network Address          Netmask  Gateway Address  Metric
              0.0.0.0          0.0.0.0     172.16.101.2  Default
    ===========================================================================

    Once the client browses to the domain listed, the following lines appear in the log file:

    [2026-08-08 18:04:47.2709530 UTC+02:00] [6764:16584] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(46.30.213.70), split_mask(255.255.255.255), local_gwy(192.168.1.228), remote_gwy(192.168.1.202)]
    [2026-08-08 18:04:47.2735528 UTC+02:00] [6764:16584] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(46.30.213.70), split_mask(255.255.255.255), local_gwy(192.168.1.228), remote_gwy(192.168.1.202)]

    The IP corresponds to the IP of blog.guenay.at.

    PS C:\Users\adkevin.AD.000> ping blog.guenay.at
    
    Pinging blog.guenay.at [46.30.213.70] with 32 bytes of data:

    If we check the route table after this, we see that the IP has been added and points towards the VPN interface.

    Route table after adding FQDN route

    IPv4 Route Table
    ===========================================================================
    Active Routes:
    Network Destination        Netmask          Gateway       Interface  Metric
              0.0.0.0          0.0.0.0  192.168.100.100    192.168.100.2     25
              0.0.0.0          0.0.0.0     172.16.101.2     172.16.101.1   9001
         46.30.213.70  255.255.255.255     172.16.101.2     172.16.101.1    100
            127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
            127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
      127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
         172.16.101.1  255.255.255.255         On-link      172.16.101.1    257
          192.168.0.0      255.255.0.0     172.16.101.2     172.16.101.1      2
        192.168.1.202  255.255.255.255  192.168.100.100    192.168.100.2     26
        192.168.100.0    255.255.255.0         On-link     192.168.100.2    281
        192.168.100.2  255.255.255.255         On-link     192.168.100.2    281
      192.168.100.255  255.255.255.255         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link         127.0.0.1    331
            224.0.0.0        240.0.0.0         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link      172.16.101.1    257
      255.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      255.255.255.255  255.255.255.255         On-link     192.168.100.2    281
      255.255.255.255  255.255.255.255         On-link      172.16.101.1    257
    ===========================================================================
    Persistent Routes:
      Network Address          Netmask  Gateway Address  Metric
              0.0.0.0          0.0.0.0     172.16.101.2  Default
    ===========================================================================

    The FortiGate also shows this traffic in the Forward Traffic log.

    FortiGate VPN traffic log

    If we disconnect the VPN, the restoration and removal of the routes can be seen. The domain routes aren’t specifically mentioned here, however.

    [2026-08-08 18:14:11.9604854 UTC+02:00] [13592:13376] [iked  1285    info] Restore route and interface setting...
    [2026-08-08 18:14:11.9626475 UTC+02:00] [13592:13376] [iked  1303    info] Restore routes...
    [2026-08-08 18:14:11.9627101 UTC+02:00] [13592:13376] [iked  364    info] Route del(0): Destination=192.168.1.202 Prefix=32 Nexthop=0.0.0.0 Index=3
    [2026-08-08 18:14:11.9627259 UTC+02:00] [13592:13376] [iked  364    info] Route del(0): Destination=192.168.0.0 Prefix=16 Nexthop=172.16.101.2 Index=59
    [2026-08-08 18:14:11.9627269 UTC+02:00] [13592:13376] [iked  1318    info] Restore interface default route option...
    [2026-08-08 18:14:11.9627275 UTC+02:00] [13592:13376] [iked  1330    info] Restore interface metric...
    [2026-08-08 18:14:11.9633352 UTC+02:00] [13592:13376] [iked  1345    info] Remove route and interface restore proto file(0) The operation completed successfully....

    For fun, let’s see what happens if I use the Video Streaming Applications option and add YouTube and browse to just the homepage.

    Adding YouTube routes and route table

    [2026-08-08 19:15:05.6023606 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.150.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6026407 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.151.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6028006 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.157.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6032742 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.156.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6034173 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.152.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6035045 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.155.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6035589 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.154.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6035967 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.153.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6036401 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.150.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6036984 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.151.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6037528 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.157.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6053667 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.156.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6054232 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.152.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6054551 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.155.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6054903 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.154.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.6055338 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.153.4), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8516774 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.127.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8518532 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.110.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8519067 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.250.154.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8519922 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.20.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8520852 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(192.178.183.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8521251 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.13.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8521565 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.14.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8521861 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.127.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8522174 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.110.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8522468 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.250.154.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8522766 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.20.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8523062 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(192.178.183.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8523485 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.13.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    [2026-08-08 19:15:05.8523786 UTC+02:00] [13044:11056] [iked  54   debug] Issue Policy: [tunnel(PSK-VPN), split_ip(142.251.14.119), split_mask(255.255.255.255), local_gwy(192.168.100.2), remote_gwy(192.168.1.202)]
    
    IPv4 Route Table
    ===========================================================================
    Active Routes:
    Network Destination        Netmask          Gateway       Interface  Metric
              0.0.0.0          0.0.0.0  192.168.100.100    192.168.100.2     25
              0.0.0.0          0.0.0.0     172.16.101.2     172.16.101.1   9001
            127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
            127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
      127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      142.250.154.119  255.255.255.255     172.16.101.2     172.16.101.1    100
       142.251.13.119  255.255.255.255     172.16.101.2     172.16.101.1    100
       142.251.14.119  255.255.255.255     172.16.101.2     172.16.101.1    100
       142.251.20.119  255.255.255.255     172.16.101.2     172.16.101.1    100
      142.251.110.119  255.255.255.255     172.16.101.2     172.16.101.1    100
      142.251.127.119  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.150.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.151.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.152.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.153.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.154.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.155.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.156.4  255.255.255.255     172.16.101.2     172.16.101.1    100
        142.251.157.4  255.255.255.255     172.16.101.2     172.16.101.1    100
         172.16.101.1  255.255.255.255         On-link      172.16.101.1    257
          192.168.0.0      255.255.0.0     172.16.101.2     172.16.101.1      2
        192.168.1.202  255.255.255.255  192.168.100.100    192.168.100.2     26
        192.168.100.0    255.255.255.0         On-link     192.168.100.2    281
        192.168.100.2  255.255.255.255         On-link     192.168.100.2    281
      192.168.100.255  255.255.255.255         On-link     192.168.100.2    281
      192.178.183.119  255.255.255.255     172.16.101.2     172.16.101.1    100
            224.0.0.0        240.0.0.0         On-link         127.0.0.1    331
            224.0.0.0        240.0.0.0         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link      172.16.101.1    257
      255.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      255.255.255.255  255.255.255.255         On-link     192.168.100.2    281
      255.255.255.255  255.255.255.255         On-link      172.16.101.1    257
    ===========================================================================
    Persistent Routes:
      Network Address          Netmask  Gateway Address  Metric
              0.0.0.0          0.0.0.0     172.16.101.2  Default
    ===========================================================================

    Lots of routes are being added, and this is reflected in the route table.

    If you use the exclude mode in the EMS VPN configuration, you see this in the log, and the routes are added to the routing table using the regular interface with your normal default gateway. Funnily enough, you don’t see these routes being added in the log.

    [2026-08-08 18:12:49.3312644 UTC+02:00] [13592:13376] [iked  801    info] Detect split tunnel with subnet 192.168.0.0 and mask 255.255.0.0
    [2026-08-08 18:12:49.3312680 UTC+02:00] [13592:13376] [iked  806    info] split is 1
    [2026-08-08 18:12:49.3312696 UTC+02:00] [13592:13376] [iked  365    info] Applying per-application policies
    [2026-08-08 18:12:49.3312705 UTC+02:00] [13592:13376] [iked  366    info] tunnel_name = PSK-VPN
    [2026-08-08 18:12:49.3327815 UTC+02:00] [13592:13376] [iked  2511    info] Non-multiconnect vpn, set dns priority to 1
    [2026-08-08 18:12:49.3327865 UTC+02:00] [13592:13376] [iked  2531    info] dns_priority 1
    [2026-08-08 18:12:49.3327878 UTC+02:00] [13592:13376] [iked  381    info] mode = 2, dp.priority = 1
    [2026-08-08 18:12:49.3327889 UTC+02:00] [13592:13376] [iked  287    info] Applying per-application policies in exclude mode
    
    IPv4 Route Table
    ===========================================================================
    Active Routes:
    Network Destination        Netmask          Gateway       Interface  Metric
              0.0.0.0          0.0.0.0  192.168.100.100    192.168.100.2     25
              0.0.0.0          0.0.0.0     172.16.101.2     172.16.101.1   9001
            127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
            127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
      127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      142.250.154.119  255.255.255.255  192.168.100.100    192.168.100.2    124
       142.251.13.119  255.255.255.255  192.168.100.100    192.168.100.2    124
       142.251.14.119  255.255.255.255  192.168.100.100    192.168.100.2    124
       142.251.20.119  255.255.255.255  192.168.100.100    192.168.100.2    124
      142.251.110.119  255.255.255.255  192.168.100.100    192.168.100.2    124
      142.251.127.119  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.150.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.151.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.152.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.153.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.154.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.155.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.156.4  255.255.255.255  192.168.100.100    192.168.100.2    124
        142.251.157.4  255.255.255.255  192.168.100.100    192.168.100.2    124
         172.16.101.1  255.255.255.255         On-link      172.16.101.1    257
          192.168.0.0      255.255.0.0     172.16.101.2     172.16.101.1      2
        192.168.1.202  255.255.255.255  192.168.100.100    192.168.100.2     26
        192.168.100.0    255.255.255.0         On-link     192.168.100.2    281
        192.168.100.2  255.255.255.255         On-link     192.168.100.2    281
      192.168.100.255  255.255.255.255         On-link     192.168.100.2    281
      192.178.183.119  255.255.255.255  192.168.100.100    192.168.100.2    124
            224.0.0.0        240.0.0.0         On-link         127.0.0.1    331
            224.0.0.0        240.0.0.0         On-link     192.168.100.2    281
            224.0.0.0        240.0.0.0         On-link      172.16.101.1    257
      255.255.255.255  255.255.255.255         On-link         127.0.0.1    331
      255.255.255.255  255.255.255.255         On-link     192.168.100.2    281
      255.255.255.255  255.255.255.255         On-link      172.16.101.1    257
    ===========================================================================
    Persistent Routes:
      Network Address          Netmask  Gateway Address  Metric
              0.0.0.0          0.0.0.0     172.16.101.2  Default
    ===========================================================================

    This method of FQDN-based split tunneling is a nice compromise when transitioning from SSL-VPN to IPsec. Don’t forget the Cloud Applications option in this, because it offers a few often-used services and acts like the other options.

    FortiGate Web Application Firewall URL access

    Due to a blog post about FortiClient EMS Let’s Encrypt Security that I wrote for my employer, I was told about the URL access feature that lives inside the WAF profile of a FortiGate, which I wasn’t aware of before. Outside of the CLI reference, I cannot find any documentation of this feature for a FortiGate, only for FortiWeb, but it is relatively self-explanatory. Still, let’s see how to configure it.

    Note: The URL access feature is CLI-only, but you should still enable Web Application Firewall in Feature Visibility, because otherwise the option to assign the profile in a policy does not show up in the GUI. Also, you can do much the same with a web filter profile, which I do in the post above, so the WAF option is not the only method to achieve this; it just saves on another profile.

    On the CLI, you can configure a basic URL access block as follows:

    config waf profile
        edit "WAF_URL-ACCESS"
            config url-access
                edit 1
                    set address "ubuntu-ws-1.ad.labdomain.com"
                    set action block
                    set log enable
                    config access-pattern
                        edit 1
                            set srcaddr "all"
                            set pattern ".*/login.html$"
                            set regex enable
                        next
                    end
                next
            end
        next
    end

    Let’s go through it.

    The address option determines the backend host that is being protected, and it is an actual address object. I have tested it with the types ipmask and fqdn, and both worked here, assuming your DNS resolution is correct for the FQDN option.

    action has the block option, which does exactly that, bypass, which allows the connection and skips all other WAF scanning, and permit, which allows the connection and continues with WAF scanning.

    log is for logging; who would have thought.

    In the access-pattern entry itself, we can set the srcaddr option, which determines to which source address this should apply, so we can target only specific addresses. Again, this is an actual address object.

    Enabling regex does that, and because I prefer regex-matching over everything else, I have it enabled. Disabling it probably does a simple match on the URL, but I didn’t test this because if regex is available, I will use only that.

    The actual regex pattern is used in the pattern option. In my case, I am checking for every URL path that ends in /login.html.

    That’s it on how to configure the feature.

    Note that there is no implicit deny rule here, so if you don’t have an entry for a pattern, it is allowed. Entries are matched top down, so keep that in mind.

    Assign the WAF profile in a policy, and if you perform deep inspection, either with an SSL/SSH profile or a virtual server, you can also control access in HTTPS traffic.

    FortiGate WAF firewall policy

    If I test the URL https://ubuntu-ws-1.ad.labdomain.com/login.html on my client, I get the block, as expected, and I see this in my FortiGate Web Application Firewall log.

    Client WAF URL access block
    WAF URL access block log

    Browsing to any other URL, like https://ubuntu-ws-1.ad.labdomain.com/index.html or just https://ubuntu-ws-1.ad.labdomain.com, does not lead to a block, because I never explicitly mentioned this in my WAF profile and I also don’t have a catch-all entry.

    If you ever need to debug this feature, then you need to know that the WAF feature uses the wad process, and the category http is the important one.

    diagnose wad debug enable category http
    diagnose wad debug enable level info
    diagnose debug enable

    In the debugs, you can see the request coming in and the URL access being matched.

    FortiGate Web Application Firewall debug

    [I][p:1758][s:11239][r:33554552] wad_dump_http_request             :3134  hreq=0x7f9df0d8e0 Received request from client: 192.168.100.2:56531
    
    GET /login.html HTTP/1.1
    Host: ubuntu-ws-1.ad.labdomain.com
    Connection: keep-alive
    Pragma: no-cache
    Cache-Control: no-cache
    sec-ch-ua: "Not=A?Brand";v="99", "Microsoft Edge";v="151", "Chromium";v="151"
    sec-ch-ua-mobile: ?0
    sec-ch-ua-platform: "Windows"
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    Sec-Fetch-Site: none
    Sec-Fetch-Mode: navigate
    Sec-Fetch-User: ?1
    Sec-Fetch-Dest: document
    Accept-Encoding: gzip, deflate, br, zstd
    Accept-Language: en-GB,en;q=0.9,en-US;q=0.8
    Cookie: session=expiry=1786211939675194
    
    [I][p:1758][s:11239][r:33554552] wad_http_parse_host               :2019  host=[28]ubuntu-ws-1.ad.labdomain.com
    [I][p:1758][s:11239][r:33554552] wad_http_str_canonicalize         :2621  enc=0 path=/login.html len=11 changes=0
    [I][p:1758][s:11239][r:33554552] wad_http_req_detect_special       :14589 captive_portal detected: false, preflight=(null)
    [I][p:1758][s:11239][r:33554552] wad_http_req_domain_front_chk     :9751  chk uri host res=pass (is_abs=0 http_encap=1 connect=0): ubuntu-ws-1.ad.labdomain.com
    [I][p:1758][s:11239][r:33554552] wad_http_waf_access_control       :1242
    [I][p:1758][s:11239][r:33554552] wad_http_waf_access_control       :1269  WAF matched URL access action=1
    [I][p:1758][s:11239][r:33554552] __wad_http_build_replmsg_resp     :933   Generating replacement message. repmsg_id 77
    [I][p:1758][s:11239][r:33554552] wad_dump_fwd_http_resp            :3155  hreq=0x7f9df0d8e0 Forward response from Internal:
    
    HTTP/1.1 403 Forbidden
    Connection: close
    Content-Type: text/html
    Cache-Control: no-cache
    X-Frame-Options: SAMEORIGIN
    X-XSS-Protection: 1; mode=block
    X-Content-Type-Options: nosniff
    Content-Security-Policy: frame-ancestors 'self'
    Content-Length: 35039

    If you want a configuration that allows a specific URL path and blocks all others, you can configure it like this:

    config waf profile
        edit "WAF_URL-ACCESS"
            config url-access
                edit 1
                    set address "ubuntu-ws-1.ad.labdomain.com"
                    set log enable
                    config access-pattern
                        edit 1
                            set srcaddr "all"
                            set pattern ".*/login.html$"
                            set regex enable
                        next
                    end
                next
                edit 2
                    set address "ubuntu-ws-1.ad.labdomain.com"
                    set action block
                    set log enable
                    config access-pattern
                        edit 1
                            set srcaddr "all"
                            set pattern ".*"
                            set regex enable
                        next
                    end
                next
            end
        next
    end

    And that’s that feature explained.

    Short and sweet and done

    The configuration for both things in this quick tips installment isn’t anything to write home about, as it should be for this series, but I haven’t seen much on either of these things, so hopefully this helps at least you, dear reader.

  • A real look at FortiGate FGCP HA BGP failover behaviour

    A FortiGate Cluster Protocol (FGCP) HA deployment is nothing new in today’s world, and with more and more of these clusters functioning as BGP routers, especially with the proliferation of SD-WAN and ADVPN, having a high BGP service uptime is becoming critical. In order to achieve this, there are a few things to keep in mind. These things, dear reader, are what this post is about.

    The setup

    • 2x FortiGate 70G on 7.6.7
    • FortiGate 60F on 7.6.5
    • 2x Windows 11 clients on 25H2
    BGP HA failover topology

    A BGP peering in the AS 65001 over the 192.0.2.0/24 subnet has been established between the 70G cluster and the standalone 60F.

    Both FortiGate deployments have a client behind them; they are announcing the client subnets, and the 60F is also announcing subnets ranging from 169.254.1.0/24 to 169.254.250.0/24 as a very small stress test.

    Note: When I use the word “downstream”, I mean any BGP peer of the HA cluster. This doesn’t mean the peer is actually downstream of the traffic flow. Writing “BGP peer of the HA cluster” every time would be bad writing.

    The most important information

    You should get the reason for reading this post as quickly as possible, so here it is, if you start with default values:

    • Enable Graceful Restart either at the BGP global level or per neighbor on both systems
    • The route-ttl setting of the HA cluster needs to be high enough to allow for full route convergence to happen
    • Keeping the preceding point in mind, the downstream device needs to have a BGP advertisement-interval at least lower than the route-ttl value of the HA cluster
    • The downstream device must not tear down the existing BGP session before the new BGP session has been established on the new primary FortiGate, meaning keep keepalive and holdtime in mind

    Basics about FGCP BGP behaviour

    Before I get into the meat and potatoes, I want to put out some information on how BGP behaves in an FGCP cluster.

    • The BGP routing process and thus the BGP neighborships only exist on the primary unit.
    • Once a failover happens, the new primary has to establish the BGP neighborship again.
    • Routes are copied from the primary to the secondary at the route-hold interval (default 10 seconds).
    • route-wait determines how long to wait after the primary recognizes a routing table update until it copies it to the secondary (default 0 seconds, meaning on every routing table update).
    • Routes are kept in a cluster for the route-ttl duration (default 10 seconds).
    • The copied routes are visible in the kernel table on the secondary using get router info kernel command. You do not see them with the get router info routing-table database command.
    • Importantly, once the route-ttl timer has run out and the new primary has not received the kernel routes previously acquired from the BGP peer, again, these kernel routes will be discarded. This behaviour is most likely consistent across all routing protocols, not just BGP.

    Here is the regular routing table and the kernel routing table output of the secondary:

    70G-BGP1(Secondary) # get router info routing-table database
    
    70G-BGP1(Secondary) #
    
    70G-BGP1(Secondary) # get router info kernel | grep port2
    tab=65535 vf=0 vrf=0 scope=253 type=3 proto=2 prio=0 0.0.0.0/0.0.0.0/0->192.0.2.0/32 pref=192.0.2.1 gwy=0.0.0.0 dev=6(port2)
    tab=65535 vf=0 vrf=0 scope=254 type=2 proto=2 prio=0 0.0.0.0/0.0.0.0/0->192.0.2.1/32 pref=192.0.2.1 gwy=0.0.0.0 dev=6(port2)
    tab=65535 vf=0 vrf=0 scope=253 type=3 proto=2 prio=0 0.0.0.0/0.0.0.0/0->192.0.2.255/32 pref=192.0.2.1 gwy=0.0.0.0 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->10.10.10.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.1.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.2.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.3.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.4.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.5.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    tab=65534 vf=0 vrf=0 scope=0 type=1 proto=19 prio=2147483649 0.0.0.0/0.0.0.0/0->169.254.6.0/24 pref=0.0.0.0 gwy=192.0.2.2 dev=6(port2)
    ...

    With this information in mind, the failover process for BGP looks as follows, with default values:

    1. The cluster performs a failover
    2. The new primary forms a new BGP neighborship
    3. The downstream peer announces its routes
      • If this happens after the route-ttl timer, connectivity is impacted.
    4. After all routes have been received and installed, normal operations continue

    With default values, you will lose connectivity if traffic relies on BGP routing despite the copied routes because of the route-ttl timer, but this will be solved.

    More information on route-ttl, route-hold, and route-wait is in the official documentation.

    With default values

    Assuming we did nothing to our BGP configuration, and we only did the minimum to establish a session, we will observe that upon a failover, the new primary will lose its BGP kernel routes after 10 seconds, the route-ttl timer.

    Here is the BGP configuration of the HA cluster and the neighbor output of the downstream peer:

    HA cluster BGP configuration and neighbor output of downstream

    70G-BGP1(Primary) # show router bgp
    config router bgp
        set as 65001
        set router-id 192.0.2.1
        config neighbor
            edit "192.0.2.2"
                set activate6 disable
                set interface "port2"
                set remote-as 65001
                set update-source "port2"
            next
        end
        config network
            edit 1
                set prefix 192.168.1.0 255.255.255.0
            next
        end
        config redistribute "connected"
        end
        config redistribute "rip"
        end
        config redistribute "ospf"
        end
        config redistribute "static"
        end
        config redistribute "isis"
        end
        config redistribute6 "connected"
        end
        config redistribute6 "rip"
        end
        config redistribute6 "ospf"
        end
        config redistribute6 "static"
        end
        config redistribute6 "isis"
        end
    end
    
    60F-DOWNSTREAM # get router info bgp neighbors 192.0.2.1
    VRF 0 neighbor table:
    BGP neighbor is 192.0.2.1, remote AS 65001, local AS 65001, internal link
      BGP version 4, remote router ID 192.0.2.1
      BGP state = Established, up for 00:04:31
      Last read 00:00:54, hold time is 180, keepalive interval is 60 seconds
      Configured hold time is 180, keepalive interval is 60 seconds
      Neighbor capabilities:
        Route refresh: advertised and received (old and new)
        Address family IPv4 Unicast: advertised and received
        Address family VPNv4 Unicast: advertised and received
        Address family VPNv6 Unicast: advertised and received
        Address family L2VPN EVPN: advertised and received
      Received 32 messages, 1 notifications, 0 in queue
      Sent 35 messages, 0 notifications, 0 in queue
      Route refresh request: received 0, sent 0
      NLRI treated as withdraw: 0
      Minimum time between advertisement runs is 30 seconds
      Update source is SW-BGP
    
     For address family: IPv4 Unicast
      BGP table version 13, neighbor version 13
      Index 1, Offset 0, Mask 0x2
      Community attribute sent to this neighbor (both)
      1 accepted prefixes, 1 prefixes in rib
      1 announced prefixes

    It takes about 3 minutes and 30 seconds for the new primary to get all routes. This duration comes from the keepalive timer of the downstream peer, with a default of 180 seconds and an advertisement interval of 30 seconds.

    Looking at the debugs from the downstream peer, we see this behaviour (I have tried to align the failover with the last keepalive being received):

    BGP debug with default values

    2026-07-19 14:30:29 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:30:29 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 26
    2026-07-19 14:30:34 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 14
    2026-07-19 14:30:35 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 58 KAlive msg(s) sent
    2026-07-19 14:30:43 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:30:43 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:30:58 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:31:10 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:31:13 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:31:28 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:31:28 [root] BGP: [RIB] Scanning BGP RIB for VRF 0...
    2026-07-19 14:31:28 [root] BGP: [NSM] Verified NH 192.0.2.1 with NSM
    2026-07-19 14:31:28 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 59 KAlive msg(s) sent
    2026-07-19 14:31:36 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:31:43 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:31:58 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:31:59 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:32:12 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 14
    2026-07-19 14:32:13 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:32:23 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 60 KAlive msg(s) sent
    2026-07-19 14:32:25 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:32:28 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:32:28 [root] BGP: [RIB] Scanning BGP RIB for VRF 0...
    2026-07-19 14:32:28 [root] BGP: [NSM] Verified NH 192.0.2.1 with NSM
    2026-07-19 14:32:43 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:32:50 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:32:58 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:33:13 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:33:19 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 61 KAlive msg(s) sent
    2026-07-19 14:33:20 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:33:28 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:33:28 [root] BGP: [RIB] Scanning BGP RIB for VRF 0...
    2026-07-19 14:33:28 [root] BGP: [NSM] Verified NH 192.0.2.1 with NSM
    2026-07-19 14:33:29 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 10
    2026-07-19 14:33:29 [root] BGP: %BGP-3-NOTIFICATION: sending to 192.0.2.1 4/0 (Hold Timer Expired/Unspecified Error Subcode) 0 data-bytes []
    2026-07-19 14:33:29 [root] BGP: VRF 0 NSM withdraw: 192.168.1.0/24
    2026-07-19 14:33:29 [root] BGP: [GRST] Timer Announce Defer: Check VRF=0
    2026-07-19 14:33:29 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down Hold Timer Expired"
    2026-07-19 14:33:29 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down BGP Notification FSM-ERR"
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Idle Event: 3
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [NETWORK] FD=28, Sock Status: 0-Success
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Connect Event: 17
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenSent Event: 19
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 62 KAlive msg(s) sent
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:33:35 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenConfirm Event: 26
    2026-07-19 14:33:35 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Up "
    2026-07-19 14:33:43 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:33:58 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:34:01 [root] BGP: 192.0.2.1-Outgoing [DECODE] Update: NLRI Len(4)
    2026-07-19 14:34:01 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 27
    2026-07-19 14:34:01 [root] BGP: 192.0.2.1-Outgoing [RIB] Update: Received Prefix 192.168.1.0/24 path_id 0
    2026-07-19 14:34:01 [root] BGP: VRF 0 NSM announce: 192.168.1.0/24
    2026-07-19 14:34:03 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    • At 14:30:29, the last keepalive was received
    • At 14:33:29, exactly 3 minutes/180 seconds later, the hold timer expired
    • At 14:33:35, the HA peer comes up
    • At 14:34:01, the prefix is received

    This is a worst-case scenario. The convergence time can be lower because the hold timer expiration depends on when the last keepalive was successfully received, so you can subtract up to 59 seconds here.

    Tweaking timers

    Keepalive and hold timer values can be easily changed, and we can do this at both the global and neighbor level on a FortiGate. I have set both timers to their minimum at the global level on both devices. Technically, it’s enough to only do it on the downstream peer. Note that changing these values requires a new BGP session.

    config router bgp
        set as 65001
        set router-id 192.0.2.2
        set keepalive-timer 1
        set holdtime-timer 3
    end

    Looking at the debugs on the downstream again, we see that we get our prefix much quicker. It takes around 33 seconds now.

    BGP debug with minimum keepalive and holdtimer values

    2026-07-19 14:41:12 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 260 KAlive msg(s) sent
    2026-07-19 14:41:13 [root] BGP: [RIB] Scanning BGP Network Routes for VRF 0...
    2026-07-19 14:41:13 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 10
    2026-07-19 14:41:13 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 3
    2026-07-19 14:41:13 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down Hold Timer Expired"
    2026-07-19 14:41:13 [root] BGP: %BGP-3-NOTIFICATION: sending to 192.0.2.1 4/0 (Hold Timer Expired/Unspecified Error Subcode) 0 data-bytes []
    2026-07-19 14:41:13 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down BGP Notification FSM-ERR"
    2026-07-19 14:41:13 [root] BGP: VRF 0 NSM withdraw: 192.168.1.0/24
    2026-07-19 14:41:13 [root] BGP: [GRST] Timer Announce Defer: Check VRF=0
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Idle Event: 3
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [NETWORK] FD=28, Sock Status: 0-Success
    ...
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [DECODE] Open Cap: RR Cap(new) for all address-families
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [DECODE] Open Opt: Option Type 2, Option Len 6
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [DECODE] Open Cap: Cap Code 65, Cap Len 4
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenSent Event: 19
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 261 KAlive msg(s) sent
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 4, length 19
    2026-07-19 14:41:16 [root] BGP: bgp_keepalive_proc: notif_rcv 4-4
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenConfirm Event: 26
    2026-07-19 14:41:16 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Up "
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:41:16 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 262 KAlive msg(s) sent
    2026-07-19 14:41:17 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 4, length 19
    2026-07-19 14:41:17 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:41:17 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 26
    2026-07-19 14:41:17 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    ...
    2026-07-19 14:41:44 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:41:44 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 26
    2026-07-19 14:41:44 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:41:44 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 290 KAlive msg(s) sent
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 2, length 48
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [DECODE] Update: Starting UPDATE decoding... Bytes To Read (29), msg_size (29)
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [DECODE] Update: NLRI Len(4)
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 27
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [RIB] Update: Received Prefix 192.168.1.0/24 path_id 0
    2026-07-19 14:41:45 [root] BGP: [NSM] Verified NH 192.0.2.1 with NSM
    2026-07-19 14:41:45 [root] BGP: VRF 0 NSM announce: 192.168.1.0/24
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 291 KAlive msg(s) sent
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 4, length 19
    2026-07-19 14:41:45 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    • At 14:41:12, the last keepalive is being sent
    • At 14:41:13, the hold timer expires
    • At 14:41:16, the HA peer comes up
    • At 14:41:45, the prefix is received

    These numbers make sense because we have a hold timer of 3 seconds and an advertisement interval of 30 seconds.

    Faster advertisements

    It’s obvious that if we advertise routes more often, we get our routes faster.

    Changing the advertisement interval on the downstream peer to 1 means we get to route convergence in about 7 seconds.

    config router bgp
        set as 65001
        set router-id 192.0.2.2
        config neighbor
            edit "192.0.2.1"
                set advertisement-interval 1
            next
        end
    end

    BGP debugs with changed advertisement interval

    2026-07-19 14:45:00 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 484 KAlive msg(s) sent
    2026-07-19 14:45:01 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:45:01 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 10
    2026-07-19 14:45:01 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 3
    2026-07-19 14:45:01 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down Hold Timer Expired"
    2026-07-19 14:45:01 [root] BGP: %BGP-3-NOTIFICATION: sending to 192.0.2.1 4/0 (Hold Timer Expired/Unspecified Error Subcode) 0 data-bytes []
    2026-07-19 14:45:01 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Down BGP Notification FSM-ERR"
    2026-07-19 14:45:01 [root] BGP: VRF 0 NSM withdraw: 192.168.1.0/24
    2026-07-19 14:45:01 [root] BGP: [GRST] Timer Announce Defer: Check VRF=0
    2026-07-19 14:45:05 [root] BGP: [NETWORK] Accept Thread: Incoming conn from host 192.0.2.1 (FD=28 VRF=0)
    2026-07-19 14:45:05 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Idle Event: 14
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Idle Event: 3
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [NETWORK] FD=28, Sock Status: 0-Success
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Connect Event: 17
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 1
    ...
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenSent Event: 19
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 485 KAlive msg(s) sent
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 4, length 19
    2026-07-19 14:45:07 [root] BGP: bgp_keepalive_proc: notif_rcv 4-4
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:45:07 [root] BGP: 192.0.2.1-Outgoing [FSM] State: OpenConfirm Event: 26
    2026-07-19 14:45:07 id=20300 msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 192.0.2.1 Up "
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 2, length 48
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [DECODE] Update: Starting UPDATE decoding... Bytes To Read (29), msg_size (29)
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [DECODE] Update: NLRI Len(4)
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 27
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [RIB] Update: Received Prefix 192.168.1.0/24 path_id 0
    2026-07-19 14:45:08 [root] BGP: VRF 0 NSM announce: 192.168.1.0/24
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [DECODE] Msg-Hdr: type 4, length 19
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [DECODE] KAlive: Received!
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 26
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [FSM] State: Established Event: 34
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 2
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Attr IP-Unicast: Tot-attr-len 21
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Update: Msg #15 Size 48
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Msg-Hdr: Type 4
    2026-07-19 14:45:08 [root] BGP: 192.0.2.1-Outgoing [ENCODE] Keepalive: 486 KAlive msg(s)
    • At 14:45:00, the last keepalive is being sent
    • At 14:45:01, the hold timer expires
    • At 14:45:07, the HA peer comes up
    • At 14:45:08, the prefix is received

    That’s okay, but we still lose traffic.

    Some more timers

    The following timers don’t actually change anything for this scenario, but they are still helpful in regular operations, and I want to highlight them. These are the scan and connect timers.

    The connect-timer value determines how long the FortiGate waits before attempting a new BGP connection attempt after the previous one has failed.

    The scan-time determines the interval at which the FortiGate scans for next-hop reachability, and if the next hop isn’t reachable, the route gets dropped.

    config router bgp
        set as 65001
        set router-id 192.0.2.2
        set scan-time 5
        config neighbor
            edit "192.0.2.1"
                set connect-timer 1
            next
        end
    end

    You can read more about these timers and lots of other ones in the official documentation.

    Not tearing down the session with Graceful Restart

    So we are down to around 8 seconds, and this is with a torn-down session on the downstream side. At this point it’s pointless to try and optimize this approach. We need the session to not get torn down on the downstream peer and keep the routes available on both sides.

    If you know BGP, you already know the answer to this: Graceful Restart (GR)

    Graceful Restart is designed to keep routes in the Routing Information Base (RIB) even if the peer is down. There are timers around this, but the defaults will be enough for us. Keep in mind that both sides need to support GR for this to work. Every half-decent router should be able to, but I just want to mention it.

    GR can be enabled at the global and neighbor level on a FortiGate, and doing so clears your BGP sessions, either globally or only for a neighbor. I enable it on the neighbor.

    70G-BGP1(Primary) # show router bgp
    onfig router bgp
        set as 65001
        set router-id 192.0.2.1
        config neighbor
            edit "192.0.2.2"
                set capability-graceful-restart enable
            next
        end
    end

    I have also unset all other previous settings, so here is the full BGP config (excluding network statements and redistribute sections) of both devices at this point:

    60F-DOWNSTREAM # show router bgp
    config router bgp
        set as 65001
        set router-id 192.0.2.2
        config neighbor
            edit "192.0.2.1"
                set activate6 disable
                set capability-graceful-restart enable
                set interface "SW-BGP"
                set remote-as 65001
                set update-source "SW-BGP"
            next
        end
    end
    
    70G-BGP1(Primary) # show router bgp
    config router bgp
        set as 65001
        set router-id 192.0.2.1
        config neighbor
            edit "192.0.2.2"
                set activate6 disable
                set capability-graceful-restart enable
                set interface "port2"
                set remote-as 65001
                set update-source "port2"
            next
        end
    end

    For this exercise, it’s technically enough to enable GR only for the HA peer, but you might as well do it for both peers.

    You can verify the GR capability using the get router info bgp neighbors <NEIGHBOR_IP> command.

    BGP neighbor output after graceful restart

    60F-DOWNSTREAM # get router info bgp neighbors 192.0.2.1
    VRF 0 neighbor table:
    BGP neighbor is 192.0.2.1, remote AS 65001, local AS 65001, internal link
      BGP version 4, remote router ID 192.0.2.1
      BGP state = Established, up for 00:00:23
      Last read 00:00:01, hold time is 3, keepalive interval is 1 seconds
      Configured hold time is 3, keepalive interval is 1 seconds
      Neighbor capabilities:
        Route refresh: advertised and received (old and new)
        Address family IPv4 Unicast: advertised and received
        Address family VPNv4 Unicast: advertised and received
        Address family VPNv6 Unicast: advertised and received
        Address family L2VPN EVPN: advertised and received
      Received 1509 messages, 7 notifications, 0 in queue
      Sent 1550 messages, 16 notifications, 0 in queue
      Route refresh request: received 0, sent 0
      NLRI treated as withdraw: 0
      Minimum time between advertisement runs is 1 seconds
      Update source is SW-BGP
    
     For address family: IPv4 Unicast
      BGP table version 19, neighbor version 18
      Index 1, Offset 0, Mask 0x2
      AF-dependant capabilities:
        Graceful restart: advertised, received, negotiated
          Forwarding states are being preserved
      Community attribute sent to this neighbor (both)
      1 accepted prefixes, 1 prefixes in rib
      1 announced prefixes
    
     For address family: VPNv4 Unicast
      BGP table version 1, neighbor version 1
      Index 1, Offset 0, Mask 0x2
      Community attribute sent to this neighbor (both)
      0 accepted prefixes, 0 prefixes in rib
      0 announced prefixes
    
     For address family: VPNv6 Unicast
      BGP table version 1, neighbor version 1
      Index 1, Offset 0, Mask 0x2
      Community attribute sent to this neighbor (both)
      0 accepted prefixes, 0 prefixes in rib
      0 announced prefixes
    
     For address family: L2VPN EVPN
      BGP table version 1, neighbor version 1
      Index 1, Offset 0, Mask 0x2
      Community attribute sent to this neighbor (both)
      0 accepted prefixes, 0 prefixes in rib
      0 announced prefixes
    
     Connections established 24; dropped 23
     Graceful-restart Status:
      Remote restart-time is 120 sec

    We see Graceful restart: advertised, received, negotiated for the IPv4 Unicast address family, and we also see some additional information at the bottom regarding the remote restart time.

    With GR enabled, our BGP routing behaviour is much better. It’s not interesting to show any debugs, but on a failover, traffic forwarding works fully up until the route-ttl timer, default of 10 seconds, is over (this behaviour was explained further above). At that point, connectivity is lost until the routes get announced again.

    We can work and fix this with our acquired knowledge.

    The last tweaks

    Now we need to make sure two things happen:

    1. Routes are announced more often
    2. Routes are kept in the cluster’s kernel routing table for longer, or at least until we get the new routes

    We know the answer to both, and the answers are advertisement-interval and route-ttl.

    If we set the advertisement interval on the downstream at least lower than the route TTL of the cluster, everything should be fine. So an interval of 3 seconds with a TTL of 10 should be enough. This is theoretically correct, but it also depends on your environment. If you expect to receive a lot of routes, it might take longer to ingest them all, so consider upping the TTL past the default, or reducing the advertisement interval, or doing both.

    My downstream peer is only announcing 251 routes, and with an interval of 3 and a TTL of 10, I see zero traffic loss.

    The final configuration

    This doesn’t differ much from the configuration posted in the GR section, except that the advertisement interval is now set to something other than the default. The route TTL is at its default of 10, but it’s still shown.

    60F-DOWNSTREAM # show router bgp
    config router bgp
        set as 65001
        set router-id 192.0.2.2
        config neighbor
            edit "192.0.2.1"
                set advertisement-interval 3
                set activate6 disable
                set capability-graceful-restart enable
                set interface "SW-BGP"
                set remote-as 65001
                set update-source "SW-BGP"
            next
        end
    end
    
    70G-BGP1(Primary) # show router bgp
    config router bgp
        set as 65001
        set router-id 192.0.2.1
        config neighbor
            edit "192.0.2.2"
                set activate6 disable
                set capability-graceful-restart enable
                set interface "port2"
                set remote-as 65001
                set update-source "port2"
            next
        end
    end
    config system ha
        set route-ttl 10
    end

    With this configuration, I was able to perform an HA failover without losing any pings or interruption of a file transfer between the downstream client and the HA client.

    You can adapt this configuration with the scan and connect timers, and of course whatever else you need, but be careful about the hold and keepalive timers, lest you tear down the BGP session.

    I heard about BFD

    No. Bidirectional Forwarding Detection (BFD) is a great feature that helps with detecting failures in routing sessions, but in this case, it does the opposite of what we want. If BFD detects that its session is down, it will also tear down the BGP session, which would lead to traffic loss.

    When it comes to combining BFD with GR, you have to consult the vendor documentation because some vendors might support this combination. Start with the assumption that this is not supported; however, Fortinet explicitly does not recommend it, as can be read here and here.

    Use BFD with caution. Combining BFD with graceful restart is not recommended by Fortinet. Other vendors also explicitly mention not to combine them in configuration, as it might cause suboptimal routing performances when graceful restart and BFD are both configured.

    BGP graceful restart or OSPF graceful restart doesn’t work with BFD, and it is not recommended.

    Wrapping up

    The fact that graceful restart was the solution to this problem wasn’t a surprise, because that’s what it is designed for, but the behaviour with route-ttl was new to me, and I am glad that I spent the time creating this post, because I have definitely learned something new. I hope that you, dear reader, have also learned something.

  • Connecting to an HA FortiClient EMS cluster without an external load balancer

    Do you want redundancy? Don’t answer that question; the answer is “Yes!” This means you want your FortiClient EMS deployment to be redundant, and this gives you the problem of how to handle the FortiClients and FortiGate connection to your HA EMS nodes if you don’t have an external load balancer.

    Well, dear reader, I’ve got just the solution for you, so let’s see how it’s done.

    The setup

    • FortiGate 70G on 7.6.7
    • 2x FortiClient EMS on 7.4.7
    • Windows 11 client on 25H2 with FortiClient 7.4.7

    As far as layer 3 is concerned, I have a CLIENT and DMZ VLAN, where, respectively, the client and the EMS instances are located, as well as the UNUSED VLAN, which gets used later.

    Topology

    The configuration is very light this time, but it is still on the Fortinet resources GitHub repository.

    FortiClient EMS HA basics

    First things first. There are several HA deployment options, and I specifically cover the one using only VM appliances. As the documentation states, this requires FortiClient EMS 7.4.5, so keep this in mind.

    Setting the cluster up is not hard; it took me about 5 minutes, and there are no additional licenses necessary; you just need the resources for the VM.

    A few points here:

    • In order to change the priority of the nodes, you use the ha standby command on the current primary (use ha get nodes to see the cluster state). There are two roles in the cluster: the database and EMS
    • If you want to demote the current primary for both roles, you use ha standby --type=”db” and ha standby --type=”ems”. The documentation linked above only mentions it but doesn’t give an example of how to do it in the document. You need the CLI reference for this.
      • In the following example, EMS2 is the primary for both roles at first and gets demoted
      • I have removed the “Preferred DCs” column so it fits nicely in here, and because it’s empty anyway
    ems@EMS2 $> ha get nodes
    EMS Node(s):
     Name                                             | Role    | Status  | Last Seen
    --------------------------------------------------+---------+---------+----------------------------
     EMS2 (*)                                         | primary | online  | 2026-06-18 18:32:57.841091
     EMS1                                             | standby | online  | 2026-06-18 18:33:00.042757
    
    DB Node(s):
     Host                            | Port    | Role    | Status   | Latency (ms)
    --------------------------------------------------+---------+---------+------------
     192.168.1.177                   | 5432    | standby | online   | 28
     127.0.0.1                       | 5432    | primary | online   | 27
    ems@EMS2 $> ha standby --type="db"
    Node demoted successfully!
    ems@EMS2 $> ha standby --type="ems"
    Node demoted successfully!
    ems@EMS2 $> ha get nodes
    EMS Node(s):
     Name                                             | Role    | Status  | Last Seen
    --------------------------------------------------+---------+---------+----------------------------
     EMS1                                             | primary | online  | 2026-06-18 18:35:10.04539
     EMS2 (*)                                         | standby | online  | 2026-06-18 18:35:01.322546
    
    DB Node(s):
     Host                            | Port    | Role    | Status   | Latency (ms)
    --------------------------------------------------+---------+---------+------------
     192.168.1.177                   | 5432    | primary | online   | 27
     127.0.0.1                       | 5432    | standby | online   | 26
    
    • The telemetry service, TCP/8013, is only active on the current primary, and the HTTPS service is active on both nodes
      • This is an important point for later
    • EMS does not have its own virtual IP or load balancer service, like haproxy, so any outside connections to it have to get load balanced via something. If you have an external load balancer, this is simple, but that’s a luxury not everyone has
    • The failover time is calculated using the following formula: High Availability Keep Alive Interval * 2 + 60
      • The High Availability Keep Alive Interval is configured in the EMS Settings menu
      • With default settings, this means it takes 80 seconds for a failover to occur

      With the basics covered, let’s connect things to EMS.

      The FortiClient to EMS connection

      This one is easy. You can use the Load Balance feature on the FortiGate to accomplish this (it has to be enabled in Feature Visibility first), and then you can configure everything in the Virtual Servers menu under Policy & Objects.

      For FortiClients, we need, at the minimum, TCP/8013 for the telemetry, so create your virtual server using that as the port and also the health check (either in-line or using the Health Check menu). If you need the installer port, default TCP/11443, you can do that too.

      FortiClient EMS HA Virtual Server
      config firewall vip
          edit "EMS-HA-FORTICLIENT"
              set type server-load-balance
              set server-type tcp
              set extip 192.168.1.190
              set extintf "CLIENTS"
              set monitor "tcp8013"
              set extport 8013
              config realservers
                  edit 1
                      set ip 192.168.1.177
                      set port 8013
                  next
                  edit 2
                      set ip 192.168.1.178
                      set port 8013
                  next
              end
          next
      end

      With the virtual server created, you can create your policy accordingly. Note that if you use the type TCP in your virtual server, the policy can be created using the flow-based inspection mode, which means hardware offloading. You only need proxy-based for non-TCP/UDP/IP types. Attach security profiles to the policy as necessary.

      FortiClient EMS HA Policy

      That’s everything you need for FortiClient to connect to EMS. Use the FQDN or an invitation code in the FortiClient GUI and hit Connect. Simple, right?

      The FortiGate to EMS connection

      This is the one that required some time to get working because the FortiGate needs to load-balance its own traffic, which I didn’t think was actually possible, but apparently it is.

      We, again, start with the virtual server, and we need TCP/443 here, but crucially, the health check must not be TCP/443. As mentioned above, the HTTPS service is active on both nodes, so this would create immediate issues depending on your load-balancing method and, in general, create issues.

      What service is only active on the primary node? The telemetry service, so we reuse the health check for TCP/8013, we already used for the FortiClient connection. The real servers still use TCP/443, however.

      I am binding the virtual server to the UNUSED VLAN because I only need to put the virtual server in a policy to activate it. This is done more so to show that this is possible. In reality, you would probably bind it to a management interface, so if you go to the virtual server IP, you always land on the active EMS node’s GUI.

      FortiGate EMS HA Virtual Server
      config firewall vip
          edit "EMS-HA-FORTIGATE"
              set type server-load-balance
              set server-type tcp
              set extip 192.168.1.190
              set extintf "LOOPBACK"
              set monitor "tcp8013"
              set extport 443
              config realservers
                  edit 1
                      set ip 192.168.1.177
                      set port 443
                  next
                  edit 2
                      set ip 192.168.1.178
                      set port 443
                  next
              end
          next
      end

      With this in mind, the policy we need is nothing special. The virtual server just needs to be active, and again, we can use a flow-based policy. Attach security profiles to the policy as necessary.

      FortiGate EMS HA Policy

      In the Fabric Connector for EMS, we use the FQDN, which resolves to the virtual server IP, and at least in my configuration, nothing happens because the traffic gets sourced incorrectly.

      FortiGate EMS connector

      To solve this, you go to the CLI and set a source IP for this connection. I am using the IP that the DMZ interface has, so that will be the one I see on EMS.

      Since we’re already in the CLI, we can use the command execute fctems verify <ID> (insert your ID) to start the verification, which should give us a certificate to accept.

      FortiGate EMS fabric CLI configuration

      70G-EMSHA # config endpoint-control fctems
      70G-EMSHA (fctems) # edit 1
      70G-EMSHA (1) # set source-ip 192.168.1.202
      70G-EMSHA (1) # end
      
      The configuration will not be effective unless server certificate is verified.
      You can get and verify server certificate by the following command:
      "execute fctems verify 1" (ems table id)
      
      70G-EMSHA # execute fctems verify 1
      
              Subject:     CN = ems-ha.ad.labdomain.com
              Issuer:      DC = com, DC = labdomain, DC = ad, CN = WIN-CA
              Valid from:  2026-06-18 08:10:35  GMT
              Valid to:    2028-06-17 08:10:35  GMT
              Fingerprint: 6D:6B:80:BC:10:9B:31:F7:15:7A:CC:03:71:01:98:9C:97:A0:4D:C5:21:12:C8:10:15:31:F3:43:E8:9B:2E:A7
              Root CA:     No
              Version:     3
              Serial Num:
                      32:00:00:00:44:88:6a:ed:ca:e6:cb:5b:39:00:02:00:00:00:44
              Extensions:
                      Name:     X509v3 Subject Key Identifier
                      Critical: no
                      Content:
                      BC:33:E8:D9:39:2F:29:36:21:E2:B7:88:9A:70:8D:79:63:F7:C6:55
      
                      Name:     X509v3 Key Usage
                      Critical: yes
                      Content:
                      Digital Signature, Key Encipherment
      
                      Name:     X509v3 Subject Alternative Name
                      Critical: no
                      Content:
                      DNS:ems-ha.ad.labdomain.com, DNS:ems1.ad.labdomain.com, DNS:ems2.ad.labdomain.com
      
                      Name:     X509v3 Authority Key Identifier
                      Critical: no
                      Content:
                      F3:CD:D6:6C:D4:C1:35:68:D7:EE:AA:07:7A:A8:5A:73:B8:48:6C:D5
      
                      Name:     X509v3 CRL Distribution Points
                      Critical: no
                      Content:
                      Full Name:
        URI:ldap:///CN=WIN-CA(2),CN=WIN-AD,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=ad,DC=labdomain,DC=com?certificateRevocationList?base?objectClass=cRLDistributionPoint
        URI:http://WIN-AD.ad.labdomain.com/CertEnroll/WIN-CA(2).crl
      
      
                      Name:     Authority Information Access
                      Critical: no
                      Content:
                      CA Issuers - URI:ldap:///CN=WIN-CA,CN=AIA,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=ad,DC=labdomain,DC=com?cACertificate?base?objectClass=certificationAuthority
      CA Issuers - URI:http://WIN-AD.ad.labdomain.com/CertEnroll/WIN-AD.ad.labdomain.com_WIN-CA(2).crt
      
                      Name:     Microsoft certificate template
                      Critical: no
                      Content:
                      0-.%+.....7.....n...z...%...w........G..f..d...
      
                      Name:     X509v3 Extended Key Usage
                      Critical: no
                      Content:
                      TLS Web Server Authentication
      
                      Name:     Microsoft Application Policies Extension
                      Critical: no
                      Content:
                      0.0
      ..+.......
      
      EMS configuration needs user to confirm server certificate.
      Do you wish to add the above certificate to trusted remote certificates? (y/n)y
      
      Certificate successfully configured and verified.

      Once we have entered the good old y, we can go to EMS and authorize the FortiGate that appeared.

      EMS fabric device

      Wrapping up

      And that’s that. You can do your failover tests (mine worked), and hopefully, dear reader, you can now connect your FortiClients and your FortiGate to your EMS HA deployment. Additional FortiGates should be much easier to handle; it’s just the first one that requires a bit of special attention.

      And if you don’t have enough of FortiClient EMS yet, you can read about how you can use its certificate management capabilities to help with VPN authentication, full SSL/TLS inspection and 802.1X.

    • FortiClient EMS certificate management for VPN, full SSL/TLS inspection, and 802.1X

      Certificates are ubiquitous in our daily life, whether we realize it or not, and most people fall into one of two camps when it comes to them:

      1. They love them because they make authentication and trust easy
      2. They hate them because they can seem like black magic to some and create unnecessary work due to having to provision and replace them.

      Both sides have valid points, but we can all agree that we need them, be it for securing web traffic, enabling strong authentication or to get rid of those pesky certificate warnings on our GUIs.

      FortiClient EMS has a few options to interact with certificates, so, dear reader, let’s have a look to see how EMS can help us with managing certificates.

      The setup

      • FortiGate 70G on 7.6.7
      • FortiClient EMS on 7.4.7
      • FortiAuthenticator on 8.0.3
      • Windows 11 client on 25H2 with FortiClient 7.4.6
      • FortiAP on 7.6.5
      • FortiSwitch 424E on 7.6.6

      Relevant configuration snippets can be found in the Fortinet resources GitHub repository.

      What am I even doing here?

      FortiClient EMS does two things out of the box when it comes to certificates:

      1. It acts as a certificate authority (CA), using a self-signed ZTNA CA to…
      2. Provision managed FortiClients with a user certificate for ZTNA purposes

      These two things open up quite a few possibilities because the user certificates aren’t anything special. They are regular certificates for client authentication, and since EMS is already distributing the certificates, we might as well put them to good use.

      We could download the default ZTNA public CA certificate and use that in our infrastructure to authenticate users, but this has some caveats, like:

      • It’s a self-signed CA that has no connection to anything, which can be seen as a problem by some people
      • We cannot issue certificates for other services, like webservers, using this CA, because the private key lies with EMS, and EMS only gives certificates to managed FortiClients
      • Corollary, this cannot scale because I only have this one CA, and I can’t create sub/intermediate CAs

      To solve this, I can create a real public key infrastructure (PKI) with a root CA, several sub CAs, including an EMS CA, hand out certificates for all purposes, and I got a nice chain of trust, and that is what I want to show in this post.

      Note: None of the use cases I cover in this post requires running your own PKI. You can do everything in some form using the default certificates and CAs you get with FortiClient EMS and a FortiGate, and without a root CA. I am doing this more to show that you can take it further.

      With the preamble out of the way, let’s begin with the foundation.

      The certificate structure

      To get started, I will be using FortiAuthenticator as a root CA to create two sub CAs:

      1. The EMS_SUBCA, which replaces the default EMS ZTNA CA and signs the certificates for managed FortiClients
      2. The FGT_SUBCA, which will be used by the FortiGate for SSL/TLS inspection

      Very important: I will use EMS_SUBCA in this post to refer to the sub CA that EMS uses, but the CN for this CA must be the serial number of your EMS server. This is a requirement for everything to function correctly.

      Additionally, I create one certificate with the server authentication extended key usage (EKU), so the FortiGate can later authenticate VPN users.

      Post PKI

      Note: I want to mention that the PKI topology I use for this post is not best practice. In a production environment, you should always start with a root CA that gets taken offline after issuing a sub CA, and that sub CA then issues all other certificates. To make it a bit easier on myself, I am only using an online root CA.

      Two-tier PKI

      How do we get to our certificates?

      FortiAuthenticator certificate management

      With FortiAuthenticator, we can do basically everything, so if we just head to Certificate Management -> Certificate Authorities -> Local CAs, we can create the root CA using the provided Root CA type, and after providing an ID and a CN, we got the first part done. You can, of course, change the settings for the CA as you wish.

      FortiAuthenticator root CA

      With the root CA in hand, we can create the EMS and FortiGate sub CAs from the same menu using the Intermediate CA type. Again, it’s very important that the Common Name is the serial number of your EMS server. I only show the EMS sub CA here.

      FortiAuthenticator EMS sub CA CSR

      After creating, make sure to export the key and cert for the two CAs and keep them safe for now. Also, download the public keys using the Export Certificate button from all three CAs.

      We need a certificate for the FortiGate later, and I will do that using the Simple Certificate Enrollment Protocol (SCEP) on FortiAuthenticator. Don’t forget to enable this service on the interface on which the FortiGate contacts FortiAuthenticator. If you use HTTPS, make sure that FortiAuthenticator’s certificate is trusted by the FortiGate.

      FortiAuthenticator SCEP interface

      Then head over to the SCEP Enrollment Requests menu and create a regular request for the FortiGate, signed by the root CA, with a fitting CN, and make sure that the Extended Key Usage of Server Authentication is present. All settings can, of course, be changed as you wish; I am just presenting a basic configuration.

      FortiAuthenticator SCEP enrollment request

      Note the password after creating the request, because we need it right now, because we get to…

      FortiGate certificate management

      Be sure to enable Certificates in Feature Visibility first.

      On the FortiGate, first import the FortiAuthenticator root CA and EMS sub CA certificates. You can do this either using SCEP or via a file upload.

      Then create a certificate signing request (CSR), making sure that the name and domain name match the CN you presented in the SCEP enrollment request (fgt-vpncert for me) and that Online SCEP is used as the Enrollment Method. The CA Server URL is in the format of http://FORTIAUTHENTICATOR-IP-FQDN/app/cert/scep, with HTTPS if you want, and your own IP or FQDN, of course.

      The Challenge Password is the one you got earlier.

      FortiGate VPN certificate CSR

      After a few seconds, the status of the CSR should get signed by FortiAuthenticator and return a Valid status.

      FortiGate VPN certificate

      While you’re here, also import the sub CA certificate for the FortiGate. Remember that this is considered a Certificate, not a CA certificate, when you click on the Create/Import button.

      That’s it for now, so let’s turn to EMS.

      EMS certificate management

      On EMS, we have to do two things:

      1. Change the ZTNA CA certificate to our created one
      2. Make sure clients get the FortiGate’s sub CA certificate

      Point one is done by going to System Settings -> EMS Settings and clicking on the cog icon next to EMS CA Certificate (ZTNA) and uploading the new CA certificate. EMS will automatically hand out certificates to managed FortiClients using this certificate.

      EMS ZTNA CA import

      For point two, we first need to upload the CA certificate under Endpoint Policy & Components -> CA Certificates.

      EMS CA import

      With the uploaded certificate, go to your System Settings profile of choice, make sure that under Other, the Install CA Certificate on Client setting is enabled and pick the CA certificates you want to push to managed FortiClients. Note that the certificate used as the EMS ZTNA CA gets installed without having to do anything.

      EMS CA System Settings

      After these two steps and waiting for a sync, we can see that a connected client now has a user certificate from the new EMS sub CA and trusts the CA certificates that are in the System Settings profile.

      Client user certificate
      Client trusted root CAs

      Note: If you have an on-net endpoint, make sure that whatever you configure in the ZTNA profile does not interfere with normal operations. Imagine that you have a ZTNA destination that uses the FQDN of an internal server. Due to the way ZTNA works, FortiClient will intercept the DNS request, return a bogus IP, and try to ZTNA this connection, which probably means the connection will fail. You probably want to have a disabled ZTNA profile attached to the endpoint’s policy when it is on-net.

      With all our certificates in order, we can actually use them, and why not begin with VPN?

      For VPN

      Note: Since EMS is provisioning user certificates, the certificate can only be used for regular VPN authentication with a logged-in user and pre-logon/start before logon (SBL) is not possible, since only machine certificates can be used in a pre-logon state.

      Starting with the FortiGate, we first need a PKI user that represents the EMS sub CA, so create this PKI user.

      FortiGate PKI user
      config user peer
          edit "EMS-SUBCA"
              set ca "EMS_SUBCA"
          next
      end

      The VPN configuration is relatively simple; just make sure that the previously created server authentication certificate is used and only accept client certificates from the EMS sub CA. All other settings, like IP assignment, encryption, DH, etc., are up to you.

      Don’t forget to create a policy using the VPN interface; otherwise, clients can’t connect.

      FortiGate certificate authentication VPN

      config vpn ipsec phase1-interface
          edit "CERT-VPN"
              set type dynamic
              set interface "port1"
              set ike-version 2
              set keylife 28800
              set authmethod signature
              set net-device disable
              set mode-cfg enable
              set ipv4-dns-server1 192.168.1.240
              set proposal aes256gcm-prfsha512
              set dhgrp 21
              set client-resume enable
              set client-resume-interval 300
              set transport auto
              set certificate "fgt-vpncert"
              set peer "EMS-SUBCA"
              set ipv4-start-ip 172.16.100.1
              set ipv4-end-ip 172.16.100.20
              set ipv4-split-include "BASE-RFC1918-192"
          next
      end
      config vpn ipsec phase2-interface
          edit "CERT-VPN"
              set phase1name "CERT-VPN"
              set proposal aes256-sha256
              set dhgrp 21
              set keepalive enable
              set keylifeseconds 3600
          next
      end
      config firewall policy
          edit 0
              set name "CERT-VPN"
              set srcintf "CERT-VPN"
              set dstintf "LAN"
              set action accept
              set srcaddr "all"
              set dstaddr "all"
              set schedule "always"
              set service "ALL"
              set logtraffic all
              set nat enable
          next
      end

      On EMS, we need a matching VPN configuration in the Remote Access profile. There are a few things to note:

      • Authentication Method is System Store Certificate
      • Disable EAP
      • Configure certificate matching

      Certificate matching is something, I think, not a lot of people are aware of, so here is a technical tip on the topic: Technical Tip: Filter client certificates in FortiClient

      The basic idea is that the certificate for the VPN connection is filtered, so only a specific one appears, which makes the user experience better because you don’t have to worry about which certificate gets used by a user. In my configuration, I am doing a regex match on the common name and a simple match on the issuer, which is enough to get what I need.

      <forticlient_configuration>
          <vpn>
              <ipsecvpn>
                  <connections>
                      <connection>
                          <ike_settings>
                              <auth_data>
                                  <certificate>
                                      <common_name>
                                          <match_type>regex</match_type>
                                          <pattern>.*</pattern>
                                      </common_name>
                                      <issuer>
                                          <match_type>simple</match_type>
                                          <pattern>EMS_SUBCA</pattern>
                                      </issuer>
                                      <issuer/>
                                      <oids/>
                                  </certificate>
                              </auth_data>
                          </ike_settings>
                      </connection>
                  </connections>
              </ipsecvpn>
          </vpn>
      </forticlient_configuration>

      The FortiClient XML reference has some more information on how to match and what options are available.

      I am configuring the session resumption feature for this tunnel, by the way.

      Both the FortiGate VPN configuration and the Remote Access XML profile EMS are in the GitHub repository linked above.

      A bit of advice when you first set this up: Create a personal VPN on a FortiClient, recreate the necessary VPN settings, and make sure that works first, just so you know that certificate authentication works. Once that is done, test the VPN configuration you get from EMS. Especially with the certificate filtering, it’s easier to do this bit by bit and not start with the EMS-provided VPN tunnel.

      With the FortiGate and EMS having the correct VPN configuration, we can test the connection, and it should work. We can see that the CN matches the FortiClient ID we see on EMS.

      FortiGate VPN success
      EMS endpoint information

      If we look at the debugs of ike and fnbamd, we can see the entire process (some parts of the debugs are removed):

      FortiGate VPN debugs

      diagnose debug application ike -1
      diagnose debug application fnbamd -1
      diagnose debug enable
      
      ike V=root:0: comes 192.168.100.2:51056->192.168.100.1:4500,ifindex=5,vrf=0,len=486....
      ike V=root:0: IKEv2 exchange=SA_INIT id=5a88db4c560f15da/0000000000000000 len=482
      ike 0: in 5A88DB4C560F15DA00000000000000002120220800000000000001E22200006C02000024010100030300000C01000014800E0100030000080400001500000008020000070000004402010007030000080300000C0300000C0100000C800E0100030000080400001503000008020000050300000802000006030000080200000700000008020000022800008C0015000000C99A8B88C2DDEE9963B6D3B52C40B57927668DC1C786D1981C35B1AD5638F56CC380D5280C6905FCF86C1FCBA01C8677249BA6C19290C86DAD24694C3B63F61D750102AB27393AB8D9D29E48D099E36A6BEF6BB06895B64DA96A2E67AF9D8481DCED49F36FA2C2B1FA09C117751D029699749DD359090740383B7EFFE6BCCFA47BE19A2B00002441A822057CFB67B96D2DBA82EE1C490EEA106F34B30490242EE4229FBABB92122B00001C4C53427B6D465D1B337BB755A37A7FEF706B9EBAF67F00002B00001CB4F01CA951E9DA8D0BAFBBD34AD3044E906B9EBAF67F00002900001CC1DC4350476B98A429B91781914CA43E202A8801FD010000290000080000F118290000080000402E2900001C0000400468F9B3E80FB82683054FF61FC4BFF1639CBAD7672900001C0000400540DAB0491BAC2CEB4C65D6506300387954912FAE0000000E0000402F000200030004
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: responder received SA_INIT msg
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID forticlient connect license 4C53427B6D465D1B337BB755A37A7FEF706B9EBAF67F0000
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID Fortinet Endpoint Control B4F01CA951E9DA8D0BAFBBD34AD3044E906B9EBAF67F0000
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID Forticlient EAP Extension C1DC4350476B98A429B91781914CA43E202A8801FD010000
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type CLIENT_RESUME
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type FRAGMENTATION_SUPPORTED
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type NAT_DETECTION_SOURCE_IP
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type NAT_DETECTION_DESTINATION_IP
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type SIGNATURE_HASH_ALGORITHMS
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: incoming proposal:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 1:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:   protocol = IKEv2:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:      encapsulation = IKEv2/none
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=ENCR, val=AES_GCM_16 (key_len = 256)
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA2_512
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=DH_GROUP, val=ECP521.
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 2:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:   protocol = IKEv2:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:      encapsulation = IKEv2/none
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=ENCR, val=AES_CBC (key_len = 256)
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=INTEGR, val=AUTH_HMAC_SHA2_256_128
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA2_512
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA2_384
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA2_256
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=DH_GROUP, val=ECP521.
      ike V=root:0: cache rebuild start
      ike V=root:0:CERT-VPN: cached as wildcard, user peer 'EMS-SUBCA'
      ike V=root:0:CERT-VPN: cached as dynamic, user peer 'EMS-SUBCA' subj='' cn=''
      ike V=root:0: cache rebuild done
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: matched proposal id 1
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 1:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:   protocol = IKEv2:
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:      encapsulation = IKEv2/none
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=ENCR, val=AES_GCM_16 (key_len = 256)
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=INTEGR, val=NONE
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=PRF, val=PRF_HMAC_SHA2_512
      ike V=root:0:5a88db4c560f15da/0000000000000000:23:         type=DH_GROUP, val=ECP521.
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: lifetime=28800
      ike V=root:0:5a88db4c560f15da/0000000000000000:23: SA proposal chosen, matched gateway CERT-VPN
      ike V=root:0:CERT-VPN:23: sending CERTREQ payload (len=21)
      ike V=root:0:CERT-VPN:23: certreq[0]: '85936D8D9D330144B1FBE59B30BE1EF36474F48F'
      ike V=root:0:CERT-VPN:23: received peer identifier DER_ASN1_DN 'C = CA, ST = BC, L = Burnaby, O = Fortinet, CN = F3AB0220E981419CAA9A13CE4811E3F9'
      ike V=root:0:CERT-VPN:23: match gw peer ID by FNBAM
      ike V=root:0:CERT-VPN:23: Validating X.509 certificate
      ike V=root:0:CERT-VPN:23: peer cert, subject='F3AB0220E981419CAA9A13CE4811E3F9', issuer='EMS_SUBCA'
      ike V=root:0:CERT-VPN:23: peer ID verified
      ike V=root:0:CERT-VPN:23: building fnbam peer candidate list
      ike V=root:0:CERT-VPN:23: FNBAM_GROUP_NAME candidate 'EMS-SUBCA'
      ike V=root:0:CERT-VPN:23: certificate validation pending
      [323] fnbamd_chain_build-Chain discovery, opt 0x13, cur total 1
      [341] fnbamd_chain_build-Following depth 0
      [376] fnbamd_chain_build-Extend chain by system trust store. (good: 'EMS-SUBCA')
      [341] fnbamd_chain_build-Following depth 1
      [376] fnbamd_chain_build-Extend chain by system trust store. (good: 'FAC_ROOT')
      [341] fnbamd_chain_build-Following depth 2
      [355] fnbamd_chain_build-Self-sign detected.
      [109] __cert_chg_st- 'Init' -> 'Validation'
      [1025] __cert_verify-req_id=40737833275404
      [1026] __cert_verify-Chain is complete.
      [540] fnbamd_cert_verify-Chain number:3
      [554] fnbamd_cert_verify-Following cert chain depth 0
      [627] fnbamd_cert_verify-Issuer found: EMS-SUBCA (SSL_DPI opt 1)
      [554] fnbamd_cert_verify-Following cert chain depth 1
      [627] fnbamd_cert_verify-Issuer found: FAC_ROOT (SSL_DPI opt 1)
      [554] fnbamd_cert_verify-Following cert chain depth 2
      [1057] __cert_verify-peer_info.no_ocsp_query:0 cert->status:640.
      [733] fnbamd_cert_check_group_list-checking group with name 'EMS-SUBCA'
      [546] __check_add_peer-check 'EMS-SUBCA'
      [422] peer_subject_cn_check-Cert subject 'C = CA, ST = BC, L = Burnaby, O = Fortinet, CN = F3AB0220E981419CAA9A13CE4811E3F9'
      [553] __check_add_peer-'EMS-SUBCA' check ret:good
      [668] __peer_user_clear_unmatched-Clear all user(s) other than 'EMS-SUBCA'
      [689] __peer_user_clear_unmatched-
      [202] __get_default_ocsp_ctx-def_ocsp_ctx=(nil), no_ocsp_query=0, ocsp_enabled=0
      [806] fnbamd_cert_check_group_list-Peer users
      [809] fnbamd_cert_check_group_list-    'EMS-SUBCA' ('N/A','N/A','N/A')
      [1069] __cert_verify_do_next-req_id=40737833275404
      [109] __cert_chg_st- 'Validation' -> 'Done'
      [1163] __cert_done-req_id=40737833275404
      [1567] fnbamd_auth_session_done-Session done, id=40737833275404
      [1209] __fnbamd_cert_auth_run-Exit, req_id=40737833275404
      [1610] create_auth_cert_session-fnbamd_cert_auth_init returns 0, id=40737833275404
      [1523] auth_cert_success-id=40737833275404
      [1321] fnbamd_cert_auth_copy_cert_status-req_id=40737833275404
      [1329] fnbamd_cert_auth_copy_cert_status-Matched peer user 'EMS-SUBCA'
      [914] fnbamd_cert_check_matched_groups-checking group with name 'EMS-SUBCA', peer_ctx->peer_user->setting.name:EMS-SUBCA
      [975] fnbamd_cert_check_matched_groups-matched
      [1361] fnbamd_cert_auth_copy_cert_status-Leaf cert status is unchecked.
      [1452] fnbamd_cert_auth_copy_cert_status-Cert st 2c0, req_id=40737833275404
      [279] fnbamd_comm_send_result-Sending result 0 (nid 672) for req 40737833275404, len=2611
      [1398] destroy_auth_cert_session-id=40737833275404
      [1293] fnbamd_cert_auth_uninit-req_id=40737833275404
      ike V=root:0:CERT-VPN:23: fnbam reply 'EMS-SUBCA'
      ike V=root:0:CERT-VPN:23: fnbam matched peer 'EMS-SUBCA'
      [1985] fnbamd_ldaps_destroy-
      ike V=root:0:CERT-VPN:23: certificate validation succeeded
      [1667] fnbamd_rads_destroy-
      [140] fnbamd_peer_ctx_free-Freeing peer ctx 'EMS-SUBCA'
      ike V=root:0:CERT-VPN:23: signature verification succeeded
      ike V=root:0:CERT-VPN:23: auth verify done
      ike V=root:0:CERT-VPN:23: responder AUTH continuation
      ike V=root:0:CERT-VPN:23: authentication succeeded
      ike V=root:0:CERT-VPN:23: processing notify type FORTICLIENT_CONNECT
      ike V=root:0:CERT-VPN:23: received FCT data len = 326, data = 'VER=1
      FCTVER=7.4.6.2001
      UID=F3AB0220E981419CAA9A13CE4811E3F9
      IP=192.168.100.2
      MAC=a4-bb-6d-13-07-2b;24-41-8c-fb-b1-b6;24-41-8c-fb-b1-b7;26-41-8c-fb-b1-b6;
      HOST=FCXLAB
      USER=labuser
      OSVER=Microsoft Windows 8.0 Professional Edition, 64-bit (build 9200)
      REG_STATUS=0
      EMSSN=FCTEMSSERIAL
      EMSID=00000000000000000000000000000000
      '
      ike V=root:0:CERT-VPN:23: received FCT-UID : F3AB0220E981419CAA9A13CE4811E3F9
      ike V=root:0:CERT-VPN:23: received EMS SN : FCTEMSSERIAL
      ike V=root:0:CERT-VPN:23: received EMS tenant ID : 00000000000000000000000000000000
      ike V=root:0:CERT-VPN:23: received FCT-HOST : FCXLAB
      ike V=root:0:CERT-VPN:23: responder creating new child
      ike V=root:0:CERT-VPN:23: mode-cfg type 1 request 0:''
      ike V=root:0:CERT-VPN: mode-cfg allocate 172.16.100.1/0.0.0.0
      ike V=root:0:CERT-VPN:23: mode-cfg using allocated IPv4 172.16.100.1
      ike V=root:0:CERT-VPN:23:8: peer proposal:
      ike V=root:0:CERT-VPN:23:8: TSi_0 0:0.0.0.0-255.255.255.255:0
      ike V=root:0:CERT-VPN:23:8: TSr_0 0:0.0.0.0-255.255.255.255:0
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: comparing selectors
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: matched by rfc-rule-2
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: phase2 matched by subset
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: using mode-cfg override 0:172.16.100.1-172.16.100.1:0
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: accepted proposal:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: TSi_0 0:172.16.100.1-172.16.100.1:0
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: TSr_0 0:0.0.0.0-255.255.255.255:0
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: dialup
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: incoming child SA proposal:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 1:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:   protocol = ESP:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:      encapsulation = TUNNEL
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ENCR, val=AES_GCM_16 (key_len = 128)
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ESN, val=NO
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         PFS is disabled
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 2:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:   protocol = ESP:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:      encapsulation = TUNNEL
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ENCR, val=AES_CBC (key_len = 256)
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=INTEGR, val=SHA256
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ESN, val=NO
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         PFS is disabled
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: matched proposal id 2
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 2:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:   protocol = ESP:
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:      encapsulation = TUNNEL
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ENCR, val=AES_CBC (key_len = 256)
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=INTEGR, val=SHA256
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         type=ESN, val=NO
      ike V=root:0:CERT-VPN:23:CERT-VPN:8:         PFS is disabled
      ike V=root:0:CERT-VPN:23:CERT-VPN:8: lifetime=3600
      ike V=root:0:CERT-VPN:23: responder preparing AUTH msg
      ike V=root:0:CERT-VPN: adding new dynamic tunnel for 192.168.100.2:51056
      ike V=root:0:CERT-VPN_0: tunnel created tun_id 172.16.100.1/::10.0.0.10 remote_location 0.0.0.0
      ike V=root:0:CERT-VPN_0: added new dynamic tunnel for 192.168.100.2:51056
      ike V=root:0:CERT-VPN_0:23: local cert, subject='fgt-vpncert', issuer='FAC-ROOT'
      ike V=root:0:CERT-VPN_0:23: mode-cfg assigned (1) IPv4 address 172.16.100.1
      ike V=root:0:CERT-VPN_0:23: mode-cfg assigned (2) IPv4 netmask 255.255.255.255
      ike V=root:0:CERT-VPN_0:23: mode-cfg send (13) 0:192.168.0.0/255.255.0.0:0
      ike V=root:0:CERT-VPN_0:23: mode-cfg send (3) IPv4 DNS(1) 192.168.1.240
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: IPsec SA selectors #src=1 #dst=1
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: src 0 7 0:0.0.0.0-255.255.255.255:0
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: dst 0 7 0:172.16.100.1-172.16.100.1:0
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: add dynamic IPsec SA selectors 683
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: added dynamic IPsec SA proxyids new 1 683
      ike V=root:0:CERT-VPN:8: add route 172.16.100.1/255.255.255.255 gw 172.16.100.1 oif CERT-VPN(36) metric 15 priority 1
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: tunnel 1 of VDOM limit 0/0
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: add IPsec SA: SPIs=78e2bc59/8dc65e44
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: added IPsec SA: SPIs=78e2bc59/8dc65e44
      ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: sending SNMP tunnel UP trap
      ike V=root:0:CERT-VPN_0: tunnel up event assigned address 172.16.100.1
      ike V=root:0:CERT-VPN_0: sent tunnel-up message to EMS: (fct-uid=F3AB0220E981419CAA9A13CE4811E3F9, intf=CERT-VPN_0, addr=172.16.100.1, vdom=root)
      
      1. We get the incoming connection with some FortiClient information as well as the IKE phase 1 proposal, so a gateway can be matched
      2. FortiClient sends the user certificate during the connection attempt (received peer identifier) and the peer cert, the EMS sub CA, gets taken from the configuration and passed to the fnbamd process for certificate validation.
      3. The certificate chain gets walked to find the possible issuers (Following cert chain)
      4. The EMS sub CA certificate provides a match (__check_add_peer-'EMS-SUBCA' check ret:good)
      5. The resulting CA gets returned to the ike process (fnbam reply 'EMS-SUBCA) and certificate validation is successful (certificate validation succeeded)
      6. The FortiClient data gets passed along, which could be used to restrict connections to only fabric-connected EMS servers by way of the EMS serial number, but this isn’t done here
      7. Regular IPsec phase 2 processes are done and we get a sending SNMP tunnel UP trap message, which we love to see

      VPN is looking good, so next up is SSL/TLS inspection.

      For SSL/TLS inspection

      I will be referring to SSL/TLS inspection as Deep Packet Inspection (DPI) going forward.

      When doing DPI, the client needs to trust the CA that is issuing the replaced certificate, and since we already did that in the EMS certificate management section, we only need to create an SSL/SSH inspection profile and configure firewall policies to use it.

      FortiGate DPI profile
      FortiGate DPI policy

      Remember that you need at least one other security profile in a policy for the SSL/SSH inspection profile to do anything, which is why I am using an application control and IPS profile. Without an additional security profile, it’s like no security profile is being used.

      If a client then, for example, browses to a website secured by HTTPS, we see that DPI does its thing, meaning the FortiGate replaces the certificate, and the connection is also trusted.

      Client DPI success

      DPI done, so on to 802.1X.

      A detour to configure FortiAuthenticator for EAP-TLS authentication with a FortiGate and FortiSwitch

      Initially, I didn’t want to explain how to configure FortiAuthenticator as a RADIUS server for EAP-TLS authentication and point to the documentation, but I couldn’t find an up-to-date example of it, so let’s quickly go through it.

      1. Make sure that at least RADIUS Auth is enabled on the FortiAuthenticator’s interface
      2. If it doesn’t already exist, create a Local Services certificate for EAP authentication in Certificate Management -> End Entities -> Local Services
        • This certificate may not have multiple SANs, but one is fine
      3. Assign the certificate as the EAP Server Certificate under Authentication -> RADIUS Service -> General
      4. Create a RADIUS client for the device that will send RADIUS requests
        • For this post, it is only the FortiGate’s IP since I NAT the FortiSwitch communication towards FortiAuthenticator
        • You don’t need it for 802.1X authentication, but you might as well enable the requirement for the Message-Authenticator attribute and also do it on the Network Access Server (NAS). For MAC Authentication Bypass (MAB), you need it anyway, and the attribute doesn’t hurt
      5. Create an Auth Profile with the Authentication type of Certificates and the EMS_SUBCA as a Trusted CA
      6. Create a RADIUS policy with your RADIUS client, the Client Credentials as EAP-TLS, and pick your Authentication Profile

      The configuration on the FortiGate is the same as always:

      1. Create a RADIUS server
      2. Create a group with that RADIUS server as a remote member
      3. Create a FortiSwitch Security Policy for 802.1X
        • You probably want MAC-based authentication so each client gets authenticated, not just the first one, like with port-based authentication
      4. Assign the security policy to a port
        • Don’t forget to enable the Security Policy column
      5. Create a firewall policy from your FortiLink interface to the RADIUS server that allows the RADIUS traffic
        • The FortiLink interface can only be assigned via the CLI
        • Optionally enable NAT on this policy if you want that, which I do

      The 802.1X configuration the FortiGate uses is in the GitHub repository linked above.

      With that completed, we can go back to the topic at hand.

      Actually…

      [Update 2026-06-07]: I have asked around a bit and got the feedback that the user certificate should not be removed if the client goes offline, which is also the experience I’ve had in the past. I have checked the release notes for EMS and FortiClient and couldn’t find anything that would hint at a change in behaviour or a bug, and I also updated FortiClient from 7.4.6 to 7.4.7, but this didn’t change anything. The user certificate continues to get removed shortly after I take the client offline. I will follow this topic, and hopefully I get to the bottom of it, because making 802.1X authentication possible in an easy manner would be great. In the meantime, you can test this behaviour yourself, and maybe you have a different experience. I can’t rule any configuration issues out on my side.

      [Update 2026-06-08]: I have found the root cause of the behaviour where FortiClient deletes the user certificate. The reason is that when writing this post, I used the CN EMS_SUBCA for the EMS sub CA. FortiClient’s FortiESNAC process, however, has a certificate check that runs in some situations, like when the client goes offline, and this check inspects the issuing CA for the user certificate and if the CN does not match the serial number of EMS, the user certificate gets deleted. Practically, this means that if you create your custom CA, the CN needs to match the EMS serial number. I have put this information further up, so people start with the correct information.

      We can see this behaviour in the FortiESNAC_1.log file (lots of lines in between are removed). I am presenting a case here where the serial number is not present in the CN.

      FortiESNAC debugs

      
      [2026-06-08 11:02:31.6791578 UTC+02:00] [964:968] [FortiESNAC  894   error] Could not find the desired certificate FCTEMSSERIAL.
      [2026-06-08 11:02:31.6791759 UTC+02:00] [964:968] [FortiESNAC  367    info] failed delete certificate in session 1
      [2026-06-08 11:02:31.6810172 UTC+02:00] [964:968] [FortiESNAC  717   error] Could not find the desired CA certificate.
      [2026-06-08 11:02:31.6816508 UTC+02:00] [964:968] [FortiESNAC  416   debug] result(1)
      [2026-06-08 11:02:31.6816530 UTC+02:00] [964:968] [FortiESNAC  418   debug] dwCertSize(1464), dwPrivateKeySize(1188), dwCACertSize(1134), dwHigh(0), dwActualRead(1134), dwDerCertLen(782), pkiLen(1172)
      [2026-06-08 11:02:31.6816538 UTC+02:00] [964:968] [FortiESNAC  422   debug] hCert(0), hPrivateKey(0), hCACert(0)
      [2026-06-08 11:02:31.6816542 UTC+02:00] [964:968] [FortiESNAC  424   debug] PrivateKey(0), pPem(0), pCAPem(0), pKeyPem(0)
      [2026-06-08 11:02:31.6870321 UTC+02:00] [964:968] [FortiESNAC  198    info] succeeded importing certificate in session 1
      [2026-06-08 11:02:31.6928331 UTC+02:00] [964:968] [FortiESNAC  958   error] Could not find the desired certificate 6E23E7C3ABBF4FE797BB811A42F0A6F3.
      [2026-06-08 11:02:31.6928521 UTC+02:00] [964:968] [FortiESNAC  96   debug] epctrl::ztna::SaveClientCertInfo 96 => GetClientCertificate failed
      [2026-06-08 11:02:31.6928533 UTC+02:00] [964:968] [FortiESNAC  1022   error] Failed to output client certificate info
      ...
      [2026-06-08 11:02:47.2217139 UTC+02:00] [964:968] [FortiESNAC  226    info] remove unmatched certificates
      [2026-06-08 11:02:47.2241330 UTC+02:00] [964:968] [FortiESNAC  824    info] delete unmatched cert, subject: 6E23E7C3ABBF4FE797BB811A42F0A6F3, issuer: EMS_SUBCA
      [2026-06-08 11:02:47.2265709 UTC+02:00] [964:968] [FortiESNAC  958   error] Could not find the desired certificate 6E23E7C3ABBF4FE797BB811A42F0A6F3.
      [2026-06-08 11:02:47.2265925 UTC+02:00] [964:968] [FortiESNAC  231   debug] ShouldRequestCertificate return : 1
      ...
      [2026-06-08 11:02:49.4696296 UTC+02:00] [964:968] [FortiESNAC  65 warning] Requesting new ZTNA client certificate: client certificate cannot be found on the system
      [2026-06-08 11:02:49.4875478 UTC+02:00] [964:968] [FortiESNAC  497   debug] Request=MSG_HEADER: FCTUID=6E23E7C3ABBF4FE797BB811A42F0A6F3
      FCTVER=7.4.7.2003
      EMSSN=FCTEMSSERIAL
      PROTO_VER=1.0.1
      FCT_ONNET=1
      CAPS=16777215
      VDOM=default
      EC_QUARANTINED=0
      TOKEN=49F4***7042
      SIZE=    1898
      
      X-FCCK-KA: SYSINFO|...|
      X-FCCK-KA-END
      
      
      [2026-06-08 11:02:49.4875537 UTC+02:00] [964:968] [FortiESNAC  497   debug] >>>>> BEGIN CERT_REQ
      [2026-06-08 11:02:49.4875541 UTC+02:00] [964:968] [FortiESNAC  497   debug] -----BEGIN CERTIFICATE REQUEST-----
      [2026-06-08 11:02:49.4875545 UTC+02:00] [964:968] [FortiESNAC  497   debug] MIICrzCCAZcCAQAwajELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAkJDMRAwDgYDVQQH
      [2026-06-08 11:02:49.4875547 UTC+02:00] [964:968] [FortiESNAC  497   debug] DAdCdXJuYWJ5MREwDwYDVQQKDAhGb3J0aW5ldDEpMCcGA1UEAwwgNkUyM0U3QzNB
      [2026-06-08 11:02:49.4875550 UTC+02:00] [964:968] [FortiESNAC  497   debug] QkJGNEZFNzk3QkI4MTFBNDJGMEE2RjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
      [2026-06-08 11:02:49.4875552 UTC+02:00] [964:968] [FortiESNAC  497   debug] ggEKAoIBAQCfB/yhfEoygLa3aWPW68O/J3vEWdNkO7tpK/F1coAOe8bvI4nXni/0
      [2026-06-08 11:02:49.4875554 UTC+02:00] [964:968] [FortiESNAC  497   debug] djdMyeeEEz77aOeJXlZrKnpH4h+5RDOAjV+mFBjT2RqbI97kydMkXG1n/AmCUaXs
      [2026-06-08 11:02:49.4875556 UTC+02:00] [964:968] [FortiESNAC  497   debug] +9iFvsB+cEVKdqf9C/179om02s6zZPCZRi46OZA5bkCTX/FAenjAKRz8NCdB0BGj
      [2026-06-08 11:02:49.4875558 UTC+02:00] [964:968] [FortiESNAC  497   debug] WoACSSRN9f4z3Ovzgtg7k8V5UpxgUJgYO4h8/6OUtVwINt1YKMxV9BPLO+7g1ZLr
      [2026-06-08 11:02:49.4875560 UTC+02:00] [964:968] [FortiESNAC  497   debug] a0Gm7oP8UKLQbml/1XvOQUlI3fVFtZ2MrnbQNqDqvRE7qjXl6lk97kzs/5KvOGXF
      [2026-06-08 11:02:49.4875562 UTC+02:00] [964:968] [FortiESNAC  497   debug] 7wqxqqiNmzIOmhBcmT8t8oUe0ZvuGeYJAgMBAAGgADANBgkqhkiG9w0BAQsFAAOC
      [2026-06-08 11:02:49.4875564 UTC+02:00] [964:968] [FortiESNAC  497   debug] AQEAWyjcMzwWHL1jahVLRAcoE4ed7nAnrOVqZN8MrK6kHEOarOHyt7+gzDoFcilP
      [2026-06-08 11:02:49.4875566 UTC+02:00] [964:968] [FortiESNAC  497   debug] W+niR9Y3nmjVPDeYiv0oskwmtf8i/DfhWenh4CnvjoCBMcCogMiwR2EhUflq3ZQv
      [2026-06-08 11:02:49.4875568 UTC+02:00] [964:968] [FortiESNAC  497   debug] 6tLy+p2IUpgd5HVIP/FkDHWaxYpQGp6qGHUIdyDxxY8U+WhmWUWTtWjC6oRRWZN0
      [2026-06-08 11:02:49.4875570 UTC+02:00] [964:968] [FortiESNAC  497   debug] SJRtFH8zzOb/XcavxH1vEOW4+69kQOMujAamznUxw9xkKkFtPXuFCymDOZzYiFUq
      [2026-06-08 11:02:49.4875574 UTC+02:00] [964:968] [FortiESNAC  497   debug] k7mhdd+U6yf18RADdH6UOD+mJeHAF0C0u9YSZX/N2k5ityPBXRTlDLY8AnXhvDVg
      [2026-06-08 11:02:49.4875576 UTC+02:00] [964:968] [FortiESNAC  497   debug] potg5Ubn7wFLI2ainkBmEbDNpQ==
      [2026-06-08 11:02:49.4875578 UTC+02:00] [964:968] [FortiESNAC  497   debug] -----END CERTIFICATE REQUEST-----
      [2026-06-08 11:02:49.4875581 UTC+02:00] [964:968] [FortiESNAC  497   debug] <<<<< END   CERT_REQ
      [2026-06-08 11:02:49.4875594 UTC+02:00] [964:968] [FortiESNAC  1060    info] Attempting to sync with EMS
      [2026-06-08 11:02:49.4875612 UTC+02:00] [964:968] [FortiESNAC  111    info] Sending and Receiving from ems.ad.labdomain.com:8013
      [2026-06-08 11:02:49.4876878 UTC+02:00] [964:968] [FortiESNAC  137   error] Failed to send data(10054): An existing connection was forcibly closed by the remote host.
      [2026-06-08 11:02:49.4877791 UTC+02:00] [964:968] [FortiESNAC  1075   error] Could not contact the current server - backing up server address and trying other available servers

      The following process happens:

      1. FortiESNAC tries to find a certificate that has been issued by a CA with the EMS serial number as the CN, and if it doesn’t match, we see that the GetClientCertificate function fails:
        1. Could not find the desired certificate FCTEMSSERIAL
        2. Could not find the desired CA certificate
        3. epctrl::ztna::SaveClientCertInfo 96 => GetClientCertificate failed
      2. FortiESNAC removes the unmatched certificate, which is the user certificate that was already deployed
      3. A new ZTNA client certificate gets requested (Requesting new ZTNA client certificate: client certificate cannot be found on the system)
      4. EMS cannot be contacted for this, because the client is offline (Could not contact the current server - backing up server address and trying other available servers)

      With this information in mind, you can ignore the rest of this section, which I have marked for you.

      IGNORE BLOCK STARTS HERE

      I am so sorry for the detours, but it would be remiss of me not to mention the next part.

      One thing rears its ugly head when it comes to 802.1X and using the EMS user certificates, which is the fact that FortiClient will delete the user certificate if it loses the EMS connection. I was able to narrow it down to the Online/Offline status displayed on FortiClient, and in my testing, this status change took about 10 seconds.

      FortiClient Online/Offline

      This behaviour leads to the problem that user authentication for 802.1X cannot really work because, by definition, you don’t have network access before authentication and you can’t get the Online status.

      As a workaround, I (ab)used the guest VLAN function in the security policy. The idea is that a client fails, or doesn’t even attempt, the 802.1X authentication, falls back to MAB, which also fails, assuming you haven’t configured MAB, and arrives in the guest VLAN after some time. This VLAN allows a connection to EMS to get a certificate, and the client can complete the 802.1X authentication with this certificate.

      You can, of course, configure MAB as a fallback.

      You have to configure the following things for this:

      • Create a guest VLAN with all the necessary components, i.e. DHCP, DNS, etc.
      • Enable the Guest VLAN option in the FortiSwitch Security Policy
      • Set the Guest authentication delay to something reasonable
      • Enable MAC authentication bypass
      • Create a firewall policy allowing the necessary traffic
        • At a minimum, guests need to reach EMS over TCP/8013
        • Keep DNS in mind

      Once the client gets the certificate, he can, at least on Windows, easily sign in using the provided pop-up.

      This works for wired 802.1X. Wireless is another beast, and I got nothing here.

      Sorry, not everything is a winner. Maybe someone can help me in this regard.

      IGNORE BLOCK ENDS HERE

      For 802.1X with EAP-TLS

      Note: Much like with the VPN part, you cannot use the user certificate in a pre-logon scenario. You need a machine/computer certificate for that.

      802.1X with EAP-TLS really is the thing you want to secure your network access in the physical space, and thanks to the work we already did, all the pieces exist to make it a reality.

      FortiAuthenticator acts as the RADIUS/AAA server, the client has a user certificate and the CA certificates installed, and so we just need a layer 2 device to act as the NAS, and for me, this role is fulfilled by a FortiSwitch and a FortiAP.

      How to configure 802.1X with EAP-TLS was already handled, so here are just some additional points:

      • In the RADIUS policy on FortiAuthenticator, only certificates issued by the EMS_SUBCA are accepted, because those are the certificates the client presents
      • If your client is validating the RADIUS server certificate, you have to make sure that it uses the correct CA certificate for this. In my case, the server certificate comes from the FAC_ROOT CA
      • The two points already mentioned are important if you are using Windows group policies for this, because you want/need to restrict both the server validation CA, as well as the issuing CA, for simple certificate selection

      With a correctly configured client, we can check the 802.1X authentication state. First, wired, using the diagnose switch-controller switch-info 802.1X S424ENTFSERIAL portX command:

      diagnose switch-controller switch-info 802.1X S424ENTFSERIAL port2
      
      Managed Switch : S424ENTFSERIAL
      
         port2 : Mode: port-based (mac-by-pass disable)
                 Link: Link up
                 Port State: authorized: (  )
                 Dynamic Authorized Vlan : 0
                 Dynamic Allowed Vlan list:
                 Dynamic Untagged Vlan list:
                 EAP pass-through : Enable
                 EAP egress-frame-tagged : Enable
                 EAP auto-untagged-vlans : Enable
                 Allow MAC Move : Enable
                 Dynamic Access Control List : Disable
                 Quarantine VLAN (4093) detection : Enable
                 Native Vlan : 11
                 Allowed Vlan list: 11,4093
                 Untagged Vlan list: 4093
                 Guest VLAN :
                 Auth-Fail Vlan :
                 AuthServer-Timeout Vlan :
      
                 Sessions info:
                 00:e0:4c:68:00:14     Type=802.1x,TLS,state=AUTHENTICATED,etime=2,eap_cnt=8 params:reAuth=3600
                 user="F3AB0220E981419CAA9A13CE4811E3F9",security_grp="FAC-RADIUS",radsec="disable":tunnel_id="-1", fortinet_grp=""

      We care about the state of AUTHENTICATED, and we can see that the user corresponds to the FortiClient ID, which we saw already in the VPN section.

      The FortiAuthenticator RADIUS debug of this is a standard and boring affair, but here it is:

      FortiAuthenticator wired 802.1X debugs

      2026-06-04T22:10:49.106876+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:10:49.107070+02:00 FortiAuthenticator radiusd[1617]: (58) Received Access-Request Id 9 from 192.168.1.202:40715 to 192.168.1.206:1812 length 192
      2026-06-04T22:10:49.107093+02:00 FortiAuthenticator radiusd[1617]: (58)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.107103+02:00 FortiAuthenticator radiusd[1617]: (58)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.107112+02:00 FortiAuthenticator radiusd[1617]: (58)   EAP-Message = 0x02ae0025014633414230323230453938313431394341413941313343453438313145334639
      2026-06-04T22:10:49.107123+02:00 FortiAuthenticator radiusd[1617]: (58)   Framed-MTU = 1500
      2026-06-04T22:10:49.107131+02:00 FortiAuthenticator radiusd[1617]: (58)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.107141+02:00 FortiAuthenticator radiusd[1617]: (58)   NAS-Port = 2
      2026-06-04T22:10:49.107154+02:00 FortiAuthenticator radiusd[1617]: (58)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.107162+02:00 FortiAuthenticator radiusd[1617]: (58)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.107171+02:00 FortiAuthenticator radiusd[1617]: (58)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.107179+02:00 FortiAuthenticator radiusd[1617]: (58)   Message-Authenticator = 0x3a4a55af4f6cd773918c326b7bf9fea0
      2026-06-04T22:10:49.107212+02:00 FortiAuthenticator radiusd[1617]: (58) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.107249+02:00 FortiAuthenticator radiusd[1617]: (58) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.107259+02:00 FortiAuthenticator radiusd[1617]: (58) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.107729+02:00 FortiAuthenticator radiusd[1617]: Not doing PAP as Auth-Type is already set.
      2026-06-04T22:10:49.107772+02:00 FortiAuthenticator radiusd[1617]: (58) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.107794+02:00 FortiAuthenticator radiusd[1617]: (58) eap: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.107807+02:00 FortiAuthenticator radiusd[1617]: (58) eap: No policy matched for client:3 
      2026-06-04T22:10:49.107816+02:00 FortiAuthenticator radiusd[1617]: (58) eap: Unable to find matching authpolicy (eap-teap) for RADIUS client with IP 192.168.1.202. 
      2026-06-04T22:10:49.107825+02:00 FortiAuthenticator radiusd[1617]: (58) eap: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.107834+02:00 FortiAuthenticator radiusd[1617]: found 1 eap methods from authprofile 1 
      2026-06-04T22:10:49.107843+02:00 FortiAuthenticator radiusd[1617]: (58) eap: No policy matched for client:3 
      2026-06-04T22:10:49.107852+02:00 FortiAuthenticator radiusd[1617]: (58) eap: Unable to find matching authpolicy (password) for RADIUS client with IP 192.168.1.202. 
      2026-06-04T22:10:49.107861+02:00 FortiAuthenticator radiusd[1617]: (58) eap: Found &control:EAP-Type = TLS
      2026-06-04T22:10:49.108091+02:00 FortiAuthenticator radiusd[1617]: (58) eap: EAP session adding &reply:State = 0x2eb73cc92e183192
      2026-06-04T22:10:49.108114+02:00 FortiAuthenticator radiusd[1617]: (58) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.108125+02:00 FortiAuthenticator radiusd[1617]: (58)   Framed-MTU = 984
      2026-06-04T22:10:49.108423+02:00 FortiAuthenticator radiusd[1617]: (58) Sent Access-Challenge Id 9 from 192.168.1.206:1812 to 192.168.1.202:40715 length 64
      2026-06-04T22:10:49.108448+02:00 FortiAuthenticator radiusd[1617]: (58)   EAP-Message = 0x01af00060d20
      2026-06-04T22:10:49.108458+02:00 FortiAuthenticator radiusd[1617]: (58)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.108467+02:00 FortiAuthenticator radiusd[1617]: (58)   State = 0x2eb73cc92e18319234453ea63f84b1d2
      2026-06-04T22:10:49.122656+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:10:49.122796+02:00 FortiAuthenticator radiusd[1617]: (59) Received Access-Request Id 10 from 192.168.1.202:51786 to 192.168.1.206:1812 length 612
      2026-06-04T22:10:49.122812+02:00 FortiAuthenticator radiusd[1617]: (59)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.122822+02:00 FortiAuthenticator radiusd[1617]: (59)   State = 0x2eb73cc92e18319234453ea63f84b1d2
      2026-06-04T22:10:49.122830+02:00 FortiAuthenticator radiusd[1617]: (59)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.122842+02:00 FortiAuthenticator radiusd[1617]: (59)   EAP-Message = 0x02af01b50d80000001ab16030101a6010001a2030319c54d1690d23565d35135d739f63ebb8906ed0ae56265a88e5100bf414c71c2209abee7eff81b5b8e20c2d576a2c3580e1f8c80be7c85bc5dca9cbcf3d1616a81002813021301c02cc02bc030c02fc024c023c028c027c00ac009c014c013009d009c003d003c0035002f01000131000500050100000000002b0009080304030303020301000d001a001808040805080604010501020104030503020302020601060300230000000a00080006001d00170018000b00020100003300d000ce001d002069d0e993cf36ad15bf7d2d3869b9713f96aefaa67fcf7076092cf7386449863f0017004104b77bbd396ae12db22b35a29ac0d358c6cdae3f02ea42ac7a03e49c280142a6a114211e5f61efc448cf4a00444209d5bae83b3e0a984694a0341b879cd1d551710018006104495b00fcee8104bca8ecf4e69cb4b62c9712d6bed06563262169412615d3c96837fb8a78c06407e0100736bda70a31abb37afbdfa1
      2026-06-04T22:10:49.122868+02:00 FortiAuthenticator radiusd[1617]: (59)   Framed-MTU = 1500
      2026-06-04T22:10:49.122877+02:00 FortiAuthenticator radiusd[1617]: (59)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.122886+02:00 FortiAuthenticator radiusd[1617]: (59)   NAS-Port = 2
      2026-06-04T22:10:49.122895+02:00 FortiAuthenticator radiusd[1617]: (59)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.122897+02:00 FortiAuthenticator radiusd[1617]: (59)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.122898+02:00 FortiAuthenticator radiusd[1617]: (59)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.122901+02:00 FortiAuthenticator radiusd[1617]: (59)   Message-Authenticator = 0xd9f2a52e4b2a46384e53481bdef26b51
      2026-06-04T22:10:49.122906+02:00 FortiAuthenticator radiusd[1617]: (59) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.122938+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.122942+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.122945+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: ===>Timestamp:1780603849.122568, age:0ms 
      2026-06-04T22:10:49.122960+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.122965+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.122974+02:00 FortiAuthenticator radiusd[1617]: (59) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.123048+02:00 FortiAuthenticator radiusd[1617]: (59) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.123060+02:00 FortiAuthenticator radiusd[1617]: (59) eap: Removing EAP session with state 0x2eb73cc92e183192
      2026-06-04T22:10:49.123069+02:00 FortiAuthenticator radiusd[1617]: (59) eap: Previous EAP request found for state 0x2eb73cc92e183192, released from the list
      2026-06-04T22:10:49.124046+02:00 FortiAuthenticator radiusd[1617]: (59) eap: EAP session adding &reply:State = 0x2eb73cc92f073192
      2026-06-04T22:10:49.124146+02:00 FortiAuthenticator radiusd[1617]: (59) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.124160+02:00 FortiAuthenticator radiusd[1617]: (59)   Framed-MTU = 984
      2026-06-04T22:10:49.124170+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:10:49.124173+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:10:49.124175+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.124177+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:10:49.124179+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:10:49.124181+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.124183+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.124185+02:00 FortiAuthenticator radiusd[1617]: (59)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.124200+02:00 FortiAuthenticator radiusd[1617]: (59) Sent Access-Challenge Id 10 from 192.168.1.206:1812 to 192.168.1.202:51786 length 1054
      2026-06-04T22:10:49.124204+02:00 FortiAuthenticator radiusd[1617]: (59)   EAP-Message = 0x01b003de0dc0000008cf160303009b0200009703032da7f1558fd723795a1a5c7a693a1db5b87b23534cc16a7bb3646e589010f984209abee7eff81b5b8e20c2d576a2c3580e1f8c80be7c85bc5dca9cbcf3d1616a81130200004f002b000203040033004500170041048da741a225ddaeabb51543bec7740f93bb4c69d03a5f4d1839034acd4ba0ac593581e149732c44950b5860e91099e06d5d2274239c3d5a63c8dfa9c6962aa6701403030001011703030023e8c102dd5346e1cfa119ce3af5c41cdda88a227726bdf8c6a9d1b89a3ce71874357d2a170303004612ec61490593ec5d62656164553e3ffd88996728d83bf680e0715924a23ff2bd217edd2c6ad78e7ebb48fb0c145ad395c90ad90cb4aa6de1e72988bdaf3d1e7511891dca77db170303064926fd85a9fc225815871d068d3f8f0efce31df7e81d33656d9370e209b9777b1d290f98c28310f87c884039299bdedec49e034190d197e36b7519fd0ed6b38f45e32f23331358ecf8e5551f5ca21208
      2026-06-04T22:10:49.124206+02:00 FortiAuthenticator radiusd[1617]: (59)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.124288+02:00 FortiAuthenticator radiusd[1617]: (59)   State = 0x2eb73cc92f07319234453ea63f84b1d2
      2026-06-04T22:10:49.128664+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:10:49.128692+02:00 FortiAuthenticator radiusd[1617]: (60) Received Access-Request Id 11 from 192.168.1.202:47130 to 192.168.1.206:1812 length 179
      2026-06-04T22:10:49.128695+02:00 FortiAuthenticator radiusd[1617]: (60)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.128698+02:00 FortiAuthenticator radiusd[1617]: (60)   State = 0x2eb73cc92f07319234453ea63f84b1d2
      2026-06-04T22:10:49.128700+02:00 FortiAuthenticator radiusd[1617]: (60)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.128701+02:00 FortiAuthenticator radiusd[1617]: (60)   EAP-Message = 0x02b000060d00
      2026-06-04T22:10:49.128704+02:00 FortiAuthenticator radiusd[1617]: (60)   Framed-MTU = 1500
      2026-06-04T22:10:49.128707+02:00 FortiAuthenticator radiusd[1617]: (60)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.128710+02:00 FortiAuthenticator radiusd[1617]: (60)   NAS-Port = 2
      2026-06-04T22:10:49.128712+02:00 FortiAuthenticator radiusd[1617]: (60)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.128714+02:00 FortiAuthenticator radiusd[1617]: (60)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.128800+02:00 FortiAuthenticator radiusd[1617]: (60)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.128809+02:00 FortiAuthenticator radiusd[1617]: (60)   Message-Authenticator = 0x3961365ff0a566189348a45fe1adc2f6
      2026-06-04T22:10:49.128814+02:00 FortiAuthenticator radiusd[1617]: (60) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.128842+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.128949+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.128967+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: ===>Timestamp:1780603849.128603, age:0ms 
      2026-06-04T22:10:49.128990+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.129002+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.129014+02:00 FortiAuthenticator radiusd[1617]: (60) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.129021+02:00 FortiAuthenticator radiusd[1617]: (60) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.129025+02:00 FortiAuthenticator radiusd[1617]: (60) eap: Removing EAP session with state 0x2eb73cc92f073192
      2026-06-04T22:10:49.129028+02:00 FortiAuthenticator radiusd[1617]: (60) eap: Previous EAP request found for state 0x2eb73cc92f073192, released from the list
      2026-06-04T22:10:49.129037+02:00 FortiAuthenticator radiusd[1617]: (60) eap: EAP session adding &reply:State = 0x2eb73cc92c063192
      2026-06-04T22:10:49.129043+02:00 FortiAuthenticator radiusd[1617]: (60) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.129045+02:00 FortiAuthenticator radiusd[1617]: (60)   Framed-MTU = 984
      2026-06-04T22:10:49.129047+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:10:49.129049+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:10:49.129051+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.129057+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:10:49.129161+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:10:49.129171+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.129180+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.129190+02:00 FortiAuthenticator radiusd[1617]: (60)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.129212+02:00 FortiAuthenticator radiusd[1617]: (60) Sent Access-Challenge Id 11 from 192.168.1.206:1812 to 192.168.1.202:47130 length 1054
      2026-06-04T22:10:49.129220+02:00 FortiAuthenticator radiusd[1617]: (60)   EAP-Message = 0x01b103de0dc0000008cf1b4ccea8c0217eabd736ac414b4cae59b7df2f4a51ffe685f4fbd039e486f21d1bb287342fc5267f98a7529c22a2d34a2eb04d5e100af0d836b057b97ac6674890284ca126a5a86fa4573f1642a6068c3b99608dc2521b7807c134c3765011af518d664055e8f9c5888ff15e4deb52e99dff8c1157c505489acc2463a056d381e97c0980c91c214c7a5d5c9c08f79e01820825dfdcbf758a62485ae88066fc2dad4995c819bee5a6d22e062d4c67f3241811e05242c945c018e9154f44c33490a3009b6172a79d029a00c3f8aa0cf9866908caab51f484409ec483872835702318ef5fb1b2d5f4382fe90b17d3fa3b959497b00deb7c825b16a0791a2443fc94c6f713017e69fab3857c66b2bac8216f34b3bc6f5e9baef6816d6720dc091400dcca504d9e9d2e7fb22ac08150ebf2745385c23522266d98962c6abd840a0ea7a67894417c951d6d38469998f593e15528b12ee08822d1c4629691fa72d22cd11d4f082d0bd24f475e8ae5b654
      2026-06-04T22:10:49.129222+02:00 FortiAuthenticator radiusd[1617]: (60)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.129223+02:00 FortiAuthenticator radiusd[1617]: (60)   State = 0x2eb73cc92c06319234453ea63f84b1d2
      2026-06-04T22:10:49.133259+02:00 FortiAuthenticator radiusd[1617]: (61) Received Access-Request Id 12 from 192.168.1.202:38086 to 192.168.1.206:1812 length 179
      2026-06-04T22:10:49.133263+02:00 FortiAuthenticator radiusd[1617]: (61)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.133265+02:00 FortiAuthenticator radiusd[1617]: (61)   State = 0x2eb73cc92c06319234453ea63f84b1d2
      2026-06-04T22:10:49.133267+02:00 FortiAuthenticator radiusd[1617]: (61)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.133269+02:00 FortiAuthenticator radiusd[1617]: (61)   EAP-Message = 0x02b100060d00
      2026-06-04T22:10:49.133271+02:00 FortiAuthenticator radiusd[1617]: (61)   Framed-MTU = 1500
      2026-06-04T22:10:49.133272+02:00 FortiAuthenticator radiusd[1617]: (61)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.133274+02:00 FortiAuthenticator radiusd[1617]: (61)   NAS-Port = 2
      2026-06-04T22:10:49.133276+02:00 FortiAuthenticator radiusd[1617]: (61)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.133277+02:00 FortiAuthenticator radiusd[1617]: (61)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.133279+02:00 FortiAuthenticator radiusd[1617]: (61)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.133295+02:00 FortiAuthenticator radiusd[1617]: (61)   Message-Authenticator = 0x402dbf8105f8a674ac29a89f68f9239b
      2026-06-04T22:10:49.133320+02:00 FortiAuthenticator radiusd[1617]: (61) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.133341+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.133354+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.133365+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: ===>Timestamp:1780603849.132900, age:0ms 
      2026-06-04T22:10:49.133379+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.133389+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.133400+02:00 FortiAuthenticator radiusd[1617]: (61) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.133411+02:00 FortiAuthenticator radiusd[1617]: (61) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.133421+02:00 FortiAuthenticator radiusd[1617]: (61) eap: Removing EAP session with state 0x2eb73cc92c063192
      2026-06-04T22:10:49.133430+02:00 FortiAuthenticator radiusd[1617]: (61) eap: Previous EAP request found for state 0x2eb73cc92c063192, released from the list
      2026-06-04T22:10:49.133444+02:00 FortiAuthenticator radiusd[1617]: (61) eap: EAP session adding &reply:State = 0x2eb73cc92d053192
      2026-06-04T22:10:49.133454+02:00 FortiAuthenticator radiusd[1617]: (61) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.133463+02:00 FortiAuthenticator radiusd[1617]: (61)   Framed-MTU = 984
      2026-06-04T22:10:49.133538+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:10:49.133541+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:10:49.133543+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.133544+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:10:49.133547+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:10:49.133549+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.133550+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.133552+02:00 FortiAuthenticator radiusd[1617]: (61)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.133559+02:00 FortiAuthenticator radiusd[1617]: (61) Sent Access-Challenge Id 12 from 192.168.1.206:1812 to 192.168.1.202:38086 length 365
      2026-06-04T22:10:49.133563+02:00 FortiAuthenticator radiusd[1617]: (61)   EAP-Message = 0x01b201310d80000008cf340423bfadf98a2922e39001ae82106a8fd4d570aa0b48c2cbb33f920e41123f3fee6e83a5c3b181111872c2fdcc34d5b46e8739946b036e739692946613ab48893fe5777a3f2806248461059541a52a3509ced40c00f6194ab2a2ee4cd48fce06e83cf19068d6e307e79c40518c2102ea18c8cce1d7898958590134dddac347123100b7383166d6e4d2c31b0222fa25b86ac6568368426d3263884d754eabeafc6c2c99aff9a1475cd6ceeae94a819980148d793d74924a94d6cabe55b5dbb4c8cf0ae152c013e8c232ca37577b90beb8803437f5efcc31b87bf057c41703030045c8ce9f66875ff465b29decf83fa0702107c0a3664475ffb2a10c86ad12ad14dfe6c597ec9ff2f9b3ba14485cd0b5ac796d2e6d2a878526a1dffa9e3f4c6202a66bf8b9e850
      2026-06-04T22:10:49.133565+02:00 FortiAuthenticator radiusd[1617]: (61)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.133570+02:00 FortiAuthenticator radiusd[1617]: (61)   State = 0x2eb73cc92d05319234453ea63f84b1d2
      2026-06-04T22:10:49.133706+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:10:49.143535+02:00 FortiAuthenticator radiusd[1617]: (62) Received Access-Request Id 13 from 192.168.1.202:33814 to 192.168.1.206:1812 length 1675
      2026-06-04T22:10:49.143539+02:00 FortiAuthenticator radiusd[1617]: (62)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.143541+02:00 FortiAuthenticator radiusd[1617]: (62)   State = 0x2eb73cc92d05319234453ea63f84b1d2
      2026-06-04T22:10:49.143543+02:00 FortiAuthenticator radiusd[1617]: (62)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.143548+02:00 FortiAuthenticator radiusd[1617]: (62)   EAP-Message = 0x02b205d40dc000000876140303000101170303086b61f4a87189c7b2c5fd31d6550e2ffd96c145e89f9093026c1a182ae2afe0152849b54314ca1e22f3744ca20204b0f94876a1b4133f91bc3daea47c2ad34d158cd672bbc6956640d1f3ba5dd5d6c279c054672adca41d8f7dd817488047e4f149abb6c8a25e697b1f0327ca5503ad328e34a15c3ad6236f539821b91ae5d32db02aec92980dfd898bcebbd8545bb12b8cae0e605f7f8e0029d6ed6f0671ed1d709fe2dcfaf387484a568f2274e7f9d034ef3f1db1da35cde0565ee3b15801e3211002fba9b1b1ecaab980ae35fb8e3d121fb4e307522d3a23a790e6090ca6dd015dd8bbb2cae1d481ce651678fdc6b5abd71b425bf86440084e4178cfd91fea5a4c87bf1f4c75cb57d3448d6efb8bd42dc00cf61c11f0abe440309d3e151d4d85991505ffd8f5c28496bb5fa84fe1641fe5df438bfbc760d9649a9b81e73494d2250588ddf7d9412669a5b7cd5b95656166cdbae1423d8cce507cf7f6bbc023c1acaf
      2026-06-04T22:10:49.143551+02:00 FortiAuthenticator radiusd[1617]: (62)   Framed-MTU = 1500
      2026-06-04T22:10:49.143553+02:00 FortiAuthenticator radiusd[1617]: (62)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.143556+02:00 FortiAuthenticator radiusd[1617]: (62)   NAS-Port = 2
      2026-06-04T22:10:49.143558+02:00 FortiAuthenticator radiusd[1617]: (62)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.143561+02:00 FortiAuthenticator radiusd[1617]: (62)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.143563+02:00 FortiAuthenticator radiusd[1617]: (62)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.143597+02:00 FortiAuthenticator radiusd[1617]: (62)   Message-Authenticator = 0x3bf73e2bcd0b2956cca1ebb35b148a03
      2026-06-04T22:10:49.143648+02:00 FortiAuthenticator radiusd[1617]: (62) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.143766+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.143769+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.143773+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: ===>Timestamp:1780603849.143409, age:0ms 
      2026-06-04T22:10:49.143785+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.143788+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.143795+02:00 FortiAuthenticator radiusd[1617]: (62) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.143801+02:00 FortiAuthenticator radiusd[1617]: (62) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.143808+02:00 FortiAuthenticator radiusd[1617]: (62) eap: Removing EAP session with state 0x2eb73cc92d053192
      2026-06-04T22:10:49.143810+02:00 FortiAuthenticator radiusd[1617]: (62) eap: Previous EAP request found for state 0x2eb73cc92d053192, released from the list
      2026-06-04T22:10:49.143822+02:00 FortiAuthenticator radiusd[1617]: (62) eap: EAP session adding &reply:State = 0x2eb73cc92a043192
      2026-06-04T22:10:49.143828+02:00 FortiAuthenticator radiusd[1617]: (62) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.143894+02:00 FortiAuthenticator radiusd[1617]: (62)   Framed-MTU = 984
      2026-06-04T22:10:49.143898+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:10:49.143900+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:10:49.143902+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.143904+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:10:49.143906+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:10:49.143907+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.143909+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.143911+02:00 FortiAuthenticator radiusd[1617]: (62)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.143919+02:00 FortiAuthenticator radiusd[1617]: (62) Sent Access-Challenge Id 13 from 192.168.1.206:1812 to 192.168.1.202:33814 length 64
      2026-06-04T22:10:49.143921+02:00 FortiAuthenticator radiusd[1617]: (62)   EAP-Message = 0x01b300060d00
      2026-06-04T22:10:49.143998+02:00 FortiAuthenticator radiusd[1617]: (62)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.144000+02:00 FortiAuthenticator radiusd[1617]: (62)   State = 0x2eb73cc92a04319234453ea63f84b1d2
      2026-06-04T22:10:49.144073+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:10:49.147269+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:10:49.147293+02:00 FortiAuthenticator radiusd[1617]: (63) Received Access-Request Id 14 from 192.168.1.202:58423 to 192.168.1.206:1812 length 867
      2026-06-04T22:10:49.147296+02:00 FortiAuthenticator radiusd[1617]: (63)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.147298+02:00 FortiAuthenticator radiusd[1617]: (63)   State = 0x2eb73cc92a04319234453ea63f84b1d2
      2026-06-04T22:10:49.147299+02:00 FortiAuthenticator radiusd[1617]: (63)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.147304+02:00 FortiAuthenticator radiusd[1617]: (63)   EAP-Message = 0x02b302b20d005b72de3189aa76b9088a4c95e02ec75b72224f87cd6425f5f80d806913c615893c48bb686b35f934b55f955cda6e8b0374130d5741fc9d9efb593407c022793550cf01b5e5d2aad2827981e9bbc37f59632e21e3922a4a83e3ba1adc6c6dab3d778c7bb9fc4cb81fc15de6f41cb95fa5d66b7333d7f3dda9083059ffbacb5858f58130be7862bf4417d7232cce5894596d07a0a10cf3b7c5894a8d1f473dae122b409c4d1e6aba9da7908ce5be0c438bf3e090183e88722137b9073cfb48fd063b655c5e56d2355f24b5fd41ff4ba7572226b10e1a2ce8a9993c95172bf40c256a98e4209cfdf5f31b45e95c8cf7195838e71a0db56964e5e826b95e8c9f423ab21ead1b94a6b657ebba94ea96fe0fe96100f765222b1f628d31128694f938b0efd119b62c602336c0f49770da324b93caeea7babd5ec7aba1278d9694aca566363aae540774f90643cf29ca19af4c1374fb539512a659afedd62755e56054ae730c4d166e6db00e75f5173ab2425f2185
      2026-06-04T22:10:49.147306+02:00 FortiAuthenticator radiusd[1617]: (63)   Framed-MTU = 1500
      2026-06-04T22:10:49.147308+02:00 FortiAuthenticator radiusd[1617]: (63)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.147310+02:00 FortiAuthenticator radiusd[1617]: (63)   NAS-Port = 2
      2026-06-04T22:10:49.147312+02:00 FortiAuthenticator radiusd[1617]: (63)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.147314+02:00 FortiAuthenticator radiusd[1617]: (63)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.147405+02:00 FortiAuthenticator radiusd[1617]: (63)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.147407+02:00 FortiAuthenticator radiusd[1617]: (63)   Message-Authenticator = 0x7b2a9075c333e8146820c94e3bec1430
      2026-06-04T22:10:49.147411+02:00 FortiAuthenticator radiusd[1617]: (63) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.147427+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.147433+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.147435+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: ===>Timestamp:1780603849.147225, age:0ms 
      2026-06-04T22:10:49.147442+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.147445+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.147449+02:00 FortiAuthenticator radiusd[1617]: (63) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.147453+02:00 FortiAuthenticator radiusd[1617]: (63) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.147456+02:00 FortiAuthenticator radiusd[1617]: (63) eap: Removing EAP session with state 0x2eb73cc92a043192
      2026-06-04T22:10:49.147502+02:00 FortiAuthenticator radiusd[1617]: (63) eap: Previous EAP request found for state 0x2eb73cc92a043192, released from the list
      2026-06-04T22:10:49.148031+02:00 FortiAuthenticator radiusd[1617]: rlm_eap_tls: Certificate passed CRL check.
      2026-06-04T22:10:49.148634+02:00 FortiAuthenticator radiusd[1617]: (63) eap_tls: rlm_eap_tls: Certificate check succeeded. (CN=F3AB0220E981419CAA9A13CE4811E3F9, Issuer=/CN=EMS_SUBCA)
      2026-06-04T22:10:49.148794+02:00 FortiAuthenticator radiusd[1617]: rlm_eap_tls: Certificate passed CRL check.
      2026-06-04T22:10:49.148901+02:00 FortiAuthenticator radiusd[1617]: (63) eap_tls: (TLS) EAP Sending final Commitment Message.
      2026-06-04T22:10:49.148928+02:00 FortiAuthenticator radiusd[1617]: (63) eap: EAP session adding &reply:State = 0x2eb73cc92b033192
      2026-06-04T22:10:49.148935+02:00 FortiAuthenticator radiusd[1617]: (63) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.148938+02:00 FortiAuthenticator radiusd[1617]: (63)   Framed-MTU = 984
      2026-06-04T22:10:49.148941+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:10:49.148943+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:10:49.148945+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.148947+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:10:49.148974+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:10:49.148980+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.148983+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.149044+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.149051+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:10:49.149053+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, Certificate"
      2026-06-04T22:10:49.149055+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Client-Cert-Common-Name += "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.149056+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Client-Cert-Issuer += "/CN=EMS_SUBCA"
      2026-06-04T22:10:49.149058+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Client-Cert-X509v3-Authority-Key-Identifier += "7B:90:6C:7C:0F:E9:82:56:B9:CE:C2:CF:91:19:78:28:B5:B0:33:08"
      2026-06-04T22:10:49.149060+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:10:49.149062+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.149064+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Cipher-Suite = "TLS_AES_256_GCM_SHA384"
      2026-06-04T22:10:49.149065+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Version = "TLS 1.3"
      2026-06-04T22:10:49.149067+02:00 FortiAuthenticator radiusd[1617]: (63)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:10:49.149082+02:00 FortiAuthenticator radiusd[1617]: (63) Sent Access-Challenge Id 14 from 192.168.1.206:1812 to 192.168.1.202:58423 length 91
      2026-06-04T22:10:49.149160+02:00 FortiAuthenticator radiusd[1617]: (63)   EAP-Message = 0x01b400210d800000001717030300128103fc2211821ecde823d968575e23b4d114
      2026-06-04T22:10:49.149169+02:00 FortiAuthenticator radiusd[1617]: (63)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:10:49.149174+02:00 FortiAuthenticator radiusd[1617]: (63)   State = 0x2eb73cc92b03319234453ea63f84b1d2
      2026-06-04T22:10:49.157079+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:10:49.157119+02:00 FortiAuthenticator radiusd[1617]: (64) Received Access-Request Id 15 from 192.168.1.202:58511 to 192.168.1.206:1812 length 179
      2026-06-04T22:10:49.157124+02:00 FortiAuthenticator radiusd[1617]: (64)   NAS-Identifier = "S424ENTFSERIAL"
      2026-06-04T22:10:49.157127+02:00 FortiAuthenticator radiusd[1617]: (64)   State = 0x2eb73cc92b03319234453ea63f84b1d2
      2026-06-04T22:10:49.157129+02:00 FortiAuthenticator radiusd[1617]: (64)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.157153+02:00 FortiAuthenticator radiusd[1617]: (64)   EAP-Message = 0x02b400060d00
      2026-06-04T22:10:49.157159+02:00 FortiAuthenticator radiusd[1617]: (64)   Framed-MTU = 1500
      2026-06-04T22:10:49.157161+02:00 FortiAuthenticator radiusd[1617]: (64)   NAS-Port-Id = "port2"
      2026-06-04T22:10:49.157164+02:00 FortiAuthenticator radiusd[1617]: (64)   NAS-Port = 2
      2026-06-04T22:10:49.157167+02:00 FortiAuthenticator radiusd[1617]: (64)   NAS-Port-Type = Ethernet
      2026-06-04T22:10:49.157169+02:00 FortiAuthenticator radiusd[1617]: (64)   Called-Station-Id = "38-C0-EA-12-E1-1D"
      2026-06-04T22:10:49.157267+02:00 FortiAuthenticator radiusd[1617]: (64)   Calling-Station-Id = "00-E0-4C-68-00-14"
      2026-06-04T22:10:49.157270+02:00 FortiAuthenticator radiusd[1617]: (64)   Message-Authenticator = 0xdd7180e060f358095d78ee4de24fd64c
      2026-06-04T22:10:49.157276+02:00 FortiAuthenticator radiusd[1617]: (64) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.157302+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.157304+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:10:49.157308+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: ===>Timestamp:1780603849.156995, age:0ms 
      2026-06-04T22:10:49.157319+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:10:49.157323+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.157329+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:10:49.157334+02:00 FortiAuthenticator radiusd[1617]: (64) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.157339+02:00 FortiAuthenticator radiusd[1617]: (64) eap: Removing EAP session with state 0x2eb73cc92b033192
      2026-06-04T22:10:49.157373+02:00 FortiAuthenticator radiusd[1617]: (64) eap: Previous EAP request found for state 0x2eb73cc92b033192, released from the list
      2026-06-04T22:10:49.157467+02:00 FortiAuthenticator radiusd[1617]: (64) # Executing section post-auth from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:10:49.157486+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::Framed-MTU += &session-state:Framed-MTU[*] -> 984
      2026-06-04T22:10:49.157496+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, ClientHello'
      2026-06-04T22:10:49.157506+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, ServerHello'
      2026-06-04T22:10:49.157515+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 ChangeCipherSpec'
      2026-06-04T22:10:49.157539+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions'
      2026-06-04T22:10:49.157547+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest'
      2026-06-04T22:10:49.157552+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Certificate'
      2026-06-04T22:10:49.157554+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify'
      2026-06-04T22:10:49.157557+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Finished'
      2026-06-04T22:10:49.157559+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 ChangeCipherSpec'
      2026-06-04T22:10:49.157561+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, Certificate'
      2026-06-04T22:10:49.157563+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Client-Cert-Common-Name += &session-state:TLS-Client-Cert-Common-Name[*] -> 'F3AB0220E981419CAA9A13CE4811E3F9'
      2026-06-04T22:10:49.157565+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Client-Cert-Issuer += &session-state:TLS-Client-Cert-Issuer[*] -> '/CN=EMS_SUBCA'
      2026-06-04T22:10:49.157568+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Client-Cert-X509v3-Authority-Key-Identifier += &session-state:TLS-Client-Cert-X509v3-Authority-Key-Identifier[*] -> '7B:90:6C:7C:0F:E9:82:56:B9:CE:C2:CF:91:19:78:28:B5:B0:33:08'
      2026-06-04T22:10:49.157570+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, CertificateVerify'
      2026-06-04T22:10:49.157572+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, Finished'
      2026-06-04T22:10:49.157574+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Cipher-Suite += &session-state:TLS-Session-Cipher-Suite[*] -> 'TLS_AES_256_GCM_SHA384'
      2026-06-04T22:10:49.157618+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Version += &session-state:TLS-Session-Version[*] -> 'TLS 1.3'
      2026-06-04T22:10:49.157621+02:00 FortiAuthenticator radiusd[1617]: (64)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Finished'
      2026-06-04T22:10:49.157624+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: ========> FACAUTH POSTAUTH 
      2026-06-04T22:10:49.157627+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:10:49.157630+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:10:49.157962+02:00 FortiAuthenticator radiusd[1617]: (64) facauth: Updated auth log 'F3AB0220E981419CAA9A13CE4811E3F9' for attempt from 192.168.1.202: 802.1x authentication (EAP-TLS) successful 
      2026-06-04T22:10:49.157999+02:00 FortiAuthenticator radiusd[1617]: (64) Sent Access-Accept Id 15 from 192.168.1.206:1812 to 192.168.1.202:58511 length 200
      2026-06-04T22:10:49.158001+02:00 FortiAuthenticator radiusd[1617]: (64)   Message-Authenticator := 0x00
      2026-06-04T22:10:49.158003+02:00 FortiAuthenticator radiusd[1617]: (64)   MS-MPPE-Recv-Key = <<< secret >>>
      2026-06-04T22:10:49.158047+02:00 FortiAuthenticator radiusd[1617]: (64)   MS-MPPE-Send-Key = <<< secret >>>
      2026-06-04T22:10:49.158049+02:00 FortiAuthenticator radiusd[1617]: (64)   EAP-Message = 0x03b40004
      2026-06-04T22:10:49.158051+02:00 FortiAuthenticator radiusd[1617]: (64)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:10:49.158053+02:00 FortiAuthenticator radiusd[1617]: (64)   Framed-MTU += 984

      For Wi-Fi connections, we can also see, on the CLI, that there is an authenticated user available using the diagnose firewall auth list command.

      diagnose firewall auth list
      
      10.100.0.4, F3AB0220E981419CAA9A13CE4811E3F9
              type: other, id: 0, duration: 204, idled: 123
              flag(10): radius
              server: FAC
              packets: in 83 out 83, bytes: in 13099 out 7500

      Again, here are the boring FortiAuthenticator RADIUS debugs:

      FortiAuthenticator wireless 802.1X debugs

      2026-06-04T22:15:00.412912+02:00 FortiAuthenticator radiusd[1617]: (65) Received Access-Request Id 0 from 192.168.1.202:23356 to 192.168.1.206:1812 length 336
      2026-06-04T22:15:00.412918+02:00 FortiAuthenticator radiusd[1617]: (65)   Message-Authenticator = 0x68cec83dc459ece9821670de46ac71ff
      2026-06-04T22:15:00.412921+02:00 FortiAuthenticator radiusd[1617]: (65)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.412924+02:00 FortiAuthenticator radiusd[1617]: (65)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.412926+02:00 FortiAuthenticator radiusd[1617]: (65)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.412928+02:00 FortiAuthenticator radiusd[1617]: (65)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.412930+02:00 FortiAuthenticator radiusd[1617]: (65)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.412933+02:00 FortiAuthenticator radiusd[1617]: (65)   Service-Type = Framed-User
      2026-06-04T22:15:00.412937+02:00 FortiAuthenticator radiusd[1617]: (65)   NAS-Port = 1
      2026-06-04T22:15:00.412938+02:00 FortiAuthenticator radiusd[1617]: (65)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.412940+02:00 FortiAuthenticator radiusd[1617]: (65)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.412961+02:00 FortiAuthenticator radiusd[1617]: (65)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.412990+02:00 FortiAuthenticator radiusd[1617]: (65)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.412999+02:00 FortiAuthenticator radiusd[1617]: (65)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.413008+02:00 FortiAuthenticator radiusd[1617]: (65)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.413016+02:00 FortiAuthenticator radiusd[1617]: (65)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.413025+02:00 FortiAuthenticator radiusd[1617]: (65)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.413033+02:00 FortiAuthenticator radiusd[1617]: (65)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.413058+02:00 FortiAuthenticator radiusd[1617]: (65)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.413082+02:00 FortiAuthenticator radiusd[1617]: (65)   Framed-MTU = 1400
      2026-06-04T22:15:00.413091+02:00 FortiAuthenticator radiusd[1617]: (65)   EAP-Message = 0x02190025014633414230323230453938313431394341413941313343453438313145334639
      2026-06-04T22:15:00.413103+02:00 FortiAuthenticator radiusd[1617]: (65) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.413144+02:00 FortiAuthenticator radiusd[1617]: (65) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.413153+02:00 FortiAuthenticator radiusd[1617]: (65) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.413211+02:00 FortiAuthenticator radiusd[1617]: Not doing PAP as Auth-Type is already set.
      2026-06-04T22:15:00.413222+02:00 FortiAuthenticator radiusd[1617]: (65) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.413238+02:00 FortiAuthenticator radiusd[1617]: (65) eap: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.413250+02:00 FortiAuthenticator radiusd[1617]: (65) eap: No policy matched for client:3 
      2026-06-04T22:15:00.413259+02:00 FortiAuthenticator radiusd[1617]: (65) eap: Unable to find matching authpolicy (eap-teap) for RADIUS client with IP 192.168.1.202. 
      2026-06-04T22:15:00.413855+02:00 FortiAuthenticator radiusd[1617]: (65) eap: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.413870+02:00 FortiAuthenticator radiusd[1617]: found 1 eap methods from authprofile 1 
      2026-06-04T22:15:00.413875+02:00 FortiAuthenticator radiusd[1617]: (65) eap: No policy matched for client:3 
      2026-06-04T22:15:00.413877+02:00 FortiAuthenticator radiusd[1617]: (65) eap: Unable to find matching authpolicy (password) for RADIUS client with IP 192.168.1.202. 
      2026-06-04T22:15:00.413881+02:00 FortiAuthenticator radiusd[1617]: (65) eap: Found &control:EAP-Type = TLS
      2026-06-04T22:15:00.413999+02:00 FortiAuthenticator radiusd[1617]: (65) eap: EAP session adding &reply:State = 0x4089863640938be4
      2026-06-04T22:15:00.414010+02:00 FortiAuthenticator radiusd[1617]: (65) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.414013+02:00 FortiAuthenticator radiusd[1617]: (65)   Framed-MTU = 984
      2026-06-04T22:15:00.414029+02:00 FortiAuthenticator radiusd[1617]: (65) Sent Access-Challenge Id 0 from 192.168.1.206:1812 to 192.168.1.202:23356 length 64
      2026-06-04T22:15:00.414031+02:00 FortiAuthenticator radiusd[1617]: (65)   EAP-Message = 0x011a00060d20
      2026-06-04T22:15:00.414033+02:00 FortiAuthenticator radiusd[1617]: (65)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.414100+02:00 FortiAuthenticator radiusd[1617]: (65)   State = 0x4089863640938be48ec08401850d17cb
      2026-06-04T22:15:00.428717+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:15:00.429034+02:00 FortiAuthenticator radiusd[1617]: (66) Received Access-Request Id 1 from 192.168.1.202:23356 to 192.168.1.206:1812 length 756
      2026-06-04T22:15:00.429039+02:00 FortiAuthenticator radiusd[1617]: (66)   Message-Authenticator = 0x46908590456de56e21be80fef082a211
      2026-06-04T22:15:00.429041+02:00 FortiAuthenticator radiusd[1617]: (66)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.429044+02:00 FortiAuthenticator radiusd[1617]: (66)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.429046+02:00 FortiAuthenticator radiusd[1617]: (66)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.429047+02:00 FortiAuthenticator radiusd[1617]: (66)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.429051+02:00 FortiAuthenticator radiusd[1617]: (66)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.429054+02:00 FortiAuthenticator radiusd[1617]: (66)   Service-Type = Framed-User
      2026-06-04T22:15:00.429055+02:00 FortiAuthenticator radiusd[1617]: (66)   NAS-Port = 1
      2026-06-04T22:15:00.429057+02:00 FortiAuthenticator radiusd[1617]: (66)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.429059+02:00 FortiAuthenticator radiusd[1617]: (66)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.429194+02:00 FortiAuthenticator radiusd[1617]: (66)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.429198+02:00 FortiAuthenticator radiusd[1617]: (66)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.429200+02:00 FortiAuthenticator radiusd[1617]: (66)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.429201+02:00 FortiAuthenticator radiusd[1617]: (66)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.429203+02:00 FortiAuthenticator radiusd[1617]: (66)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.429205+02:00 FortiAuthenticator radiusd[1617]: (66)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.429206+02:00 FortiAuthenticator radiusd[1617]: (66)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.429208+02:00 FortiAuthenticator radiusd[1617]: (66)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.429210+02:00 FortiAuthenticator radiusd[1617]: (66)   Framed-MTU = 1400
      2026-06-04T22:15:00.429215+02:00 FortiAuthenticator radiusd[1617]: (66)   EAP-Message = 0x021a01b50d80000001ab16030101a6010001a20303ad07be8db04e75b6e7521edaf284ea1fdb8fa04a4cfc8696742d890574b372562024e18e7cacc3e92109519fc46b14fa834b3869231a2e4cea35db4798c446bc50002813021301c02cc02bc030c02fc024c023c028c027c00ac009c014c013009d009c003d003c0035002f01000131000500050100000000002b0009080304030303020301000d001a001808040805080604010501020104030503020302020601060300230000000a00080006001d00170018000b00020100003300d000ce001d002018328c8bef2a0b0f500075c1ee5b43f178439e7acfcae6752f0affc6841c27200017004104f1e1dd85ba7f404e8fe2777ba6f0e58254a7c0e0753eca25ad10ce8fee6b72d5392900c74211eef408335f0c8440d7afc653f7d2263319be209ec4df7222e09600180061046801727e018e2566f6bcbcea358f7796276f4ea687f93eefef1ae0bfdb99765ed9b186d0d07e4a32b6b8a26699d56a8825f974dda0
      2026-06-04T22:15:00.429216+02:00 FortiAuthenticator radiusd[1617]: (66)   State = 0x4089863640938be48ec08401850d17cb
      2026-06-04T22:15:00.429295+02:00 FortiAuthenticator radiusd[1617]: (66) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.429325+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.429327+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.429331+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: ===>Timestamp:1780604100.428628, age:0ms 
      2026-06-04T22:15:00.429343+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.429347+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.429354+02:00 FortiAuthenticator radiusd[1617]: (66) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.429360+02:00 FortiAuthenticator radiusd[1617]: (66) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.429365+02:00 FortiAuthenticator radiusd[1617]: (66) eap: Removing EAP session with state 0x4089863640938be4
      2026-06-04T22:15:00.429368+02:00 FortiAuthenticator radiusd[1617]: (66) eap: Previous EAP request found for state 0x4089863640938be4, released from the list
      2026-06-04T22:15:00.430099+02:00 FortiAuthenticator radiusd[1617]: (66) eap: EAP session adding &reply:State = 0x4089863641928be4
      2026-06-04T22:15:00.432648+02:00 FortiAuthenticator radiusd[1617]: (66) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.432668+02:00 FortiAuthenticator radiusd[1617]: (66)   Framed-MTU = 984
      2026-06-04T22:15:00.432673+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:15:00.432675+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:15:00.432677+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.432679+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:15:00.432681+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:15:00.432683+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.432684+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.432686+02:00 FortiAuthenticator radiusd[1617]: (66)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.432710+02:00 FortiAuthenticator radiusd[1617]: (66) Sent Access-Challenge Id 1 from 192.168.1.206:1812 to 192.168.1.202:23356 length 1054
      2026-06-04T22:15:00.432730+02:00 FortiAuthenticator radiusd[1617]: (66)   EAP-Message = 0x011b03de0dc0000008cf160303009b0200009703033dc1aed6c7dd0ffc0d1ff07eee8e42ce8a04bbbe06ffd16b0eafb3ef775943962024e18e7cacc3e92109519fc46b14fa834b3869231a2e4cea35db4798c446bc50130200004f002b00020304003300450017004104d303e88dd3b685dd05f69047629123b22e455965d83a05ecf56da30379f3809416446191e80315bbcce67ef2dbbd7d0cbb67a82430eeb07251417a6466b041ca1403030001011703030023700dcf52789585dadd15f0872b4837b1efa122fbcc2331316e92ddd0190e816fc37fae17030300464bb055288479dc828f32af7bc2add56c3c2f0339aa2d069f0b887e90024900decab19c34e2992d3ac2f25a56e6af5c50396566aeb2a3b1214e43a695dabd52ebc31aab7bd599170303064910c41698720cd20975bfb882c04d9418e4911e967f3beea9c6cb741872fd1e0b107004eb44e9a22c728917c6c81ce21342a26adff9f73c67302396db3b7111ebc229f9cfea85ece89cd6466d34221f
      2026-06-04T22:15:00.432752+02:00 FortiAuthenticator radiusd[1617]: (66)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.432761+02:00 FortiAuthenticator radiusd[1617]: (66)   State = 0x4089863641928be48ec08401850d17cb
      2026-06-04T22:15:00.440458+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.3 seconds.
      2026-06-04T22:15:00.440551+02:00 FortiAuthenticator radiusd[1617]: (67) Received Access-Request Id 2 from 192.168.1.202:23356 to 192.168.1.206:1812 length 323
      2026-06-04T22:15:00.440555+02:00 FortiAuthenticator radiusd[1617]: (67)   Message-Authenticator = 0x5d35fee7480011ef4b0094327853eeec
      2026-06-04T22:15:00.440558+02:00 FortiAuthenticator radiusd[1617]: (67)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.440561+02:00 FortiAuthenticator radiusd[1617]: (67)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.440562+02:00 FortiAuthenticator radiusd[1617]: (67)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.440564+02:00 FortiAuthenticator radiusd[1617]: (67)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.440567+02:00 FortiAuthenticator radiusd[1617]: (67)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.440570+02:00 FortiAuthenticator radiusd[1617]: (67)   Service-Type = Framed-User
      2026-06-04T22:15:00.440572+02:00 FortiAuthenticator radiusd[1617]: (67)   NAS-Port = 1
      2026-06-04T22:15:00.440574+02:00 FortiAuthenticator radiusd[1617]: (67)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.440575+02:00 FortiAuthenticator radiusd[1617]: (67)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.440611+02:00 FortiAuthenticator radiusd[1617]: (67)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.440613+02:00 FortiAuthenticator radiusd[1617]: (67)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.440615+02:00 FortiAuthenticator radiusd[1617]: (67)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.440616+02:00 FortiAuthenticator radiusd[1617]: (67)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.440618+02:00 FortiAuthenticator radiusd[1617]: (67)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.440620+02:00 FortiAuthenticator radiusd[1617]: (67)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.440621+02:00 FortiAuthenticator radiusd[1617]: (67)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.440623+02:00 FortiAuthenticator radiusd[1617]: (67)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.440625+02:00 FortiAuthenticator radiusd[1617]: (67)   Framed-MTU = 1400
      2026-06-04T22:15:00.440626+02:00 FortiAuthenticator radiusd[1617]: (67)   EAP-Message = 0x021b00060d00
      2026-06-04T22:15:00.440628+02:00 FortiAuthenticator radiusd[1617]: (67)   State = 0x4089863641928be48ec08401850d17cb
      2026-06-04T22:15:00.440638+02:00 FortiAuthenticator radiusd[1617]: (67) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.440692+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.440702+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.440712+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: ===>Timestamp:1780604100.440383, age:0ms 
      2026-06-04T22:15:00.440732+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.440744+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.440759+02:00 FortiAuthenticator radiusd[1617]: (67) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.440772+02:00 FortiAuthenticator radiusd[1617]: (67) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.440784+02:00 FortiAuthenticator radiusd[1617]: (67) eap: Removing EAP session with state 0x4089863641928be4
      2026-06-04T22:15:00.440793+02:00 FortiAuthenticator radiusd[1617]: (67) eap: Previous EAP request found for state 0x4089863641928be4, released from the list
      2026-06-04T22:15:00.440814+02:00 FortiAuthenticator radiusd[1617]: (67) eap: EAP session adding &reply:State = 0x4089863642958be4
      2026-06-04T22:15:00.440826+02:00 FortiAuthenticator radiusd[1617]: (67) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.440835+02:00 FortiAuthenticator radiusd[1617]: (67)   Framed-MTU = 984
      2026-06-04T22:15:00.440845+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:15:00.440853+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:15:00.440862+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.440877+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:15:00.440896+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:15:00.440905+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.440913+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.440922+02:00 FortiAuthenticator radiusd[1617]: (67)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.440942+02:00 FortiAuthenticator radiusd[1617]: (67) Sent Access-Challenge Id 2 from 192.168.1.206:1812 to 192.168.1.202:23356 length 1054
      2026-06-04T22:15:00.440953+02:00 FortiAuthenticator radiusd[1617]: (67)   EAP-Message = 0x011c03de0dc0000008cf44daf571f4adb35b720800e8b0cb680aacdf9c950d71e475c4bc18216a59ce07f82fd359b07f7ddb2d49fe927ca4fa629792b516c9da27115be18133ce74e4e3ab0ea91a285843d9ba07c167902bd573ccb730ed946f74196f185f38e762df738fac5525253dc1ebe618d63ae6f3339e7bcec6732d7137b85f00d88e1944e5daab69ee48f062661470e374cbc5aa2b07a45fa2b1cde5f9894b5b2bb03f09aa0891afa09692bf0422a18e2dfdf8d54e26ce073c75b8198178e51c03b5820ca47884cf0354df34684ce842f5a4f86f7aac4d09b8edd788cf30e8076b4482407201907cc9abf80c5c5f214e06a957461365d20ea9d94c4dd0eff02f10f2cb0cc70ed9d3e9054e6532efaf55ce7b9c865f77e9e000ae1752663b1f22717daa6262da020f61561fe4eaf753d0a6e383ce029361b4caec4ed0d7ffa6fd44e533af29843adcc410883c0c629aaf8a24e64c1f9e972076a5579dcb66198c6bae00c1981b6fc3e6b9be5e0e62d4824ddf3e
      2026-06-04T22:15:00.440962+02:00 FortiAuthenticator radiusd[1617]: (67)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.440970+02:00 FortiAuthenticator radiusd[1617]: (67)   State = 0x4089863642958be48ec08401850d17cb
      2026-06-04T22:15:00.446416+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:15:00.446504+02:00 FortiAuthenticator radiusd[1617]: (68) Received Access-Request Id 3 from 192.168.1.202:23356 to 192.168.1.206:1812 length 323
      2026-06-04T22:15:00.446508+02:00 FortiAuthenticator radiusd[1617]: (68)   Message-Authenticator = 0x65c013575d3732127c7494d3637b55be
      2026-06-04T22:15:00.446510+02:00 FortiAuthenticator radiusd[1617]: (68)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.446512+02:00 FortiAuthenticator radiusd[1617]: (68)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.446514+02:00 FortiAuthenticator radiusd[1617]: (68)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.446517+02:00 FortiAuthenticator radiusd[1617]: (68)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.446519+02:00 FortiAuthenticator radiusd[1617]: (68)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.446522+02:00 FortiAuthenticator radiusd[1617]: (68)   Service-Type = Framed-User
      2026-06-04T22:15:00.446524+02:00 FortiAuthenticator radiusd[1617]: (68)   NAS-Port = 1
      2026-06-04T22:15:00.446525+02:00 FortiAuthenticator radiusd[1617]: (68)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.446527+02:00 FortiAuthenticator radiusd[1617]: (68)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.446574+02:00 FortiAuthenticator radiusd[1617]: (68)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.446576+02:00 FortiAuthenticator radiusd[1617]: (68)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.446578+02:00 FortiAuthenticator radiusd[1617]: (68)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.446580+02:00 FortiAuthenticator radiusd[1617]: (68)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.446581+02:00 FortiAuthenticator radiusd[1617]: (68)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.446583+02:00 FortiAuthenticator radiusd[1617]: (68)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.446584+02:00 FortiAuthenticator radiusd[1617]: (68)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.446586+02:00 FortiAuthenticator radiusd[1617]: (68)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.446588+02:00 FortiAuthenticator radiusd[1617]: (68)   Framed-MTU = 1400
      2026-06-04T22:15:00.446590+02:00 FortiAuthenticator radiusd[1617]: (68)   EAP-Message = 0x021c00060d00
      2026-06-04T22:15:00.446591+02:00 FortiAuthenticator radiusd[1617]: (68)   State = 0x4089863642958be48ec08401850d17cb
      2026-06-04T22:15:00.446601+02:00 FortiAuthenticator radiusd[1617]: (68) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.446635+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.446644+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.446654+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: ===>Timestamp:1780604100.446348, age:0ms 
      2026-06-04T22:15:00.446678+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.446688+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.446702+02:00 FortiAuthenticator radiusd[1617]: (68) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.446715+02:00 FortiAuthenticator radiusd[1617]: (68) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.446727+02:00 FortiAuthenticator radiusd[1617]: (68) eap: Removing EAP session with state 0x4089863642958be4
      2026-06-04T22:15:00.446736+02:00 FortiAuthenticator radiusd[1617]: (68) eap: Previous EAP request found for state 0x4089863642958be4, released from the list
      2026-06-04T22:15:00.446753+02:00 FortiAuthenticator radiusd[1617]: (68) eap: EAP session adding &reply:State = 0x4089863643948be4
      2026-06-04T22:15:00.446777+02:00 FortiAuthenticator radiusd[1617]: (68) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.446787+02:00 FortiAuthenticator radiusd[1617]: (68)   Framed-MTU = 984
      2026-06-04T22:15:00.446796+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:15:00.446805+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:15:00.446813+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.446822+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:15:00.446831+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:15:00.446839+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.446848+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.446856+02:00 FortiAuthenticator radiusd[1617]: (68)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.446874+02:00 FortiAuthenticator radiusd[1617]: (68) Sent Access-Challenge Id 3 from 192.168.1.206:1812 to 192.168.1.202:23356 length 365
      2026-06-04T22:15:00.446885+02:00 FortiAuthenticator radiusd[1617]: (68)   EAP-Message = 0x011d01310d80000008cf16c1506b049234082a7db8e0aa1ef2e847d0e8710d5c68fecaad43726935ac1f055ab27ad33e355a879800d4521f48bbd3b7ec8515844188d18c5cc36a644e8c62f4bd1a101ebcfd83fe78f55773f06af78fd220f240047f05bb931232eb0765780f62dfd0fb96de25ebae655f1e0b20a41bd12c9b66329c58f9e92e432b0029a36b9038f612e6f9044a1f887b8b3b36f8bdeb86bbe04e4fd0d3c29f557b842900912aeb1479eab3d9761c0f1cf41f5919ecda9c2765c163d5069c9ee51c1675d963f2a39375576a1500f8436d03f967cbd586f5de3c870173e118453317030300451df88fbe8bdba65d4076fb68b651e68c5c58ced14288bbc87f556ac87354abc94de8873a8f04fdf26582373b2bb0b0516e0d0f0d0aafb33f9303b02952767d338a79edcacc
      2026-06-04T22:15:00.446894+02:00 FortiAuthenticator radiusd[1617]: (68)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.446902+02:00 FortiAuthenticator radiusd[1617]: (68)   State = 0x4089863643948be48ec08401850d17cb
      2026-06-04T22:15:00.459684+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:15:00.459775+02:00 FortiAuthenticator radiusd[1617]: (69) Received Access-Request Id 4 from 192.168.1.202:23356 to 192.168.1.206:1812 length 1819
      2026-06-04T22:15:00.459789+02:00 FortiAuthenticator radiusd[1617]: (69)   Message-Authenticator = 0xd0d668cce91a5520d600dbc18a88d3ae
      2026-06-04T22:15:00.459799+02:00 FortiAuthenticator radiusd[1617]: (69)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.459808+02:00 FortiAuthenticator radiusd[1617]: (69)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.459817+02:00 FortiAuthenticator radiusd[1617]: (69)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.459826+02:00 FortiAuthenticator radiusd[1617]: (69)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.459834+02:00 FortiAuthenticator radiusd[1617]: (69)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.459844+02:00 FortiAuthenticator radiusd[1617]: (69)   Service-Type = Framed-User
      2026-06-04T22:15:00.459852+02:00 FortiAuthenticator radiusd[1617]: (69)   NAS-Port = 1
      2026-06-04T22:15:00.459861+02:00 FortiAuthenticator radiusd[1617]: (69)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.459869+02:00 FortiAuthenticator radiusd[1617]: (69)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.459877+02:00 FortiAuthenticator radiusd[1617]: (69)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.459886+02:00 FortiAuthenticator radiusd[1617]: (69)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.459894+02:00 FortiAuthenticator radiusd[1617]: (69)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.459902+02:00 FortiAuthenticator radiusd[1617]: (69)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.459911+02:00 FortiAuthenticator radiusd[1617]: (69)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.459919+02:00 FortiAuthenticator radiusd[1617]: (69)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.459928+02:00 FortiAuthenticator radiusd[1617]: (69)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.459937+02:00 FortiAuthenticator radiusd[1617]: (69)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.459945+02:00 FortiAuthenticator radiusd[1617]: (69)   Framed-MTU = 1400
      2026-06-04T22:15:00.459956+02:00 FortiAuthenticator radiusd[1617]: (69)   EAP-Message = 0x021d05d40dc000000876140303000101170303086b6db62461bb97a4f122a484ff202fac2136da380f2234edbdc278e704e468092a859d08818e0bb64f42033fbfc397bd5e5e1aca8a0ace9ada2be8a319869ad25b17b76c5e32c9b8c374b62b66f6e0394e7e26b34c8269a7fb4e3e764531d27f66b8d2906971be2afc14a1d4e916c766affe901cc36603065fd2501b4fc3f2c4a20a7210ee7ac40bcec6f1ba80c046875f25780db78b249c5d8d200aa860746f43cd72b7ecc652545a7bd4a935638a5af646e2192c02eb066e1de242b7cf15124f47f55b1c5a9fb7fdffc16e3117d70cde1435b036716d62eda5340818253f4d46ee4712ddc4c99ef1c9348cb713d1e6c67f020157d3ed2dd5d692639e6d99fbaaade2082ee8d28cb39d540db6d24165e93e80127cb17210fb9819ab901e7715ee75eef27a42835948c27c6bf72f6309c2f06e0354cae1bee28fcc4d0c77eb5d0dd5afa5001f99803a2f05538db6cc8cfe9a8f2926f28ada89f3e944c58eaa367e12c5
      2026-06-04T22:15:00.459976+02:00 FortiAuthenticator radiusd[1617]: (69)   State = 0x4089863643948be48ec08401850d17cb
      2026-06-04T22:15:00.459988+02:00 FortiAuthenticator radiusd[1617]: (69) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.460018+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.460027+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.460038+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: ===>Timestamp:1780604100.459620, age:0ms 
      2026-06-04T22:15:00.460055+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.460065+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.460079+02:00 FortiAuthenticator radiusd[1617]: (69) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.460091+02:00 FortiAuthenticator radiusd[1617]: (69) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.460103+02:00 FortiAuthenticator radiusd[1617]: (69) eap: Removing EAP session with state 0x4089863643948be4
      2026-06-04T22:15:00.460112+02:00 FortiAuthenticator radiusd[1617]: (69) eap: Previous EAP request found for state 0x4089863643948be4, released from the list
      2026-06-04T22:15:00.460128+02:00 FortiAuthenticator radiusd[1617]: (69) eap: EAP session adding &reply:State = 0x4089863644978be4
      2026-06-04T22:15:00.460139+02:00 FortiAuthenticator radiusd[1617]: (69) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.460149+02:00 FortiAuthenticator radiusd[1617]: (69)   Framed-MTU = 984
      2026-06-04T22:15:00.460158+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:15:00.460167+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:15:00.460176+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.460185+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:15:00.460193+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:15:00.460202+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.460211+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.460220+02:00 FortiAuthenticator radiusd[1617]: (69)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.460234+02:00 FortiAuthenticator radiusd[1617]: (69) Sent Access-Challenge Id 4 from 192.168.1.206:1812 to 192.168.1.202:23356 length 64
      2026-06-04T22:15:00.460243+02:00 FortiAuthenticator radiusd[1617]: (69)   EAP-Message = 0x011e00060d00
      2026-06-04T22:15:00.460251+02:00 FortiAuthenticator radiusd[1617]: (69)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.460263+02:00 FortiAuthenticator radiusd[1617]: (69)   State = 0x4089863644978be48ec08401850d17cb
      2026-06-04T22:15:00.476273+02:00 FortiAuthenticator radiusd[1617]: (70) Received Access-Request Id 5 from 192.168.1.202:23356 to 192.168.1.206:1812 length 1011
      2026-06-04T22:15:00.476278+02:00 FortiAuthenticator radiusd[1617]: (70)   Message-Authenticator = 0xa85e0cb5813ceadc869b6326a24e6f1a
      2026-06-04T22:15:00.476280+02:00 FortiAuthenticator radiusd[1617]: (70)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.476282+02:00 FortiAuthenticator radiusd[1617]: (70)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.476284+02:00 FortiAuthenticator radiusd[1617]: (70)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.476286+02:00 FortiAuthenticator radiusd[1617]: (70)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.476290+02:00 FortiAuthenticator radiusd[1617]: (70)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.476292+02:00 FortiAuthenticator radiusd[1617]: (70)   Service-Type = Framed-User
      2026-06-04T22:15:00.476294+02:00 FortiAuthenticator radiusd[1617]: (70)   NAS-Port = 1
      2026-06-04T22:15:00.476295+02:00 FortiAuthenticator radiusd[1617]: (70)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.476297+02:00 FortiAuthenticator radiusd[1617]: (70)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.476407+02:00 FortiAuthenticator radiusd[1617]: (70)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.476410+02:00 FortiAuthenticator radiusd[1617]: (70)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.476411+02:00 FortiAuthenticator radiusd[1617]: (70)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.476413+02:00 FortiAuthenticator radiusd[1617]: (70)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.476415+02:00 FortiAuthenticator radiusd[1617]: (70)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.476417+02:00 FortiAuthenticator radiusd[1617]: (70)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.476418+02:00 FortiAuthenticator radiusd[1617]: (70)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.476420+02:00 FortiAuthenticator radiusd[1617]: (70)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.476422+02:00 FortiAuthenticator radiusd[1617]: (70)   Framed-MTU = 1400
      2026-06-04T22:15:00.476426+02:00 FortiAuthenticator radiusd[1617]: (70)   EAP-Message = 0x021e02b20d00af9261939df2ce2116bcb267909b4ce3097462f9fa5e5f5665eb5fb5c6e4a23b7af8c90a058d247e9b5cc831b56a7ed4e6685d4bd9dd3f8b6757d96b03ae05e369436fc23b625a7c5a5f695784d19cebd252a91eb2a005d78030be95d6efd7d7433804939a6b94f7b4e51980ed838b4da1d95f4538718b54966101baf15925c403a7ba80a20abdd5fa6ab17b00d892d13ce6e3b3c6322d60a3eca449b0876de8d1341473e0ec7cf244000db49c641702821314819f1683e1c6bb603d37fac99048b1236ff347cfecff7ba5f3414810a78b61e420a31d4ba7b7155c40e104887200b085107ecb3481a7ff86744b516fad6f56a8229301c33be4ff73a4ab809af6f9cb4f31a6ebeb3bf01572097f0e7f71810b707f0b4b22bb2490fe7a109eef0878c7140c6571ba91a8bc7b9aa113d8706b28201a38e8080ab3980e33f74e4757562cc7a4bdf4c82bf0cd6c341c9ada10c5e130684e20b477d69bc390d6d74e201ee5d9d972bae7b4d08e80a9b49b02c50e
      2026-06-04T22:15:00.476428+02:00 FortiAuthenticator radiusd[1617]: (70)   State = 0x4089863644978be48ec08401850d17cb
      2026-06-04T22:15:00.476495+02:00 FortiAuthenticator radiusd[1617]: (70) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.476518+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.476521+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.476523+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: ===>Timestamp:1780604100.476181, age:0ms 
      2026-06-04T22:15:00.476532+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.476535+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.476540+02:00 FortiAuthenticator radiusd[1617]: (70) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.476545+02:00 FortiAuthenticator radiusd[1617]: (70) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.476550+02:00 FortiAuthenticator radiusd[1617]: (70) eap: Removing EAP session with state 0x4089863644978be4
      2026-06-04T22:15:00.476552+02:00 FortiAuthenticator radiusd[1617]: (70) eap: Previous EAP request found for state 0x4089863644978be4, released from the list
      2026-06-04T22:15:00.477063+02:00 FortiAuthenticator radiusd[1617]: rlm_eap_tls: Certificate passed CRL check.
      2026-06-04T22:15:00.477173+02:00 FortiAuthenticator radiusd[1617]: Waking up in 0.2 seconds.
      2026-06-04T22:15:00.477610+02:00 FortiAuthenticator radiusd[1617]: (70) eap_tls: rlm_eap_tls: Certificate check succeeded. (CN=F3AB0220E981419CAA9A13CE4811E3F9, Issuer=/CN=EMS_SUBCA)
      2026-06-04T22:15:00.477758+02:00 FortiAuthenticator radiusd[1617]: rlm_eap_tls: Certificate passed CRL check.
      2026-06-04T22:15:00.477870+02:00 FortiAuthenticator radiusd[1617]: (70) eap_tls: (TLS) EAP Sending final Commitment Message.
      2026-06-04T22:15:00.477893+02:00 FortiAuthenticator radiusd[1617]: (70) eap: EAP session adding &reply:State = 0x4089863645968be4
      2026-06-04T22:15:00.477901+02:00 FortiAuthenticator radiusd[1617]: (70) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.477923+02:00 FortiAuthenticator radiusd[1617]: (70)   Framed-MTU = 984
      2026-06-04T22:15:00.477928+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, ClientHello"
      2026-06-04T22:15:00.477930+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, ServerHello"
      2026-06-04T22:15:00.477932+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.477934+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions"
      2026-06-04T22:15:00.477936+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest"
      2026-06-04T22:15:00.477938+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.477940+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.478024+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.478033+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 ChangeCipherSpec"
      2026-06-04T22:15:00.478042+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, Certificate"
      2026-06-04T22:15:00.478050+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Client-Cert-Common-Name += "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.478059+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Client-Cert-Issuer += "/CN=EMS_SUBCA"
      2026-06-04T22:15:00.478068+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Client-Cert-X509v3-Authority-Key-Identifier += "7B:90:6C:7C:0F:E9:82:56:B9:CE:C2:CF:91:19:78:28:B5:B0:33:08"
      2026-06-04T22:15:00.478077+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, CertificateVerify"
      2026-06-04T22:15:00.478085+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - recv TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.478094+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Cipher-Suite = "TLS_AES_256_GCM_SHA384"
      2026-06-04T22:15:00.478102+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Version = "TLS 1.3"
      2026-06-04T22:15:00.478118+02:00 FortiAuthenticator radiusd[1617]: (70)   TLS-Session-Information = "(TLS) TLS - send TLS 1.3 Handshake, Finished"
      2026-06-04T22:15:00.478138+02:00 FortiAuthenticator radiusd[1617]: (70) Sent Access-Challenge Id 5 from 192.168.1.206:1812 to 192.168.1.202:23356 length 91
      2026-06-04T22:15:00.478147+02:00 FortiAuthenticator radiusd[1617]: (70)   EAP-Message = 0x011f00210d8000000017170303001240b58b9b82ac678f565d68072899d09e553d
      2026-06-04T22:15:00.478158+02:00 FortiAuthenticator radiusd[1617]: (70)   Message-Authenticator = 0x00000000000000000000000000000000
      2026-06-04T22:15:00.478166+02:00 FortiAuthenticator radiusd[1617]: (70)   State = 0x4089863645968be48ec08401850d17cb
      2026-06-04T22:15:00.482381+02:00 FortiAuthenticator radiusd[1617]: (71) Received Access-Request Id 6 from 192.168.1.202:23356 to 192.168.1.206:1812 length 323
      2026-06-04T22:15:00.482386+02:00 FortiAuthenticator radiusd[1617]: (71)   Message-Authenticator = 0xc67c8744d1e42cd3a7426436aa79f26b
      2026-06-04T22:15:00.482389+02:00 FortiAuthenticator radiusd[1617]: (71)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.482391+02:00 FortiAuthenticator radiusd[1617]: (71)   NAS-IP-Address = 0.0.0.0
      2026-06-04T22:15:00.482393+02:00 FortiAuthenticator radiusd[1617]: (71)   NAS-Identifier = "10.10.10.2:5246-LAB"
      2026-06-04T22:15:00.482395+02:00 FortiAuthenticator radiusd[1617]: (71)   Called-Station-Id = "04-01-A1-60-58-10:LAB"
      2026-06-04T22:15:00.482397+02:00 FortiAuthenticator radiusd[1617]: (71)   NAS-Port-Type = Wireless-802.11
      2026-06-04T22:15:00.482399+02:00 FortiAuthenticator radiusd[1617]: (71)   Service-Type = Framed-User
      2026-06-04T22:15:00.482401+02:00 FortiAuthenticator radiusd[1617]: (71)   NAS-Port = 1
      2026-06-04T22:15:00.482402+02:00 FortiAuthenticator radiusd[1617]: (71)   Fortinet-Vdom-Name = "root"
      2026-06-04T22:15:00.482404+02:00 FortiAuthenticator radiusd[1617]: (71)   Fortinet-SSID = "LAB"
      2026-06-04T22:15:00.482426+02:00 FortiAuthenticator radiusd[1617]: (71)   Fortinet-AP-Name = "FP241KTFSERIAL"
      2026-06-04T22:15:00.482463+02:00 FortiAuthenticator radiusd[1617]: (71)   Calling-Station-Id = "24-41-8C-FB-B1-B6"
      2026-06-04T22:15:00.482472+02:00 FortiAuthenticator radiusd[1617]: (71)   Connect-Info = "CONNECT 0/0Mbps(Tx/Rx) 11N_2G"
      2026-06-04T22:15:00.482481+02:00 FortiAuthenticator radiusd[1617]: (71)   Acct-Session-Id = "6A212F020000002A"
      2026-06-04T22:15:00.482490+02:00 FortiAuthenticator radiusd[1617]: (71)   Acct-Multi-Session-Id = "0B07FEA15ADB8898"
      2026-06-04T22:15:00.482498+02:00 FortiAuthenticator radiusd[1617]: (71)   WLAN-Pairwise-Cipher = 1027076
      2026-06-04T22:15:00.482506+02:00 FortiAuthenticator radiusd[1617]: (71)   WLAN-Group-Cipher = 1027076
      2026-06-04T22:15:00.482515+02:00 FortiAuthenticator radiusd[1617]: (71)   WLAN-AKM-Suite = 1027073
      2026-06-04T22:15:00.482524+02:00 FortiAuthenticator radiusd[1617]: (71)   Framed-MTU = 1400
      2026-06-04T22:15:00.482532+02:00 FortiAuthenticator radiusd[1617]: (71)   EAP-Message = 0x021f00060d00
      2026-06-04T22:15:00.482540+02:00 FortiAuthenticator radiusd[1617]: (71)   State = 0x4089863645968be48ec08401850d17cb
      2026-06-04T22:15:00.482552+02:00 FortiAuthenticator radiusd[1617]: (71) # Executing section authorize from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.482581+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.482589+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: ===>Username:F3AB0220E981419CAA9A13CE4811E3F9 
      2026-06-04T22:15:00.482599+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: ===>Timestamp:1780604100.482304, age:0ms 
      2026-06-04T22:15:00.482614+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: Found authclient from preloaded authclients list for 192.168.1.202: FGT (192.168.1.202) 
      2026-06-04T22:15:00.482624+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.482636+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: Client type: external (subtype: radius) 
      2026-06-04T22:15:00.482648+02:00 FortiAuthenticator radiusd[1617]: (71) # Executing group from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.482658+02:00 FortiAuthenticator radiusd[1617]: (71) eap: Removing EAP session with state 0x4089863645968be4
      2026-06-04T22:15:00.482668+02:00 FortiAuthenticator radiusd[1617]: (71) eap: Previous EAP request found for state 0x4089863645968be4, released from the list
      2026-06-04T22:15:00.482748+02:00 FortiAuthenticator radiusd[1617]: (71) # Executing section post-auth from file /usr/etc/raddb/sites-enabled/default
      2026-06-04T22:15:00.482765+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::Framed-MTU += &session-state:Framed-MTU[*] -> 984
      2026-06-04T22:15:00.482775+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, ClientHello'
      2026-06-04T22:15:00.482784+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, ServerHello'
      2026-06-04T22:15:00.482799+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 ChangeCipherSpec'
      2026-06-04T22:15:00.482808+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, EncryptedExtensions'
      2026-06-04T22:15:00.482817+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, CertificateRequest'
      2026-06-04T22:15:00.482826+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Certificate'
      2026-06-04T22:15:00.482848+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, CertificateVerify'
      2026-06-04T22:15:00.482857+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Finished'
      2026-06-04T22:15:00.482866+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 ChangeCipherSpec'
      2026-06-04T22:15:00.482875+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, Certificate'
      2026-06-04T22:15:00.482884+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Client-Cert-Common-Name += &session-state:TLS-Client-Cert-Common-Name[*] -> 'F3AB0220E981419CAA9A13CE4811E3F9'
      2026-06-04T22:15:00.482895+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Client-Cert-Issuer += &session-state:TLS-Client-Cert-Issuer[*] -> '/CN=EMS_SUBCA'
      2026-06-04T22:15:00.482904+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Client-Cert-X509v3-Authority-Key-Identifier += &session-state:TLS-Client-Cert-X509v3-Authority-Key-Identifier[*] -> '7B:90:6C:7C:0F:E9:82:56:B9:CE:C2:CF:91:19:78:28:B5:B0:33:08'
      2026-06-04T22:15:00.482914+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, CertificateVerify'
      2026-06-04T22:15:00.482923+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - recv TLS 1.3 Handshake, Finished'
      2026-06-04T22:15:00.482931+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Cipher-Suite += &session-state:TLS-Session-Cipher-Suite[*] -> 'TLS_AES_256_GCM_SHA384'
      2026-06-04T22:15:00.482940+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Version += &session-state:TLS-Session-Version[*] -> 'TLS 1.3'
      2026-06-04T22:15:00.482949+02:00 FortiAuthenticator radiusd[1617]: (71)       &reply::TLS-Session-Information += &session-state:TLS-Session-Information[*] -> '(TLS) TLS - send TLS 1.3 Handshake, Finished'
      2026-06-04T22:15:00.482959+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: ========> FACAUTH POSTAUTH 
      2026-06-04T22:15:00.482969+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: ===>NAS IP:192.168.1.202 
      2026-06-04T22:15:00.482980+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: Found authpolicy 'EAP' (id 4) for client:'3' 
      2026-06-04T22:15:00.483265+02:00 FortiAuthenticator radiusd[1617]: (71) facauth: Updated auth log 'F3AB0220E981419CAA9A13CE4811E3F9' for attempt from 192.168.1.202: 802.1x authentication (EAP-TLS) successful 
      2026-06-04T22:15:00.483295+02:00 FortiAuthenticator radiusd[1617]: (71) Sent Access-Accept Id 6 from 192.168.1.206:1812 to 192.168.1.202:23356 length 200
      2026-06-04T22:15:00.483297+02:00 FortiAuthenticator radiusd[1617]: (71)   Message-Authenticator := 0x00
      2026-06-04T22:15:00.483298+02:00 FortiAuthenticator radiusd[1617]: (71)   MS-MPPE-Recv-Key = <<< secret >>>
      2026-06-04T22:15:00.483300+02:00 FortiAuthenticator radiusd[1617]: (71)   MS-MPPE-Send-Key = <<< secret >>>
      2026-06-04T22:15:00.483301+02:00 FortiAuthenticator radiusd[1617]: (71)   EAP-Message = 0x031f0004
      2026-06-04T22:15:00.483303+02:00 FortiAuthenticator radiusd[1617]: (71)   User-Name = "F3AB0220E981419CAA9A13CE4811E3F9"
      2026-06-04T22:15:00.483305+02:00 FortiAuthenticator radiusd[1617]: (71)   Framed-MTU += 984

      Something on the side that cost me more time than I want to admit: I was using an out-of-band connection for the Windows 11 client and was connected via RDP to it so I could test the wired 802.1X configuration from a different room, and I constantly got authentication failures displayed on the NIC. I checked the Windows Event Viewer and saw the following error message on every authentication attempt:

      Event ID 15514
      
      Wired 802.1X Authentication failed.
      
      	Network Adapter: Intel(R) Ethernet Connection (6) I219-V
      	Interface GUID: {c8f64a9d-a891-4332-9e1e-1430095beadf}
      	Peer Address: 000000000000
      	Local Address: A4BB6D13072B
      	Connection ID: 0xf
      	Identity: -
      	User: -
      	Domain: -
      	Reason: 0x50001
      	Reason Text: Unable to identify a user for 802.1X authentication
      	Error Code: 0x525

      I looked for a long time to find out why a user couldn’t be found for 802.1X authentication, because, in theory, everything is in order. I then physically went to the client, and the authentication immediately worked. I went back to my office, connected via RDP and was met with the same failure message. Turns out that 802.1X EAP-TLS with user authentication doesn’t work when you are connected via RDP. Maybe there is a setting somewhere that allows this, but it’s such an edge case that I didn’t want to spend more time on it.

      [Update 2026-06-19]: I recently found the reason, and 802.1X user authentication not working in an RDP session is expected and documented.

      For mobile devices

      If you have managed mobile devices, you might be able to use the EMS MDM Integration option to let your Android or iOS devices get a certificate from EMS if they are managed by one of the supported options.

      MDM Integration option

      In such a scenario, EMS acts as an SCEP server, reachable over TCP/4001 and TCP/4002, where mobile devices can request a certificate.

      Once you have the certificate, you can do much the same as with regular clients, so VPN and 802.1X.

      DPI is also possible, of course. If you can install the CA using your MDM, that saves you from having to do it manually using the certificates FortiClient gets from EMS.

      Covering this topic could be a post in itself, so I will leave you with the documentation for now.

      MDM Integration | FortiClient 7.4.7 | Fortinet Document Library

      Wrapping up

      Using EMS user certificates outside of their intended use, that is, ZTNA connections, is a niche use case, and provisioning certificates using other methods can be a better option, but if it fits your use case, this is a good alternative.
      The idea for this post actually came from a talk I had with Manuel Lehner from Fortinet at an event. He brainstormed some stuff with me, and I thought the topic was interesting enough to see how everything behaves. I am glad that I did, because I definitely learned something from this, and I hope you, dear reader, were able to take something of value with you while reading this.

    • The FortiGate Ansible best practices baseline configurator

      Back when I initially published my FortiGate best practices baseline, I had already planned to create configurators to more easily deploy what the baseline recommends. Due to time constraints, this wasn’t possible, however. I have now gotten around to taking the first step by publishing the FortiGate Ansible best practices baseline configurator on GitHub. The README.md on GitHub should be enough to get someone familiar with Ansible started using the configurator.

      The configurator takes care of most of the baseline, but some topics cannot be covered, either because of limitations of the Ansible modules or because additional information is required. These things have to be handled manually, but I am still happy with what I was able to accomplish so far with it.

      If you, dear reader, have any input for the configurator or the baseline in general, please don’t hesitate to bring it up. The best way is to create an issue on GitHub.

      Looking ahead

      As I wrote in the GitHub project README.md, more configurators are planned, but that will take some more time. For now, dear reader, please check out what the Ansible configurator can do.

    • FortiGate onboard automation: Stitches, auto-scripts, batch mode

      A FortiGate comes with several tools built in to help us with automation and responding to events. In this post, I want to explore these options a bit more and show what you can do with them.

      So, dear reader, let’s see what we can do with these tools!

      The setup:

      • 2x FortiGate 70G running 7.6.6
      • A FortiAnalyzer VM running 7.6.6

      Automation stitches

      The biggest, most powerful, and most dynamic option a FortiGate offers are automation stitches. You combine actions, which do something, and triggers, which respond to something, in a stitch, where you handle the logic and the flow, like sequential and parallel execution of actions and delays.

      Before I get more into this part, please check out Yuri Slobodyanyuk’s blog post on automation stitches because he already did a lot of work on this topic.

      Automation actions

      Actions in stitches are the things actually being done. You can send notifications to Slack, Teams, send mails, trigger functions in cloud environments, execute CLI scripts, or execute webhooks.

      There isn’t much to say about actions because they are rather self-explanatory, but one thing I want to mention is that CLI scripts are limited to 1023 characters, and you can use some variables in them.

      FortiGate automation action help

      Automation triggers

      Triggers are the second thing we need to care about, because if we don’t know what event interests us, we won’t do anything in response.

      FortiOS comes with some default triggers, like Configuration Change, HA Failover, and Conserve Mode, but I will focus on FortiOS Event Log, FortiAnalyzer Event Handler, and Incoming Webhook, because they offer the most flexibility.

      FortiOS Event Log triggers

      You cannot trigger on every event log, but the ones we can choose from are vast and knowing how to work with them is important.

      Let’s take a look at two practical examples.

      Example: Link monitor events

      First, assume that you are monitoring a server using a link monitor and you need to respond to it being down, like enabling a static route.

      The Link monitor status event produces the following log entry:

      date=2026-05-02 time=16:19:29 eventtime=1777738768978397230 logid="0100022922" type="event" subtype="system" level="notice" vd="root" logdesc="Link monitor status" name="WIN-AD" msg="Link monitor state is changed from 0 to 1, please check if this triggers HA failover."
      Link monitor log details

      In this case, it is a down event (changed from 0 to 1). There is also a Link monitor status warning event, which makes this particular scenario clearer and easier to work with, but I won’t focus on that for a reason I will get to later.

      In the log details, we can see the message (msg) and the link monitor name (name). This information is enough to build an automation trigger specific to this link monitor using Field filters.

      Link monitor down trigger
      config system automation-trigger
      	edit "LINK-MONITOR-DOWN"
              set event-type event-log
              set logid 22922
              config fields
                  edit 1
                      set name "name"
                      set value "WIN-AD"
                  next
                  edit 2
                      set name "msg"
                      set value "*from 0 to 1*"
                  next
              end
          next
      end

      The wildcard symbol * does the magic here, because it allows us to focus on only what we need in the message and the name field covers the specific link monitor.

      Use the trigger in a stitch and add whatever action you need. You can also build the reverse trigger, i.e. a link monitor coming alive, using the same event and name, but the message *from 1 to 0*.

      The reason I am not using the Link monitor status warning event here is that a dead link monitor uses the warning event (ID 0100022932) in addition to the Link monitor status event (ID 0100022922). A link monitor coming alive uses only Link monitor status (ID 0100022922). I don’t want to use two different IDs for one process (link monitor alive and dead), but you can choose to do that.

      Here are all the log messages for these scenarios:

      date=2026-05-02 time=16:19:41 eventtime=1777738780479592552 logid="0100022922" type="event" subtype="system" level="notice" vd="root" logdesc="Link monitor status" name="WIN-AD" msg="Link monitor state is changed from 1 to 0, please check if this triggers HA failover."
      date=2026-05-02 time=16:19:41 eventtime=1777738780479544752 logid="0100022922" type="event" subtype="system" level="notice" vd="root" logdesc="Link monitor status" name="WIN-AD" interface="undefined" probeproto="ping" msg="Link Monitor changed state from dead to alive, protocol: ping."
      date=2026-05-02 time=16:19:29 eventtime=1777738768978397230 logid="0100022922" type="event" subtype="system" level="notice" vd="root" logdesc="Link monitor status" name="WIN-AD" msg="Link monitor state is changed from 0 to 1, please check if this triggers HA failover."
      date=2026-05-02 time=16:19:29 eventtime=1777738768978357571 logid="0100022932" type="event" subtype="system" level="warning" vd="root" logdesc="Link monitor status warning" name="WIN-AD" interface="undefined" probeproto="ping" msg="Link Monitor changed state from alive to dead, protocol: ping."
      Link monitor log entries

      Example: BGP neighborship events

      The second example is around BGP neighborships. Assume that you want to create a trigger for a specific BGP neighbor going down and enable a static route if such an event happens. If we look at the logs for a BGP neighborship down event, we see the following:

      date=2026-05-02 time=16:23:18 eventtime=1777738998290854913 logid="0103020300" type="event" subtype="router" level="warning" vd="root" logdesc="BGP neighbor status changed" msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 198.51.100.1 Down BGP Notification FSM-ERR"
      BGP down log detail

      In this case, we only have the message to work with, so a trigger to respond to such an event can look like this:

      BGP neighbor down trigger
      config system automation-trigger
          edit "BGP-DOWN"
              set event-type event-log
              set logid 20300
              config fields
                  edit 1
                      set name "msg"
                      set value "*neighbor 198.51.100.1 Down*"
                  next
              end
          next
      end

      A full debug of the event being generated and the automation stitch firing can be seen here (in my case, I am simply disabling port2):

      BGP down event debug

      autod(pid:1622) log packet: total sz:470 data sz:230 fld_num:8
      
      autod(pid:1622) log datetime: 2026-05-01 11:56:27
      
      autod(pid:1622) log header: logid:20300 vfid:0 sever:4 cat:1 subcat:3 key:0 flags:0404 reqlen:110 timestamp:1777636586845355006
      fields:
                      id:10 name:(9)eventtime value:(19)1777636586845355006
                      id:2 name:(5)logid value:(10)0103020300
                      id:3 name:(4)type value:(5)event
                      id:4 name:(7)subtype value:(6)router
                      id:5 name:(5)level value:(7)warning
                      id:6 name:(2)vd value:(4)root
                      id:38 name:(7)logdesc value:(27)BGP neighbor status changed
                      id:24 name:(3)msg value:(80)BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 198.51.100.1 Down BGP Notification FSM-ERR
      pid:1622-__subscr_close_cur_pkg()-141: close package size:1536 logs:2
      __action_cli_script_open()-169: cli script action:PORT2-DOWN is called. svc ctx:0x559d57c000
      accprof:super_admin script:
      config system interface
      edit port2
      set status down
      next
      end
      
      __read_cli_script_result()-117: cli script:
      autod.10
       output:
      
      ========== #1, 2026-05-01 11:56:27 ==========
      70G-02  config system interface
      70G-02 (interface)  edit port2
      70G-02 (port2)  set status down
      70G-02 (port2)  next
      70G-02 (interface)  end
      
      ======= end of #1, 2026-05-01 11:56:27 ======
      
      
      __action_cli_script_close()-219: cli script action is done. script:
      config system interface
      edit port2
      set status down
      next
      end
      output:
      
      ========== #1, 2026-05-01 11:56:27 ==========
      70G-02  config system interface
      70G-02 (interface)  edit port2
      70G-02 (port2)  set status down
      70G-02 (port2)  next
      70G-02 (interface)  end
      
      ======= end of #1, 2026-05-01 11:56:27 ======
      
      
      pid:1622-__handle_msg()-428: Subscriber:4 received package. pubid:0 pkgid:1206 pkg_index:0
      pid:1622-__handle_pkg_logs()-370: Subscriber:4 processing package size:2253 logs:3 pickup:3
      
      autod(pid:1622) log packet: total sz:1194 data sz:252 fld_num:12
      
      autod(pid:1622) log datetime: 2026-05-01 11:56:28
      
      autod(pid:1622) log header: logid:46600 vfid:0 sever:5 cat:1 subcat:0 key:0 flags:0484 reqlen:131 timestamp:1777636587533074284
      fields:
                      id:10 name:(9)eventtime value:(19)1777636587533074284
                      id:2 name:(5)logid value:(10)0100046600
                      id:3 name:(4)type value:(5)event
                      id:4 name:(7)subtype value:(6)system
                      id:5 name:(5)level value:(6)notice
                      id:6 name:(2)vd value:(4)root
                      id:38 name:(7)logdesc value:(27)Automation stitch triggered
                      id:321 name:(6)stitch value:(8)BGP-DOWN
                      id:322 name:(7)trigger value:(8)BGP-DOWN
                      id:377 name:(12)stitchaction value:(10)PORT2-DOWN
                      id:226 name:(4)from value:(3)log
                      id:24 name:(3)msg value:(29)stitch:BGP-DOWN is triggered.
      
      autod(pid:1622) log packet: total sz:626 data sz:298 fld_num:14
      
      autod(pid:1622) log datetime: 2026-05-01 11:56:28
      
      autod(pid:1622) log header: logid:44547 vfid:0 sever:6 cat:1 subcat:0 key:0 flags:0404 reqlen:180 timestamp:1777636587767735898
      fields:
                      id:10 name:(9)eventtime value:(19)1777636587767735898
                      id:2 name:(5)logid value:(10)0100044547
                      id:3 name:(4)type value:(5)event
                      id:4 name:(7)subtype value:(6)system
                      id:5 name:(5)level value:(11)information
                      id:6 name:(2)vd value:(4)root
                      id:38 name:(7)logdesc value:(27)Object attribute configured
                      id:57 name:(2)ui value:(11)auto-script
                      id:12 name:(6)action value:(4)Edit
                      id:59 name:(6)cfgtid value:(9)600113152
                      id:62 name:(7)cfgpath value:(16)system.interface
                      id:63 name:(6)cfgobj value:(5)port2
                      id:64 name:(7)cfgattr value:(16)status[up->down]
                      id:24 name:(3)msg value:(27)Edit system.interface port2
      
      autod(pid:1622) log packet: total sz:433 data sz:193 fld_num:8
      
      autod(pid:1622) log datetime: 2026-05-01 11:56:28
      
      autod(pid:1622) log header: logid:32549 vfid:0 sever:6 cat:1 subcat:0 key:0 flags:0404 reqlen:67 timestamp:1777636587778632118
      fields:
                      id:10 name:(9)eventtime value:(19)1777636587778632118
                      id:2 name:(5)logid value:(10)0100032549
                      id:3 name:(4)type value:(5)event
                      id:4 name:(7)subtype value:(6)system
                      id:5 name:(5)level value:(11)information
                      id:6 name:(2)vd value:(4)root
                      id:38 name:(7)logdesc value:(29)Autoscript stop automatically
                      id:24 name:(3)msg value:(37)script autod.10 stopped automatically
      

      You can also create a reverse trigger and use the BGP neighbor up message.

      Here are all the log messages for these scenarios:

      date=2026-05-02 time=16:23:47 eventtime=1777739026657108610 logid="0103020300" type="event" subtype="router" level="warning" vd="root" logdesc="BGP neighbor status changed" msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 198.51.100.1 Up "
      date=2026-05-02 time=16:23:47 eventtime=1777739026657075206 logid="0103020300" type="event" subtype="router" level="warning" vd="root" logdesc="BGP neighbor status changed" msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 198.51.100.1 Down Peer being deleted"
      date=2026-05-02 time=16:23:47 eventtime=1777739026657040239 logid="0103020304" type="event" subtype="router" level="warning" vd="root" logdesc="Routing log warning" msg="BGP: %BGP-3-NOTIFICATION: sending to 198.51.100.1 6/0 (CeaseUnspecified Error Subcode) 0 data-bytes []"
      date=2026-05-02 time=16:23:18 eventtime=1777738998290854913 logid="0103020300" type="event" subtype="router" level="warning" vd="root" logdesc="BGP neighbor status changed" msg="BGP: %BGP-5-ADJCHANGE: VRF 0 neighbor 198.51.100.1 Down BGP Notification FSM-ERR"
      date=2026-05-02 time=16:23:18 eventtime=1777738998290842322 logid="0103020304" type="event" subtype="router" level="warning" vd="root" logdesc="Routing log warning" msg="BGP: %BGP-3-NOTIFICATION: received from 198.51.100.1 4/0 (Hold Timer Expired/Unspecified Error Subcode) 0 data-bytes []"
      BGP Up/Down log entries
      config system automation-trigger
          edit "BGP-UP"
              set event-type event-log
              set logid 20300
              config fields
                  edit 1
                      set name "msg"
                      set value "*neighbor 198.51.100.1 Up*"
                  next
              end
          next
      end

      Incoming Webhook

      This option is powerful, but boring. You create the trigger, and you get a URL to send HTTP POST requests to. Moving on… 

      An automation debugging aside

      If you ever need to debug automation stitches, you do that using the autod application and a triggered stitch looks like this:

      Automation stitch executed

      __action_cli_script_open()-169: cli script action:PORT2-DOWN is called. svc ctx:0x55a8894cf0
      accprof:super_admin script:
      config system interface
      edit port2
      set status down
      next
      end
      
      __read_cli_script_result()-117: cli script:
      autod.0
       output:
      
      ========== #1, 2026-05-02 17:28:27 ==========
      70G-02  config system interface
      70G-02 (interface)  edit port2
      70G-02 (port2)  set status down
      70G-02 (port2)  next
      70G-02 (interface)  end
      
      ======= end of #1, 2026-05-02 17:28:27 ======
      
      
      __action_cli_script_close()-219: cli script action is done. script:
      config system interface
      edit port2
      set status down
      next
      end
      output:
      
      ========== #1, 2026-05-02 17:28:27 ==========
      70G-02  config system interface
      70G-02 (interface)  edit port2
      70G-02 (port2)  set status down
      70G-02 (port2)  next
      70G-02 (interface)  end
      
      ======= end of #1, 2026-05-02 17:28:27 ======

      Some more options regarding debugging are in Yuri’s blog post (again, check it out), like testing, statistics, etc.

      One thing I think is interesting is the diagnose test application autod 1 option. In Yuri’s blog, the description is “Enable automation stitches logging.” but the description of the option is actually “Enable/disable log dumping”, and this really does mean that. It dumps all logs straight to the CLI.

      Turn it on, and you get a live feed of every log being created, and since not only the examples of link monitors or BGP events create log entries, but also configuration changes, we can get some nice information quickly. Here is the output of a new firewall policy being created, as well as the regular log entry, for example:

      autod(pid:1622) log header: logid:44547 vfid:0 sever:6 cat:1 subcat:0 key:0 flags:0404 reqlen:359 timestamp:1777637024413047347
      fields:
                      id:10 name:(9)eventtime value:(19)1777637024413047347
                      id:2 name:(5)logid value:(10)0100044547
                      id:3 name:(4)type value:(5)event
                      id:4 name:(7)subtype value:(6)system
                      id:5 name:(5)level value:(11)information
                      id:6 name:(2)vd value:(4)root
                      id:38 name:(7)logdesc value:(27)Object attribute configured
                      id:22 name:(4)user value:(5)admin
                      id:57 name:(2)ui value:(20)https(192.168.1.101)
                      id:12 name:(6)action value:(3)Add
                      id:59 name:(6)cfgtid value:(8)96927855
                      id:58 name:(4)uuid value:(36)cc0608f6-4555-51f1-0501-0fe328bf84a4
                      id:62 name:(7)cfgpath value:(15)firewall.policy
                      id:63 name:(6)cfgobj value:(1)6
                      id:64 name:(7)cfgattr value:(150)name[DEBUG-LOG-POLICY]srcintf[port1]dstintf[SDWAN-OUTSIDE]action[accept]srcaddr[all]dstaddr[all]schedule[always]service[ALL]logtraffic[all]nat[enable]
                      id:24 name:(3)msg value:(21)Add firewall.policy 6
      New policy creation log

      Cool, right?

      FortiAnalyzer Event Handlers

      The FortiOS Event Log triggers are nice, but they aren’t flexible. If there is no log for the scenario you need, if you need something more specific, or if you need some logic, they hit their limit. FortiAnalyzer takes care of this. Now, this is not strictly “onboard” because you do need another product, but I would be remiss not to mention it.

      If you integrate FortiAnalyzer as a Fabric Connector, you can leverage the Event Handlers you create on FortiAnalyzer as a trigger on a FortiGate. Let’s take a practical example.

      FortiAnalyzer comes with the Default-Brute-Force-Account-Login-Attack-FGT event handler, which does exactly what it says. It recognizes brute force administrator logins on a FortiGate. It uses a correlation sequence, where failed logins are checked, and if they aren’t followed by a successful login after 5 minutes, the event triggers.

      If we want to use this event handler on our FortiGate, we clone it, because event handlers of the Built-in origin cannot be used for this, enable the Automation Stitch option, change the Threshold Duration to be the same as the NOT_FOLLOWED_BY value and then we go onto the FortiGate and create our trigger.

      FortiAnalyzer brute force login event handler
      FortiAnalyzer Event Handler trigger
      config system automation-trigger
          edit "FAZ-BRUTE-FORCE"
              set event-type faz-event
              set faz-event-name "Custom-Brute-Force-Account-Login-Attack-FGT"
          next
      end

      Debugging this communication is done on FortiAnalyzer using the oftpd application, like such (192.168.1.201 is my FortiGate):

      FAZVM64 # diagnose debug application oftpd 8 192.168.1.201
      oftpd debug filter:     filter(string)==192.168.1.201
      FAZVM64 # diagnose debug enable

      The full debugs for such an event handler triggering, with the regular communication removed, look like this:

      FortiAnalyzer oftpd debug output

      [T2313:oftp_restapi_util.c:1110] [FGT70GSERIAL] OFTP_RESTAPI_GENERIC_REQ sched success, uuid=e0a31dd8-4647-11f1-8ce6-000c29563bff, u
      rl=/api/v2/monitor/log/event/generate?vdom=root, data={ "log_type": "security-event", "fields": { "ackflag": "no", "alertid": "202605021
      000010016", "logcount": "5", "alerttime": "1777740995", "devid": "FGT70GSERIAL", "devname": "70G-01", "eventtype": "", "groupby1": "
      FGT70GSERIAL", "groupby2": "", "groupby3": "", "readflag": "no", "severity": "medium", "subject": "Brute force account login attack
      on FGT device FGT70GSERIAL detected", "tag": "Credential Access,login,attack", "triggername": "Custom-Brute-Force-Account-Login-Atta
      ck-FGT", "vdom": "root", "epid": "3", "euid": "3", "epip": "192.168.1.231", "srcip": "192.168.1.231", "dstip": "192.168.1.201", "epname"
      : "192.168.1.231", "euname": "N\/A", "extrainfo": "", "ephostname": "", "epmac": "", "eposname": "", "eposversion": "", "fctuid": "", "m
      itre_info": "{\"enterprise\": [\"T1110.001\"]}" } }.
      
      [T2320:oftp_restapi.c:592] [FGT70GSERIAL] req :
      POST /api/v2/monitor/log/event/generate?vdom=root HTTP/1.1
      Host: 127.0.0.1
      Connection: close
      User-Agent: FortiAnalyzer/7.6 (FortiAnalyzer-VM64; FAZ-VMTMSERIAL; v7.6.6-build3654 260127 (GA.M))
      Accept: */*
      Cookie: __fake_cookie_authorized_device;
      X-CSRFTOKEN: __fake_cookie_authorized_device
      Content-Length: 813
      Content-Type: application/x-www-form-urlencoded
      
      { "log_type": "security-event", "fields": { "ackflag": "no", "alertid": "202605021000010016", "logcount": "5", "alerttime": "1777740995"
      , "devid": "FGT70GSERIAL", "devname": "70G-01", "eventtype": "", "groupby1": "FGT70GSERIAL", "groupby2": "", "groupby3": "", "re
      adflag": "no", "severity": "medium", "subject": "Brute force account login attack on FGT device FGT70GSERIAL detected", "tag": "Cred
      ential Access,login,attack", "triggername": "Custom-Brute-Force-Account-Login-Attack-FGT", "vdom": "root", "epid": "3", "euid": "3", "ep
      ip": "192.168.1.231", "srcip": "192.168.1.231", "dstip": "192.168.1.201", "epname": "192.168.1.231", "euname": "N\/A", "extrainfo": "",
      "ephostname": "", "epmac": "", "eposname": "", "eposversion": "", "fctuid": "", "mitre_info": "{\"enterprise\": [\"T1110.001\"]}" } }
      
      [T2301:oftp_restapi_resp.c:1946] [FGT70GSERIAL] OFTP_TASK_T_GENERIC_RESP uuid=e0a31dd8-4647-11f1-8ce6-000c29563bff, url=/api/v2/moni
      tor/log/event/generate?vdom=root, req_data={ "log_type": "security-event", "fields": { "ackflag": "no", "alertid": "202605021000010016",
       "logcount": "5", "alerttime": "1777740995", "devid": "FGT70GSERIAL", "devname": "70G-01", "eventtype": "", "groupby1": "FGT70GSERIAL",
       "groupby2": "", "groupby3": "", "readflag": "no", "severity": "medium", "subject": "Brute force account login attack on FGT devi
      ce FGT70GSERIAL detected", "tag": "Credential Access,login,attack", "triggername": "Custom-Brute-Force-Account-Login-Attack-FGT", "v
      dom": "root", "epid": "3", "euid": "3", "epip": "192.168.1.231", "srcip": "192.168.1.231", "dstip": "192.168.1.201", "epname": "192.168.
      1.231", "euname": "N\/A", "extrainfo": "", "ephostname": "", "epmac": "", "eposname": "", "eposversion": "", "fctuid": "", "mitre_info":
       "{\"enterprise\": [\"T1110.001\"]}" } }

      First, we get the requests for a REST API action, which includes all information from the log that generated the event.

      Then we get the actual HTTP POST request towards the FortiGate, which includes the log information in JSON.

      Lastly, we get the response to the task.

      On the FortiGate, this communication can be seen in the httpsd application, because FortiAnalyzer sends a POST request after all.

      Enable the debugs and check the output.

      diagnose debug application httpsd -1
      diagnose debug enable
      
      [httpsd 4590 - 1777740996     info] fweb_debug_init[614] -- New POST request for "/api/v2/monitor/log/event/generate" from "/tmp/httpsd-faz.sock:0"
      [httpsd 4590 - 1777740996     info] fweb_debug_init[617] -- User-Agent: "FortiAnalyzer/7.6 (FortiAnalyzer-VM64; FAZ-VMTMSERIAL; v7.6.6-build3654 260127 (GA.M))"
      [httpsd 4590 - 1777740996     info] fweb_debug_init[622] -- Handler "api_monitor_v2-handler" assigned to request
      [httpsd 4590 - 1777740996     info] api_access_check_for_http_authd_session[51] -- Session key request authorized for daemon_admin.
      [httpsd 4590 - 1777740996     info] api_store_parameter[298] -- add API parameter 'vdom' (type=string)
      [httpsd 4590 - 1777740996     info] api_store_parameter[298] -- add API parameter 'vdom' (type=string)
      [httpsd 4590 - 1777740996     info] api_store_parameter[298] -- add API parameter 'log_type' (type=string)
      [httpsd 4590 - 1777740996     info] api_store_parameter[298] -- add API parameter 'fields' (type=object)
      [httpsd 4590 - 1777740996     info] api_endpoint_execute_handler[922] -- new API request (action='generate',path='log',name='event',vdom='root',user='daemon_admin')
      [httpsd 4590 - 1777740996     info] generate_event[1026] -- Created an internal event: ackflag="no" alertid="202605021000010016" logcount="5" alerttime="1777740995" devid="FGT70GSERIAL" devname="70G-01" eventtype="" groupby1="FGT70GSERIAL" groupby2="" groupby3="" readflag="no" severity="medium" subject="Brute force account login attack on FGT device FGT70GSERIAL detected" tag="Credential Access,login,attack" triggername="Custom-Brute-Force-Account-Login-Attack-FGT" vdom="root" epid="3" euid="3" epip="192.168.1.231" srcip="192.168.1.231" dstip="192.168.1.201" epname="192.168.1.231" euname="N\/A" extrainfo="" ephostname="" epmac="" eposname="" eposversion="" fctuid="" mitre_info="{\"enterprise\": [\"T1110.001\"]}"
      [httpsd 4590 - 1777740996     info] api_endpoint_execute_handler[937] -- completed API request (mem_start=55268, mem_end=55332, mem_change=64)
      [httpsd 4590 - 1777740996     info] fweb_debug_final[455] -- Completed POST request for "/api/v2/monitor/log/event/generate" (HTTP 200)

      There isn’t a lot going on here that we haven’t already seen, but we know it’s there.

      A FortiGate dynamically pulls the available event handlers every time you try to select one in the trigger, by the way. This is done using an OFTP JSONRPC request. This also shows up on FortiAnalyzer in the oftpd debugs:

      FortiGate requesting FortiAnalyzer Event Handlers

      [T2321:oftps.c:2148 :192.168.1.201] SSL clienthello incoming on sockfd[26]
      [T2321:oftps.c:1436 :192.168.1.201] dft-idx=0 inited=1.
      [T2321:oftps.c:1927 :192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] SSL_new() success.
      [T2321:oftps.c:1804 :192.168.1.201] ssl verify peer cert
      [T2321:oftps.c:1826 :192.168.1.201] Peer is using a fortinet certificate. ON=Fortinet
      [T2321:oftps.c:1839 :192.168.1.201] Peer cert info, CommonName(CN=FGT70GSERIAL).
      [T2321:oftps.c:2160 :192.168.1.201] SSL_accept one client SUCCESS [ protocol : (772) TLS 1.3  ]
      [T2321:oftps.c:2199 :192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] SSL_accepted
      [T2322:oftps.c:2257 :192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] received [512] bytes:
      [T2322:oftps.c:2257 :192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] received [83] bytes:
      [T2322:main.c:5147 :192.168.1.201] handle LOGIN_REQUEST_LEGACY
      [T2323:login.c:3539 :192.168.1.201] login-ver: 0.0
      [T2323:login.c:3443 :192.168.1.201] host = 'FGT-70G01'
      [T2323:login.c:3489 :192.168.1.201] Version: FortiGate-70G v8.0.0,build0167,260420 (GA.F)
      Virus-DB: 1.00001(2026-04-02 13:48)
      IPS-DB: 36.00208(2026-04-23 01:22)
      APP-DB: 36.00206(2026-04-21 03:52)
      FMWP-DB: 26.00040(2026-04-10 09:35)
      Industrial-DB: 6.00741(2015-12-01 02:30)
      Serial-Number: FGT70GSERIAL
      Virtual domain configuration: disable
      Current HA mode: a-p
      Current HA group: labcluster
      [T2323:login.c:348 :192.168.1.201] os_type(0) os_ver(8) mr(0) patch(0) build(167) beta(-1)
      [T2323:login.c:398 :192.168.1.201] ha_group_name:labcluster, ha_mode:1.
      [T2323:login.c:3448 :192.168.1.201] vdom = 1
      [T2323:login.c:3468 :192.168.1.201] tz-offset = 7200, tz-name = Europe/Vienna
      [T2323:oftps.c:2323 FGT70GSERIAL:192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] sent [103] bytes:
      [T2323:login.c:3943 FGT70GSERIAL:192.168.1.201] login succeed
      [T2322:oftps.c:2257 FGT70GSERIAL:192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] received [236] bytes:
      [T2322:main.c:5147 FGT70GSERIAL:192.168.1.201] handle RESTAPI REQUEST
      [T2329:oftp_svc_fwd.c:812 FGT70GSERIAL:192.168.1.201] handle_oftp_jsonrpc_request entry
      [T2329:oftp_svc_fwd.c:726 FGT70GSERIAL:192.168.1.201] cannot find display-timezone for FGT70GSERIAL
      [T2329:oftp_svc_fwd.c:898 FGT70GSERIAL:192.168.1.201] sent req={ "id": 1777742118, "params": [ { "url": "eventmgmt\/dev\/FGT70GSERIAL
      \/vdom\/root\/config\/trigger", "apiver": 3, "filter": [ "handlertype", "==", "handler-type-remote" ] } ], "jsonrpc": "2.0", "metho
      d": "get" }
       response '{ "jsonrpc": "2.0", "id": 1777742118, "result": { "status": { "code": 0, "message": "OK" }, "data": [ { "auto-raise-incident"
      : 0, "automation-stitch": 1, "creation-time": 0, "description": "Default event handler to detect botnet communication and report to Fort
      iGate", "enable": 1, "enable-time": 0, "handler-id": "30000", "mitre-domain": "enterprise", "mitre-info": "{\"enterprise\": [\"T1584.005
      \", \"T1071\"]}", "mitre-tech-id": "T1584.005,T1071", "name": "Default-Botnet-Communication-Detection", "protected": 1, "rule": [ { "agg
      regate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${dstip}:${dstpor
      t}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "filter-expr": "logid==0202009248", "filter-relation": 0, "groupby1": "endp
      oint", "groupby2": "virus", "groupby3": null, "indicator": null, "logtype": "virus", "name": "Traffic to Botnet CnC blocked in virus log
      ", "risk-severity": 4, "rule-id": "1", "severity": 1, "subject": "Traffic to Botnet C&C $groupby2 blocked", "tags": "Default,Botnet,IP,C
      &C", "thres-duration": 1440, "utmevent": null }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "eventstatus": "open",
      "eventtype": null, "extrainfo": "C&C: ${dstip}:${dstport}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "filter-expr": "logi
      d==0202009249", "filter-relation": 0, "groupby1": "endpoint", "groupby2": "virus", "groupby3": null, "indicator": null, "logtype": "viru
      s", "name": "Traffic to Botnet CnC detected in virus log", "risk-severity": 4, "rule-id": "2", "severity": 0, "subject": "Traffic to Bot
      net C&C $groupby2 detected", "tags": "Default,Botnet,IP,C&C", "thres-duration": 1440, "utmevent": null }, { "aggregate-expr": "COUNT(*)
      >= 1", "devtype": 0, "enable": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "Traffic path: ${devname} (Policy ID:${policyid
      })\\${dstintf}\\${dstip}", "extrainfo-type": 1, "filter": null, "filter-expr": "logid==1501054601 OR logid==1501054600", "filter-relatio
      n": 0, "groupby1": "endpoint", "groupby2": "qname", "groupby3": null, "indicator": null, "logtype": "dns", "name": "DNS traffic to Botne
      t CnC blocked", "risk-severity": 4, "rule-id": "3", "severity": 1, "subject": "DNS traffic to Botnet C&C $groupby2 blocked", "tags": "De
      fault,Botnet,Domain,C&C", "thres-duration": 1440, "utmevent": null }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "e
      ventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${dstip}:${dstport}, Traffic path: ${devname} (Policy ID:${policyid})\\${dsti
      ntf}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "filter-expr": "attack ~ Botnet and direction=incoming and (action=='dete
      cted' or action=='pass session')", "filter-relation": 0, "groupby1": "endpoint", "groupby2": "attack", "groupby3": null, "indicator": nu
      ll, "logtype": "ips", "name": "Traffic to Botnet CnC detected in ips log 1", "risk-severity": 4, "rule-id": "4", "severity": 0, "subject
      ": "Traffic to Botnet C&C $groupby2 detected", "tags": "Default,Botnet,Signature,C&C,Incoming", "thres-duration": 1440, "utmevent": null
       }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${dstip
      }:${dstport}, Traffic path: ${devname} (Policy ID:${policyid})\\${dstintf}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "fi
      lter-expr": "attack ~ Botnet and direction=incoming and action!='detected' and action!='pass session'", "filter-relation": 0, "groupby1"
      : "endpoint", "groupby2": "attack", "groupby3": null, "indicator": null, "logtype": "ips", "name": "Traffic to Botnet CnC blocked in ips
       log 1", "risk-severity": 4, "rule-id": "5", "severity": 1, "subject": "Traffic to Botnet C&C $groupby2 blocked", "tags": "Default,Botne
      t,Signature,C&C,Incoming", "thres-duration": 1440, "utmevent": null }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "
      eventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${srcip}:${srcport}, Traffic path: ${devname} (Policy ID:${policyid})\\${src
      intf}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "filter-expr": "attack ~ Botnet and direction=outgoing and (action=='det
      ected' or action=='pass session')", "filter-relation": 0, "groupby1": "dstendpoint", "groupby2": "attack", "groupby3": null, "indicator"
      : null, "logtype": "ips", "name": "Traffic from Botnet CnC detected in ips log", "risk-severity": 4, "rule-id": "6", "severity": 0, "sub
      ject": "Traffic from Botnet C&C $groupby2 detected", "tags": "Default,Botnet,Signature,C&C,Outgoing", "thres-duration": 1440, "utmevent"
      : null }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "C&C: $
      {srcip}:${srcport}, Traffic path: ${devname} (Policy ID:${policyid})\\${srcintf}, Reference: ${ref}", "extrainfo-type": 1, "filter": nul
      l, "filter-expr": "attack ~ Botnet and direction=outgoing and action!='detected' and action!='pass session'", "filter-relation": 0, "gro
      upby1": "dstendpoint", "groupby2": "attack", "groupby3": null, "indicator": null, "logtype": "ips", "name": "Traffic from Botnet CnC blo
      cked in ips log", "risk-severity": 4, "rule-id": "7", "severity": 1, "subject": "Traffic from Botnet C&C $groupby2 blocked", "tags": "De
      fault,Botnet,Signature,C&C,Outgoing", "thres-duration": 1440, "utmevent": null }, { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "en
      able": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${dstip}:${dstport}, Reference: ${ref}", "extrainfo-type": 1, "fi
      lter": null, "filter-expr": "logid==0422016400", "filter-relation": 0, "groupby1": "endpoint", "groupby2": "attack", "groupby3": null, "
      indicator": null, "logtype": "ips", "name": "Traffic to Botnet CnC blocked in ips log 2", "risk-severity": 4, "rule-id": "8", "severity"
      : 1, "subject": "Traffic to Botnet C&C $groupby2 blocked", "tags": "Default,Botnet,IP,C&C", "thres-duration": 1440, "utmevent": null },
      { "aggregate-expr": "COUNT(*) >= 1", "devtype": 0, "enable": 1, "eventstatus": "open", "eventtype": null, "extrainfo": "C&C: ${dstip}:${
      dstport}, Reference: ${ref}", "extrainfo-type": 1, "filter": null, "filter-expr": "logid==0422016401", "filter-relation": 0, "groupby1":
       "endpoint", "groupby2": "attack", "groupby3": null, "indicator": null, "logtype": "ips", "name": "Traffic to Botnet CnC detected in ips
       log 2", "risk-severity": 4, "rule-id": "9", "severity": 0, "subject": "Traffic to Botnet C&C $groupby2 detected", "tags": "Default,Botn
      et,IP,C&C", "thres-duration": 30, "utmevent": null } ], "template-url": "\/fazcfg-template\/basic-handler\/fgt", "update-time": 0, "vers
      ion": 2, "handlertype": 1, "correlationtype": "basic-handler" }, { "auto-raise-incident": 0, "automation-stitch": 1, "content-pack-id":
      "", "content-pack-uuid": "", "creation-time": 1777736984, "data-selector": "", "description": "", "enable": 1, "enable-time": 0, "handle
      r-id": "679_809_2dd_c09", "mitre-info": "", "name": "SINGLE-LOGIN-FAILED", "notification": "", "protected": 0, "rule": [ { "aggregate-ex
      pr": "COUNT(*)>=1", "devtype": 0, "enable": 1, "eventstatus": "auto", "eventtype": null, "extrainfo": null, "extrainfo-type": 0, "filter
      ": [ { "id": 1, "key": "logid", "oper": 0, "value": "0100032002" } ], "filter-expr": "", "filter-relation": 1, "groupby1": "srcip", "gro
      upby2": "", "groupby3": "", "indicator": null, "logtype": "event", "name": "SINGLE-LOGIN-FAILED", "risk-severity": 4, "rule-id": "401_4b
      9_c06_724", "severity": 2, "tags": "", "thres-duration": 1, "utmevent": "system" } ], "template-url": "", "update-time": 1777742099, "uu
      id": "", "version": 2, "handlertype": 1, "correlationtype": "basic-handler" }, { "auto-raise-incident": 0, "automation-stitch": 1, "cont
      ent-pack-id": "", "content-pack-uuid": "", "creation-time": 1777735167, "data-selector": "", "description": "This handler is to detect i
      f an account login failed many times not followed by a login success for FortiGate.", "enable": 1, "enable-time": 0, "eventstatus": "aut
      o", "extrainfo-type": 0, "handler-id": "360_fba_9a7_67b", "indicator": null, "mitre-domain": "enterprise", "mitre-info": "{\"enterprise\
      ": [\"T1110.001\"]}", "mitre-tech-id": "T1110.001", "name": "Custom-Brute-Force-Account-Login-Attack-FGT", "notification": "", "protecte
      d": 0, "risk-severity": 4, "rule": [ { "aggregate-expr": "COUNT(*) >= 5", "devtype": 0, "filter": [ { "id": 1, "key": "logid", "oper": 0
      , "value": "0100032002" } ], "filter-expr": "", "filter-relation": 1, "groupby1": "devid", "groupby2": "", "groupby3": "", "logtype": "e
      vent", "name": "Login Failed 5 times", "rule-id": "rule_1", "utmevent": "system" }, { "aggregate-expr": "COUNT(*)>=1", "devtype": 0, "fi
      lter": [ { "id": 1, "key": "logid", "oper": 0, "value": "0100032001" } ], "filter-expr": "", "filter-relation": 1, "groupby1": "devid",
      "groupby2": "", "groupby3": "", "logtype": "event", "name": "Login-Success", "rule-id": "rule_2", "utmevent": "system" } ], "rule-relati
      on": "rule_1 NOT_FOLLOWED_BY[1m] rule_2", "rule-relation-constraint": "rule_1.devid=rule_2.devid", "severity": 2, "subject": "Brute forc
      e account login attack on FGT device ${Login Failed 5 times.groupby1} detected", "tags": "Credential Access,login,attack", "template-url
      ": "\/fazcfg-template\/correlation-handler\/fgt", "thres-duration": 1, "update-time": 1777736344, "uuid": "", "version": 2, "handlertype
      ": 1, "correlationtype": "correlation-handler" } ] } }', len=9427.
      
      [T2322:oftps.c:2235 FGT70GSERIAL:192.168.1.201] The SSL/TLS connection has been closed
      [T2322:main.c:937 FGT70GSERIAL:192.168.1.201] Client connection closed. Reason 0(OK)
      [T2322:oftps.c:2342 FGT70GSERIAL:192.168.1.201] SSL pid[1688] ssl[0x7f3e50041820] shuting down sockfd[26] ip[192.168.1.201] connecte
      d[1]
      [T2322:oftps.c:2361 FGT70GSERIAL:192.168.1.201] SSL_shutdown SUCCESS
      [T2322:oftps.c:2369 FGT70GSERIAL:192.168.1.201] SSL socket[26] pid[1688] ssl[0x7f3e50041820] destroy_SSL_context

      With FortiAnalyzer Event Handlers, the sky really is the limit. You can do practically everything your heart desires if you speak FortiAnalyzer.

      Auto-scripts

      The next automation option is auto-scripts, which are CLI-only in config system auto-script, and not that well-known, but they are powerful. Just look at the options:

      70G-02 (LAB-SCRIPT) # set
      interval       Repeat interval in seconds.
      repeat         Number of times to repeat this script (0 = infinite).
      start          Script starting mode.
      script         List of FortiOS CLI commands to repeat.
      output-size    Number of megabytes to limit script output to (10 - 1024, default = 10).
      timeout        Maximum running time for this script in seconds (0 = no timeout).

      The script option takes whatever FortiOS CLI commands you want, but there is a limit of 1023 characters, so you won’t be able to import certificates.

      You can set the interval, how often the script runs and if it should start automatically or be executed manually.

      If you ever need to escape some characters in the script, like double quotes (), you can do that using backslashes \ as can be seen here:

      config system auto-script
          edit "LAB-SCRIPT"
              set start auto
              set script "config firewall address
      edit \"TEST\"
      set subnet 192.0.2.0/24
      set comment \"Escape double quotes\"
      next
      end
      "
          next
      end

      Auto-scripts have the nice advantage of being able to be created by outside systems but run with local privileges on a FortiGate, which becomes important if you try to automate things that outside systems aren’t allowed to do. A good example is FortiManager. FortiManager cannot change some options on a FortiGate, like certain options related to HA or the central management options itself, meaning FortiManager cannot change the FortiManager options on a FortiGate.

      To get around these limitations, you can create an auto-script on FortiManager and change whatever you need in that.

      For debugging around auto-scripts, you use the execute auto-script command, and lots of information is available in this technical tip: Technical Tip: Automated scripts (auto-script). Execution, testing and verification explained with examples 

      Importantly, auto-scripts can be created and controlled in automation actions, which opens up a lot of possibilities.

      One use case for auto-scripts I want to highlight is regarding the execute auto-script result option. Since this command shows the result of an auto-script and auto-scripts can run diagnose and get commands, you can use this as a crude information storage.

      Imagine that you run into the dreaded conserve mode, and you want to gather some information during that time, like which process took the memory. FortiGates come with a Conserve Mode automation trigger, and if you create the following automation action, you can later check what the status of your processes was using diagnose sys top 1 99 1 when conserve mode was triggered.

      config system auto-script
          edit "LAB-SCRIPT-TOP"
              set script "diagnose sys top 1 99 1"
          next
      end

      The reason I am performing an execute auto-script start LAB-SCRIPT-TOP after creating the auto-script is because setting set start auto is, for some reason, not possible with an automation action.

      ========== #1, 2026-05-01 12:39:06 ==========
      70G-02  config system auto-script
      70G-02 (auto-script)      edit "LAB-SCRIPT-TOP"
      70G-02 (LAB-SCRIPT-TOP)          set start auto
      Script start-mode could not be changed from auto-script.
      node_check_object fail! for start auto
      
      value parse error before 'auto'
      Command fail. Return code -37
      70G-02 (LAB-SCRIPT-TOP)          set script "diagnose sys top 1 99 1"
      70G-02 (LAB-SCRIPT-TOP)      next
      70G-02 (auto-script)  end
      70G-02  execute auto-script start LAB-SCRIPT-TOP
      
      ======= end of #1, 2026-05-01 12:39:06 ======

      Once your conserve mode problem has been solved, or it has solved itself, run an execute auto-script result LAB-SCRIPT-TOP and check your output.

      Auto-script output

      70G-02 # execute auto-script result LAB-SCRIPT-TOP
      Script LAB-SCRIPT-TOP output:
      
      ========== #1, 2026-05-02 18:13:44 ==========
      70G-02  diagnose sys top 1 99 1
      06:13:46 PM up 0 days, 2 hours and 21 minutes
      0U, 0N, 0S, 100I, 0WA, 0HI, 0SI, 0ST; 3708T, 2315F
               miglogd     1714      S       0.5     1.3    2
                  node     1572      S       0.0     2.6    3
             ipsengine     1893      S       0.0     2.1    3
             ipshelper     1604      S       0.0     2.1    2
             ipsengine     1895      S       0.0     2.1    2
             ipsengine     1894      S       0.0     2.1    0
                   wad     1665      S       0.0     1.6    3
                   wad     1663      S       0.0     1.5    1
               cmdbsvr     1484      S       0.0     1.5    0
                cw_acd     1614      S       0.0     1.3    2
             forticron     1558      S       0.0     1.3    0
             extenderd     1631      S       0.0     1.2    1
               miglogd     1570      S       0.0     1.2    0
                  csfd     1634      S       0.0     1.2    3
                   wad     1664      S       0.0     1.2    0
                newcli     5113      S       0.0     1.2    3
                newcli     2866      S       0.0     1.2    2
                httpsd     5707      S       0.0     1.1    1
                   wad     1656      S       0.0     1.1    0
                 fgfmd     1613      S       0.0     1.1    2
       initXXXXXXXXXXX        1      S       0.0     1.1    3
                   wad     1580      S       0.0     1.0    1
            http_authd     1551      S       0.0     1.0    2
                   wad     1659      S       0.0     1.0    0
                   wad     1657      S       0.0     0.9    2
                  iked     1718      S       0.0     0.9    3
                   wad     1661      S       0.0     0.9    1
                   wad     1662      S       0.0     0.8    0
               fgtlogd     1593      S       0.0     0.8    3
                   wad     1655      S       0.0     0.8    0
                   wad     1660      S       0.0     0.8    1
                   wad     1658      S       0.0     0.8    2
                authd3     1726      S       0.0     0.8    1
                csfd 0     1964      S       0.0     0.8    3
                authd2     1725      S       0.0     0.8    3
       eap_proxy_worke     1728      S       0.0     0.8    3
                  iked     1716      S       0.0     0.8    3
            telemetryd     1633      S       0.0     0.8    1
                  iked     1717      S       0.0     0.7    0
                fcnacd     1567      S       0.0     0.7    0
              dnsproxy     1617      S       0.0     0.7    3
             forticldd     1559      S       0.0     0.7    1
                cu_acd     1623      S       0.0     0.7    0
                httpsd     1550      S       0.0     0.7    2
                   imi     1546      S       0.0     0.7    3
                  bgpd     1543      S <     0.0     0.7    2
                   nsm     1542      S       0.0     0.7    3
               updated     1589      S       0.0     0.7    0
                 authd     1560      S       0.0     0.7    2
                fltund     1626      S       0.0     0.6    0
                 snmpd     1596      S       0.0     0.6    3
            cid-config     1610      S N     0.0     0.6    2
                fnbamd     1557      S       0.0     0.6    3
               fortimq     1637      S       0.0     0.6    3
                  iotd     1598      S       0.0     0.6    1
                 voipd     1579      S       0.0     0.6    0
               foauthd     1561      S       0.0     0.6    1
                  sshd     1600      S       0.0     0.6    3
               wpad_ac     1618      S       0.0     0.6    2
                  ntpd     1599      S <     0.0     0.6    2
                flcfgd     1624      S       0.0     0.6    2
            http_authd     1554      S       0.0     0.5    2
                 radvd     1603      S       0.0     0.5    2
                  iked     1715      S       0.0     0.5    0
             forticron     5778      S       0.0     0.5    2
                lnkmtd     1601      S       0.0     0.5    3
                 autod     1635      S       0.0     0.5    2
        zebos_launcher     1527      S       0.0     0.5    3
             cloudapid     1636      S       0.0     0.5    3
                  sshd     2865      S       0.0     0.4    3
               syslogd     1591      S       0.0     0.4    3
             locallogd     1595      S       0.0     0.4    2
             forticron     5779      R       0.0     0.4    3
             forticron     5777      S       0.0     0.4    1
              cid-scan     1620      S       0.0     0.4    0
            fortilinkd     1622      S       0.0     0.4    2
                   vwl     1602      S       0.0     0.4    2
        ikecryptd_dhw0     1583      S       0.0     0.4    3
        ikecryptd_dhw2     1585      S       0.0     0.4    3
        ikecryptd_dhw1     1584      S       0.0     0.4    3
            ipsmonitor     1555      S       0.0     0.3    1
                flpold     1625      S       0.0     0.3    2
         cw_acd_helper     1733      S       0.0     0.3    2
              dnsproxy     1606      S       0.0     0.3    1
          ipmc_sensord     1548      S       0.0     0.3    1
                dhcp6c     1630      S       0.0     0.3    1
             ikecryptd     1581      S       0.0     0.3    1
                 getty     1552      S <     0.0     0.3    0
             alertmail     1605      S       0.0     0.3    3
             eap_proxy     1611      S       0.0     0.3    3
        merged_daemons     1556      S       0.0     0.3    2
             clearpass     1563      S       0.0     0.3    3
               uploadd     1547      S       0.0     0.3    2
             fsso_ldap     1569      S       0.0     0.3    3
                   fas     1566      S       0.0     0.3    1
              httpclid     1565      S       0.0     0.3    2
                   fsd     1629      S       0.0     0.3    1
              kmiglogd     1549      S       0.0     0.3    3
                 getty     1553      S       0.0     0.2    3
      
      ======= end of #1, 2026-05-02 18:13:47 ======

      You can of course also send a mail with all this information, as is detailed in this technical tip: Technical Tip: Automation stitch for conserve mode

      Batch mode

      The last topic for automation I want to write about is batch mode. This isn’t a way to automate something, but you can use it in automation, so it gets a spot.

      Batch mode is something you start, write down your CLI commands, and once you end the mode, the commands get executed. This allows you to make lots of changes in a single “transaction”, not to be confused with the actual config transactions, where the individual configurations might cut off your access at some point.

      Imagine you are managing a FortiGate via a VPN, and you need to drastically change the VPN configuration, which would normally disconnect you somewhere along the configuration. With batch mode, you can do this all without disruption.

      One very nice thing about batch mode is that you don’t have to worry about formatting, character limits or anything of the sort. Since you’re working like normal on the CLI, you can do just about every configuration. In the auto-script section, I mentioned how the character limit is 1023, so importing certificates won’t work. This is no problem with batch mode.

      Batch mode certificate import

      70G-02 # execute batch start
      
      Enter batch mode...
      
      70G-02 # config vpn certificate remote
      70G-02 #     edit "TEST-CERT"
      70G-02 #         set remote "-----BEGIN CERTIFICATE-----
      > MIIHSzCCBTOgAwIBAgITMgAAADd86bYatj17eQACAAAANzANBgkqhkiG9w0BAQ0F
      > ADBVMRMwEQYKCZImiZPyLGQBGRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbGFiZG9t
      > YWluMRIwEAYKCZImiZPyLGQBGRYCYWQxDzANBgNVBAMTBldJTi1DQTAeFw0yNTA4
      > MDkxOTU0MDdaFw0yNzA4MDkxOTU0MDdaMCMxITAfBgNVBAMMGGxhYnVzZXJAYWQu
      > bGFiZG9tYWluLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrJ
      > EQqx06rKJ9OULN2155KGM31WjJvUDM+aPEiHgmAUE9pPmob/kLzQ97ylnj3AzY9e
      > FF+coqgphfpxdLhlYEOUSAzQsK2B/Jn7MS8dhwfaYGtVvtfEjOZa833pXWmz1bQE
      > Onfrnp1n3QZL6WYkSLHUbBESQY6tmfzOohcu9rfNfMVFEsPz/MPTHOFkqn5CfPMT
      > p9gV3X3/UhoWrXBphUfKjHf5+NY2kBVySRIsaFxv/yTI/ypbc0Zk3a7Ux4dJRRcd
      > jIGfHxoN6d9EphlMd/fEY27yJ0Ul1HZJh7skRsP9y0egMQtbEbsOmCxeyXysNuLe
      > bTMYVxJ5w9DFB+bbmYUCAwEAAaOCA0QwggNAMCcGA1UdJQQgMB4GCCsGAQUFBwMC
      > BggrBgEFBQcDBAYIKwYBBQUHAwEwMwYJKwYBBAGCNxUKBCYwJDAKBggrBgEFBQcD
      > AjAKBggrBgEFBQcDBDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUTQ/QuOQjE0fjDKCy
      > ajTzs7KtenYwDgYDVR0PAQH/BAQDAgWgMCMGA1UdEQQcMBqCGGxhYnVzZXJAYWQu
      > bGFiZG9tYWluLmNvbTAfBgNVHSMEGDAWgBTzzdZs1ME1aNfuqgd6qFpzuEhs1TCC
      > AQcGA1UdHwSB/zCB/DCB+aCB9qCB84aBt2xkYXA6Ly8vQ049V0lOLUNBKDIpLENO
      > PVdJTi1BRCxDTj1DRFAsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2Vy
      > dmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1sYWJkb21haW4sREM9Y29t
      > P2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxE
      > aXN0cmlidXRpb25Qb2ludIY3aHR0cDovL1dJTi1BRC5hZC5sYWJkb21haW4uY29t
      > L0NlcnRFbnJvbGwvV0lOLUNBKDIpLmNybDCCAR8GCCsGAQUFBwEBBIIBETCCAQ0w
      > ga0GCCsGAQUFBzAChoGgbGRhcDovLy9DTj1XSU4tQ0EsQ049QUlBLENOPVB1Ymxp
      > YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24s
      > REM9YWQsREM9bGFiZG9tYWluLERDPWNvbT9jQUNlcnRpZmljYXRlP2Jhc2U/b2Jq
      > ZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBbBggrBgEFBQcwAoZPaHR0
      > cDovL1dJTi1BRC5hZC5sYWJkb21haW4uY29tL0NlcnRFbnJvbGwvV0lOLUFELmFk
      > LmxhYmRvbWFpbi5jb21fV0lOLUNBKDIpLmNydDA9BgkrBgEEAYI3FQcEMDAuBiYr
      > BgEEAYI3FQiFrZZuhK/KeoeNhyWH9NB3gfixAQWDyMVZhdXVWQIBZAIBCTANBgkq
      > hkiG9w0BAQ0FAAOCAgEAvQt0Mm/hcFZ4GX5KZj7mlj3Aqm4HyapaRSAHVp7eN+I2
      > kZiWtzn8WP6fEcniHNXn4hs/5c/MEBzpIbQ4z0UvymH2AHijiZAEq/zOqOlGCOjy
      > 8/lhRFY0cjGxg6GfpN7Pm9p0PEEOGKvapn92hxE/rAL4S5m4jxu9Nukfcw8mcb7g
      > kaGr/3HAlRcsttU0VyJichmfBOYutpj3mhJ38Sc7eCxbTqj+U71NSjdrNx2WN2jh
      > PAhS31tP1YTyNjW7Tgrn9unplxi2BqjDc0j72/247/E6DBdroV5HiQ2F3e2u1eXk
      > Ss3IKYKtU1UixDAYsN28Nir4u87P4yo1xcpGTHr5Qne1E2tHNH1jb951MZPMuhh6
      > bSIm+U0b0EzNgfQp6wI+a/9ZtliZfpxrvzyp7mPwhri9ZbNtDkWPTOjjWrw4yEVZ
      > /e+UFt9ghYB0xSvllvd8fquzG5bgOv46EfwSw0J8a4PGwy4Wy7kiyjrsphimNY6/
      > ql8WEeg48QTukluhULsKosO1JNx6RLe85gCcQ7Jb9Q77q2DJiTHjqu7OACns4DkV
      > BKPtYE0+40/TOCc4GgsRHk/iLy0/vnh7KH5ITblokygKMFAjhBlmSmdhajK/7Vyl
      > cWMKYzZMGk6FvNU8XXx/fjVc8dL2lj+P3E6UOvtYtX4Yb3TuVUp+NtJSJZ6eLiA=
      > -----END CERTIFICATE-----"
      70G-02 #     next
      70G-02 # end
      70G-02 # execute batch end
      Exit and run batch commands...
      
      70G-02 # execute batch lastlog
      0: config vpn certificate remote
      0:     edit "TEST-CERT"
      0:         set remote "-----BEGIN CERTIFICATE-----
      0: MIIHSzCCBTOgAwIBAgITMgAAADd86bYatj17eQACAAAANzANBgkqhkiG9w0BAQ0F
      0: ADBVMRMwEQYKCZImiZPyLGQBGRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbGFiZG9t
      0: YWluMRIwEAYKCZImiZPyLGQBGRYCYWQxDzANBgNVBAMTBldJTi1DQTAeFw0yNTA4
      0: MDkxOTU0MDdaFw0yNzA4MDkxOTU0MDdaMCMxITAfBgNVBAMMGGxhYnVzZXJAYWQu
      0: bGFiZG9tYWluLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrJ
      0: EQqx06rKJ9OULN2155KGM31WjJvUDM+aPEiHgmAUE9pPmob/kLzQ97ylnj3AzY9e
      0: FF+coqgphfpxdLhlYEOUSAzQsK2B/Jn7MS8dhwfaYGtVvtfEjOZa833pXWmz1bQE
      0: Onfrnp1n3QZL6WYkSLHUbBESQY6tmfzOohcu9rfNfMVFEsPz/MPTHOFkqn5CfPMT
      0: p9gV3X3/UhoWrXBphUfKjHf5+NY2kBVySRIsaFxv/yTI/ypbc0Zk3a7Ux4dJRRcd
      0: jIGfHxoN6d9EphlMd/fEY27yJ0Ul1HZJh7skRsP9y0egMQtbEbsOmCxeyXysNuLe
      0: bTMYVxJ5w9DFB+bbmYUCAwEAAaOCA0QwggNAMCcGA1UdJQQgMB4GCCsGAQUFBwMC
      0: BggrBgEFBQcDBAYIKwYBBQUHAwEwMwYJKwYBBAGCNxUKBCYwJDAKBggrBgEFBQcD
      0: AjAKBggrBgEFBQcDBDAKBggrBgEFBQcDATAdBgNVHQ4EFgQUTQ/QuOQjE0fjDKCy
      0: ajTzs7KtenYwDgYDVR0PAQH/BAQDAgWgMCMGA1UdEQQcMBqCGGxhYnVzZXJAYWQu
      0: bGFiZG9tYWluLmNvbTAfBgNVHSMEGDAWgBTzzdZs1ME1aNfuqgd6qFpzuEhs1TCC
      0: AQcGA1UdHwSB/zCB/DCB+aCB9qCB84aBt2xkYXA6Ly8vQ049V0lOLUNBKDIpLENO
      0: PVdJTi1BRCxDTj1DRFAsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2Vy
      0: dmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1sYWJkb21haW4sREM9Y29t
      0: P2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxE
      0: aXN0cmlidXRpb25Qb2ludIY3aHR0cDovL1dJTi1BRC5hZC5sYWJkb21haW4uY29t
      0: L0NlcnRFbnJvbGwvV0lOLUNBKDIpLmNybDCCAR8GCCsGAQUFBwEBBIIBETCCAQ0w
      0: ga0GCCsGAQUFBzAChoGgbGRhcDovLy9DTj1XSU4tQ0EsQ049QUlBLENOPVB1Ymxp
      0: YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24s
      0: REM9YWQsREM9bGFiZG9tYWluLERDPWNvbT9jQUNlcnRpZmljYXRlP2Jhc2U/b2Jq
      0: ZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBbBggrBgEFBQcwAoZPaHR0
      0: cDovL1dJTi1BRC5hZC5sYWJkb21haW4uY29tL0NlcnRFbnJvbGwvV0lOLUFELmFk
      0: LmxhYmRvbWFpbi5jb21fV0lOLUNBKDIpLmNydDA9BgkrBgEEAYI3FQcEMDAuBiYr
      0: BgEEAYI3FQiFrZZuhK/KeoeNhyWH9NB3gfixAQWDyMVZhdXVWQIBZAIBCTANBgkq
      0: hkiG9w0BAQ0FAAOCAgEAvQt0Mm/hcFZ4GX5KZj7mlj3Aqm4HyapaRSAHVp7eN+I2
      0: kZiWtzn8WP6fEcniHNXn4hs/5c/MEBzpIbQ4z0UvymH2AHijiZAEq/zOqOlGCOjy
      0: 8/lhRFY0cjGxg6GfpN7Pm9p0PEEOGKvapn92hxE/rAL4S5m4jxu9Nukfcw8mcb7g
      0: kaGr/3HAlRcsttU0VyJichmfBOYutpj3mhJ38Sc7eCxbTqj+U71NSjdrNx2WN2jh
      0: PAhS31tP1YTyNjW7Tgrn9unplxi2BqjDc0j72/247/E6DBdroV5HiQ2F3e2u1eXk
      0: Ss3IKYKtU1UixDAYsN28Nir4u87P4yo1xcpGTHr5Qne1E2tHNH1jb951MZPMuhh6
      0: bSIm+U0b0EzNgfQp6wI+a/9ZtliZfpxrvzyp7mPwhri9ZbNtDkWPTOjjWrw4yEVZ
      0: /e+UFt9ghYB0xSvllvd8fquzG5bgOv46EfwSw0J8a4PGwy4Wy7kiyjrsphimNY6/
      0: ql8WEeg48QTukluhULsKosO1JNx6RLe85gCcQ7Jb9Q77q2DJiTHjqu7OACns4DkV
      0: BKPtYE0+40/TOCc4GgsRHk/iLy0/vnh7KH5ITblokygKMFAjhBlmSmdhajK/7Vyl
      0: cWMKYzZMGk6FvNU8XXx/fjVc8dL2lj+P3E6UOvtYtX4Yb3TuVUp+NtJSJZ6eLiA=
      0: -----END CERTIFICATE-----"
      0:     next
      0: end

      You can, of course, use batch mode in combination with other automation options, like automation actions in stitches, but keep the 1023-character limit in mind.

      In combination with automation actions, I want to note that batch mode does not display output of the command being executed, so if you do a get system status in batch mode, you don’t have any output of this directly. You have to do an execute batch lastlog to get the output.

      A use case at the end: Push API feed

      A little-known feature a FortiGate has is being able to host feeds itself using the Push API method. The idea is that you use API requests to send IPs, MAC addresses, domains, etc., to the FortiGate; it adds or removes the entries from the feed, and you use the feed wherever it makes sense, like in firewall policies.

      One caveat is that the information in the feed is volatile and does not survive a reboot.

      Let’s combine a few things from this post to create an example.

      We start with our FortiAnalyzer Event Handler Custom-Brute-Force-Account-Login-Attack-FGT (this is the cloned event handler from above) as our trigger. If it gets triggered, we add the source IP from that event to the Push API feed using a webhook,ban the IP and also send an email notification with the log information.

      Push API stitch
      Push API feed valid

      Push API automation stitch configuration

      config system automation-trigger
          edit "FAZ-BRUTE-FORCE"
              set event-type faz-event
              set faz-event-name "Custom-Brute-Force-Account-Login-Attack-FGT"
          next
      end
      config system automation-action
          edit "PUSH-API-WEBHOOK"
              set action-type webhook
              set protocol https
              set uri "192.168.1.201/api/v2/monitor/system/external-resource/dynamic"
              set http-body "{ \"commands\": [{ \"name\": \"PUSH-FEED\", \"command\": \"add\", \"entries\": [ \"%%log.srcip%%\" ] } ]}"
              set port 8443
              config http-headers
                  edit 1
                      set key "Authorization"
                      set value "4xdzrjdhdrs3417b51wqt4cw05p0rd"
                  next
              end
              set verify-host-cert disable
          next
      end
      config system automation-stitch
          edit "FAZ-BRUTE-FORCE-PUSH-API"
              set status disable
              set trigger "FAZ-BRUTE-FORCE"
              config actions
                  edit 3
                      set action "IP Ban"
                      set required enable
                  next
                  edit 2
                      set action "PUSH-API-WEBHOOK"
                      set required enable
                  next
                  edit 4
                      set action "Email Notification"
                      set required enable
                  next
              end
          next
      end

      Wrapping up

      I realize that nothing I covered in this post is that advanced, but I saw some of the things I covered being asked a few times, and some things, like the log dumping, Push API feeds, and auto-scripts as information storage, are things I personally wanted to look at a bit more, and I turned it into a post.

      So, dear reader, thank you for your time in indulging me.

    • FortiGate 8.0 ZTNA configuration – All methods + Entra SAML SSO

      As Fortinet has written in their 8.0 New Features guide, the Zero Trust Network Access configuration got an overhaul and was simplified. To get used to the new way, I went through all the ways you can configure ZTNA, as well as integrate it with Entra SAML SSO. The fruits of my labour, dear reader, can be seen in this video:

      …but wait, there’s more!

      Just a video alone is boring, so here is some more information:

      • The default minimum TLS/SSL version of the ZTNA VIP is still set to 1.1, same as always. Vulnerability scanners usually don’t like that, so you can set this to something better, like 1.2.
      config firewall vip
          edit "ZTNA-PORT-9443"
              set ssl-min-version tls-1.2
          next
      end
      • Before FortiOS 8.0, setting the vhost/host for a Traffic Forwarding server was optional, which led to the problem of the FQDN value in EMS constantly resetting if you made certain changes and you didn’t set the value yourself beforehand. With 8.0, you have to set this value when creating a Traffic Forwarding server, which creates a better administrative and user experience.
      • The synchronization time between FortiClient and FortiClient EMS is at a default of 60 seconds for on-prem and 300 seconds for EMS Cloud. You can lower this to a minimum of 20 seconds by changing the Keepalive Interval value in System Settings -> EMS Settings -> Endpoint Settings
      • While you can use regular firewall policies as ZTNA policies, instead of proxy policies, I wouldn’t recommend it. I ran into weird issues if I didn’t use proxy policies, and the recommendation by Fortinet is also to use proxy policies.

      Afterthoughts

      I not only made this video to get used to the new way. I also needed an excuse to finally set up my Entra tenant and work on my video editing and recording skills. I have learned a few non-Fortinet things in this process, and I’m glad I went through it.

      So, dear reader, hopefully you can get something out of this post. Maybe not now, but when 8.0 is a recommended version.

    • Quick tips: FortiSwitch custom command double quotes, ZTNA SAML vhost error, session evaluation after ZTNA tag changes

      Not every post needs to be something that takes hours to get together, and that is why I want to start a new type of post, where I write about things I experience in my day-to-day, that seem interesting, and want to put out there, so maybe someone else doesn’t have to spend hours looking for a solution.

      With that, dear reader, read about the quick tips for today.

      Models and versions:

      • FortiGate 70G on 7.6.6
      • FortiSwitch 424E on 7.6.6
      • FortiClient EMS on 7.4.7
      • FortiClient on 7.4.6
      • Windows 11 client on 25H2

      Handling double quotes in FortiSwitch custom commands

      Custom commands are an important part of managing FortiSwitches, because not every piece of configuration you can make on a FortiSwitch is available on the FortiGate. Lots of things you don’t touch every day have to be configured using custom commands, and in some cases, you run into the issue of having to use double quotes, which creates an issue, because double quotes, on the FortiGate, signify where the custom command starts and where it ends.

      Looking at the example that prompted this post: You want to push a Certificate Authority (CA) certificate to a FortiSwitch for LDAPS authentication.

      On a FortiSwitch, this would look like this (certificate shortened for brevity):

      config system certificate ca
          edit WIN-CA
      	set ca "-----BEGIN CERTIFICATE-----
      		MIIFrDCCA5SgAwIBAgIQP+FZ4Onx66RLEDsn8YuCGTANBgkqhkiG9w0BAQ0FADBV
      		MRMwEQYKCZImiZPyLGQBGRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbGFiZG9tYWlu
      		MRIwEAYKCZImiZPyLGQBGRYCYWQxDzANBgNVBAMTBldJTi1DQTAeFw0yNTA4MDIx
      		...
      		TETr69CP4eDwJGD7gZp8Lnz98Xj+fQUcco+/9xuK7JXQlE97H05Mn32YZmv4aFC0
      		v2S0t0Lk+YqUVZKAMRfnZ9nu8dTHFE4Q+5TqOxcyEwpmAeXRIWs/HPOXhegu87tu
      		bqYj4gD9n/S8BjlOqtx/Yw==
      		-----END CERTIFICATE-----"
          next
      end

      The double quotes in the BEGIN CERTIFICATE line will create your first problem when using a custom command. If you want to push this certificate using a custom command on the FortiGate, you have to replace the double quotes with a % symbol followed by the hex code. In this case, it would be %22.

      Using it in a custom command, we can deploy a CA certificate like this (certificate shortened for brevity):

      config switch-controller custom-command
          edit "WIN-CA"
              set command "config system certificate ca%0a edit WIN-CA%0a set ca %22-----BEGIN CERTIFICATE-----%0aMIIFrDCCA5SgAwIBAgIn/S8BjlOqtx/Yw==%0a-----END CERTIFICATE-----%22%0a next%0a end%0a"
          next
      end
      config switch-controller managed-switch
          edit "SERIAL/NAME"
              config custom-command
                  edit "WIN-CA"
                      set command-name "WIN-CA"
                  next
              end
          next
      end

      Someone hinted at the possibility of this by telling me to use a backslash and the hex code, which didn’t work, and then I remembered that in custom commands, %0a should be used for a line feed, and 0a is the hex code for that. Put two and two together, and you end up with %22.

      Thank you to this specific person, who, I am sure, wants to remain anonymous, but he will likely read this.

      If you ever need a lookup table or a converter for characters and their hex representation, check out one of the many sites that offer this, like this one: ASCII Character Chart with Decimal, Binary and Hexadecimal Conversions

      ZTNA “failed to match an API-gateway” error using SAML SSO authentication

      If you’re configuring SAML SSO authentication for your ZTNA connections and you run into the issue where FortiClient displays a “The page you requested has been blocked because no API gateway was matched” and the FortiGate shows an error like “Traffic denied because HTTP url (https://ztnaproxy.domain.com/tcp?address=win-ad.ad.labdomain.com&port=3389&tls=0) failed to match an API-gateway with vhost(name/hostname:saml_auto_vhost_SAML/ztnaproxy.domain.com)” (the FQDN and the “SAML” is custom and will look different for you), I might have an answer for you.

      Go into your ZTNA server configuration and the service/server mapping, specify the virtual host, supply a host matched by a substring that corresponds to the domain from the HTTPS URL in the error, and select your certificate.

      ZTNA virtual host
      config firewall access-proxy-virtual-host
          edit "auto-ztna-vhmoifbct0"
              set ssl-certificate "ztnaproxy.domain.com"
              set host "ztnaproxy.domain.com"
          next
      end
      config firewall access-proxy
          edit "LAB-ZTNA"
              set vip "LAB-ZTNA"
              config api-gateway
                  edit 1
                      set url-map "/tcp"
                      set service tcp-forwarding
                      set virtual-host "auto-ztna-vhmoifbct0"
                      config realservers
                          edit 1
                              set address "win-ad.ad.labdomain.com"
                              set mappedport 3389
                          next
                          edit 2
                              set address "ubuntu-ws-1.ad.labdomain.com"
                              set mappedport 80
                          next
                      end
                  next
                  edit 2
                      set service samlsp
                      set saml-server "ENTRA-SAML"
                  next
              end
          next
      end

      I’m not sure why this error happened, because I have configured this thing before without having to specify a virtual host, but this works.

      [Update 2026-04-29]: Yesterday, in bed, I found the cause of this behaviour change. Since 7.6.1 FortiOS learns, and creates, a virtual host implicitly from a SAML authentication when using ZTNA. The proposed workaround of “To avoid this behavior, put the SAML api-gateway in a separate access-proxy and use a different virtual host for your server.” is a bit confusing to me, but unless you have multiple SAML servers for ZTNA, which creates other issues due to authentication rules, it’s not relevant. If you have multiple SAML servers, you will always fall into the authentication rule that is first in the list, unless you configure source and/or destination addresses (CLI-only), so you can authenticate with that one, and you get a user on the FortiGate in WAD, but authorization will fail, because you are hitting a proxy rule with one SAML server, but you were authenticated with another.

      Evaluating firewall sessions after dynamic address changes (ZTNA tags, FSSO)

      After solving the error from the previous section, I played around with ZTNA tags and noticed that, if a tag changes, existing sessions won’t get evaluated and possibly blocked. This is something you have to be aware of. If you are using dynamic address objects, you probably want existing sessions to not transfer traffic if new ones should get blocked.

      Think of an attacker establishing a channel from inside your network to an external command and control server, and if your ZTNA tags notice this, this connection won’t get blocked, because it’s an existing session.

      If you want existing firewall policies to get evaluated if dynamic address objects change, be that ZTNA tags, FQDN objects, Fortinet Single Sign-On (FSSO) objects, or otherwise, it’s only a single command away.

      config system settings
          set dyn-addr-session-check enable
      end

      Short and sweet and done

      I might do this type of post more often, and I hope that you, dear reader, can take something away from this.

    • FortiOS 8.0 new features review

      A new major release of FortiOS is always an exciting time in the Fortinet ecosystem, because it gives the direction on where Fortinet wants to take Fortinet and, of course, FortiGate.

      7.0 brought us the Zero Trust Network Access (ZTNA) features, for example, and that is still a big talking point.

      I want to take this post to focus on new features, big and small, that I think are interesting, both from an operational viewpoint as someone in the field, as well as features Fortinet wants you to know about.

      So, dear reader, join me in going through the depths of the FortiOS 8.0 New Features guide to find out what 8.0 has got to offer.

      Note: Some features don’t have an entry in the New Features guide. In these cases, I will link to the release notes with the corresponding feature ID and, if available, supplemental documentation.

      Custom tags for objects and policies

      Custom tags for addresses, policies, and dynamic tag address groups | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      I am putting this one first, because its usefulness might otherwise slip through the cracks, and it would be a shame, because the feature is awesome and quite possibly my number 1 feature in 8.0.

      At first glance, it just looks like you can attach the tag to things to make elements stand out, but where it shines is with address objects and address groups.

      Imagine a scenario where you have a fleet of Linux servers that all need to get to a repository for updates. Previously, you had to create your servers as address objects, optionally add them to a group, and then use either the object itself or the group in a policy.

      With tags, you can put the tag on the server address object, use the tag in an address group and use the group in a policy. Every additional Linux server gets an object with a tag, and you’re done. This makes the process more dynamic, and deleting individual objects is also easier, because there is no reference to a group.

      I’m sure you can already think of some fantastic use cases for this, because I sure can.

      Fortinet has a nice video demonstrating this, so I don’t have to make one.

      FortiOS 8.0: Dynamic Tag Based Address Group

      Using these tags for policies is a nice bonus, but I don’t see that much use here outside of filtering. If you want to tag your policies, be sure to first enable the feature Policy Tagging in Feature Visibility.

      Fabric Feeds

      Cloud-based Fabric Feed synchronization | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      This is a combined new feature, because it also requires FortiAnalyzer 8.0, but it’s too cool not to mention.

      With fabric feeds, you can leverage the Fortinet Message Queue (FortiMQ) fabric to synchronize threat feeds across your fabric, with FortiAnalyzer being at the heart of it. On FortiAnalyzer, you can block indicators (IPs, domains, URLs, and MAC addresses), and these get synchronized to all FortiGates that are registered under the same FortiCloud account as fabric feeds.

      FortiAnalyzer indicators

      These fabric feeds are configured in the same place as external feeds and act much the same way in how you handle and use them. Use IP and MAC feeds in policies, domains in DNS filters, and URLs in web filters as categories.

      FortiGate Fabric Feeds

      If you think this is useful, but don’t want to upgrade to 8.0, I understand your hesitation; you might be in luck. A very similar feature exists in 7.6, except it also requires FortiManager.

      Blocking indicators | FortiAnalyzer 7.6.6 | Fortinet Document Library

      In this configuration, indicators are blocked on FortiAnalyzer; this information is sent to FortiManager using a playbook, and FortiManager hosts these indicators as threat feeds for FortiGates to consume.

      TLS 1.3 based VPN over TCP

      TLS 1.3 based VPN over TCP | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      I’m putting this feature here for the future, because as it stands, it’s something you should look forward to, but it isn’t all that useful right now.

      IKE over TCP has been a thing for quite some time now, and while it works, it has the distinct downside of not being TLS traffic, meaning that application-based firewalls don’t recognize it as such, even if it uses TCP/443 as its port and protocol. This can be a problem for employees on the go, because wherever they are, their non-TLS application traffic might get blocked.

      With this feature, the entire IKE communication gets wrapped in a nice TLS 1.3 bow and is sent out into the world, passing itself off as any other communication you might expect on a network that allows TCP/443.

      This feature also fills the requirement of TLS-based VPN being available, which 7.6 lost with 7.6.3, since SSL-VPN was deprecated.

      The reason I wrote that this feature is for the future is that, at the time of writing (April 2026), FortiClient does not support this. It should come with FortiClient 8.0, but right now, it’s only FortiGates that can use this feature.

      High Availability actions in the GUI

      GUI support for HA actions, health status, and config diff | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      Before 8.0, lots of HA processes were CLI-only, like resyncs, forced failovers, configuration difference checks, etc. With 8.0, you can perform these things directly in the GUI, which makes this topic more approachable.

      You now also get a quick overview of some vital statistics, like RAM usage and checksums.

      Note: The failover in the GUI uses the not-so-nice static and manual failover method under the hood. This means that if you want to perform a failback, you have to do so manually. Due to this, I would not recommend using it, except, as the link says, for testing and demonstrations. If you want to perform a lasting failover, continue using the diagnose sys ha reset-uptime method on the CLI, which assumes you are outside the ha-uptime-diff-margin window.

      DHCP templates

      New features or enhancements | FortiGate / FortiOS 8.0.0 | Fortinet Document Library (Feature ID 1130044)

      config system dhcp template | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      If you needed a lot of similar DHCP servers on your FortiGate, you previously had to create them individually per interface, and if you ever needed to make changes, like a new DNS or NTP server, you had to edit each entry. With DHCP templates, you can create one template entry and attach it to each interface. Pretty easy to see the value in this new feature.

      In the GUI, you can find the DHCP Templates option in Network -> IPAM -> DHCP Templates. You can also create DHCP templates inline while configuring a DHCP server. You first have to enable the DHCP server option on the interface, then the DHCP template option, and in the dropdown, click on the + to get to the wizard. In the CLI, the configuration is under config system dhcp template.

      DHCP template GUI

      Once there, you can set all options you normally can in the GUI as well. Some options, like the domain, continue to be CLI-only.

      DHCP template configuration

      One thing I want to expand on here is how the Excluded ranges option works in the template, because, at least to me, it was not immediately obvious.

      When you add an excluded range, you have a field titled Offset (start-ip-index in the CLI) and Number of addresses to reserve (ip-count in the CLI).

      Offset in this case means at which IP does the excluded range start, and the number of addresses to reserve is the number of reserved IPs after, and including, the offset IP.

      Let’s look at an example, using the DHCP template from above.

      If you turn on the DHCP server debugs (diagnose debug application dhcps -1) on the CLI, we get the following output after enabling debugs and applying the template:

      [debug]setting up address range 192.0.2.1 - 192.0.2.9
      [debug]setting up address range 192.0.2.30 - 192.0.2.59
      [debug]setting up address range 192.0.2.70 - 192.0.2.254

      The first excluded range has the offset 10, so the first excluded IP is 192.0.2.10. The reserved addresses are set to 20, so, including .10, we have 20 reserved IPs, which gives us a range from 192.0.2.10 to 192.0.2.29.

      The second excluded range has the offset 60, so the first excluded IP is 192.0.2.60. The reserved addresses are set to 10, so, including .60, we have 10 reserved IPs, which gives us a range from 192.0.2.60 to 192.0.2.69.

      Everything outside the excluded ranges is fair game for DHCP leases.

      SD-WAN member selection based on monthly traffic volume

      Control SD-WAN interface usage based on monthly traffic volume (quota) | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      A big benefit of SD-WAN on FortiGates is that you can use whatever as your local internet breakout, and lots of sites are using 5G/LTE modems, which usually have a monthly traffic limit. If the limit is reached, you are often capped on your speed or incur additional costs. Using this new feature, you can set various parameters regarding your monthly quota and what should happen if your quota is reached (increasing the cost if using a Lowest cost SLA, for example), so you can deprioritize the SD-WAN member.

      SD-WAN on-demand duplication

      Duplication, in relation to SD-WAN, is the act of sending duplicates of packets to another FortiGate to overcome link problems, like high latency or packet loss. Using this new feature, the hub in an SD-WAN deployment can enable duplication based on the received SLA information from a spoke.

      The hub usually has a remote SLA, which measures the spoke’s connection status, using the spoke’s embedded SLA information in its ICMP probes towards the hub. If the spoke is out of SLA, the hub can now enable duplication dynamically to send duplicate packets, in the hope that this is enough to provide a good experience for users on the spoke’s site.

      Changes to ZTNA configuration

      ZTNA configuration simplification | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      8.0 has overhauled how you configure ZTNA, both in the GUI and on the CLI. Fortinet calls it a simplification, and maybe this is true; only time will tell, but it’s a bit of a shakeup from before and will take some time to get used to.

      ZTNA 8.0 overview

      Instead of, for example, creating, at least in the GUI, only a ZTNA server, which has everything in it, you now create:

      • A port, which holds the information on how your clients connect
      • A traffic forwarding server, which binds the port and is used in your policies
      • Destinations, which are the resources accessed using the traffic forwarding server and are also used in your policies

      The decoupling of the traffic forwarding server and the destinations is definitely where I can see the greatest enhancement, because it promotes reusability instead of duplication.

      [Update 2026-05-08]: Since writing this post, I have created a blog post and video about how to configure ZTNA on 8.0 with a Microsoft Entra integration for SAML SSO.  I actually like the new way more.

      FortiGate 8.0 ZTNA configuration – All methods + Entra SAML SSO – KNS | ˈkɛvɪn netsec

      Post-quantum cryptography

      A maybe unexpected big focus of 8.0 is post-quantum cryptography (PQC). PQC isn’t a new thing in FortiOS, with Post-Quantum Preshared Keys (PPK) having been implemented in FortiOS 7.2, and quantum-safe algorithms like ML-KEM and HQC being in 7.6, but 8.0 brings some more things.

      Fortinet has its own Quantum Security Hub if you want to read more about this topic in relation to Fortinet solutions, but let’s look at practical things.

      HTTPS and SSH management use quantum-safe algorithms

      New features or enhancements | FortiGate / FortiOS 8.0.0 | Fortinet Document Library (Feature ID 1202253)

      If you access the FortiGate GUI with a browser that uses quantum-safe algorithms, you will automatically use X25519MLKEM768 to connect. This behaviour, to my knowledge, cannot be changed in FortiOS, which is not a bad thing.

      FortiGate GUI PQC

      On the CLI, you can find mlkem768x25519-sha256 as a default SSH key exchange algorithm.

      FGT-80-REVIEW(Primary) # get system ssh-config | grep ssh-kex-algo
      ssh-kex-algo        : diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 mlkem768x25519-sha256

      Proxy-mode deep inspection supports hybrid PQC

      Support for hybrid PQC SSL deep inspection in proxy mode | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      If you perform deep inspection in a proxy-mode policy, connections are no longer downgraded to a non-hybrid PQC algorithm, bringing an immediate security benefit.

      Agentless VPN supports PQC

      New features or enhancements | FortiGate / FortiOS 8.0.0 | Fortinet Document Library (Feature ID 1152420)

      Agentless VPN, or SSL-VPN web mode for the old-timers, can now use post-quantum and hybrid post-quantum key exchange groups to secure client access.

      FGT-80-REVIEW(Primary) # config vpn ssl settings 
      
      FGT-80-REVIEW(Primary) (settings) # set tls-groups 
      P-521              P-521
      P-384              P-384
      P-256              P-256
      ML-KEM512          ML-KEM512
      ML-KEM768          ML-KEM768
      ML-KEM1024         ML-KEM1024
      P-384-MLKEM1024    P-384-MLKEM1024
      P-256-MLKEM768     P-256-MLKEM768
      X25519-MLKEM768    X25519-MLKEM768
      X448               X448
      X25519             X25519
      FFDHE2048          FFDHE2048
      FFDHE3072          FFDHE3072
      FFDHE4096          FFDHE4096
      FFDHE6144          FFDHE6144
      FFDHE8192          FFDHE8192

      Artificial Intelligence

      We all knew it would come, but Artificial Intelligence (AI) is now in FortiOS in earnest. Previously, AI-anything, in FortiOS, was used for things like anti-virus for malware scanning and recognizing and controlling AI-usage using application control, but now we have so much more, beginning with…

      FortiAI-Assist

      FortiAI assistant and CLI Code Lab | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      If you have used FortiManager or FortiAnalyzer in 7.6, you should be familiar with FortiAI-Assist, but Fortinet puts it best in explaining this new feature:

      “FortiAI can answer technical questions by using the FortiOS documentation and troubleshoot issues by reading logs directly from FortiGate or by analyzing debug outputs provided by administrators. The CLI Code Lab tool helps administrators use natural language prompts to generate and execute complex FortiGate configurations.”

      The option to choose between FortiAI and OpenAI as the provider is nice if you already have an OpenAI plan and don’t want to spend additional money on tokens, though, depending on your FortiCare support status, you get a good amount of FortiAI tokens per month.

      Messages sent to FortiAI use data masking, so sensitive data, like IPs, email addresses, URLs, etc., is masked. The documentation doesn’t make it clear, but I assume it uses the same method as FortiAnalyzer and FortiManager and to-be-masked data is replaced with bogus information before being sent to the LLM and after receiving a response, the replacement is undone.

      The new features entry gives some examples of what you can do, but let’s see what else it can do, like figuring out why a subnet cannot access the internet based only on a prompt (spoiler: I disabled SNAT on the policy).

      FortiAI SNAT problem

      Evidently, there are some issues with FortAI and accessing FortiGate configuration and diagnosis. Maybe these are guardrails, but for the time being, I have only executed the debug flow commands because FortiAI put an “especially” there.

      If you click on the “Completed thought process” button, you can also see what steps are being performed in all of this.

      FortiAI thinking

      Let’s give it the debug output and check again.

      FortiAI SNAT debug result

      So it gave some good information here, but the important command, get firewall policy 1, does not work. Another try, and it delivered the correct command and also the solution.

      FortiAI SNAT policy problem

      As another example, let’s see how it handles local-in policy creation.

      FortiAI local-in prompt
      FortiAI local-in result

      It took just a bit of help, but it got there. The logtraffic setting doesn’t exist, but I’m not going to hold it against it. In my prompt, you can also see that masked pieces of information are highlighted.

      CLI Code Lab

      FortiAI assistant and CLI Code Lab | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      This is not an AI feature, but it was introduced with it, so I put it in this section to not spoil the surprise of FortiAI-Assist.

      The CLI Code Lab is awesome. If tags are my number 1, this is my number 2. What the CLI Code Lab offers is a no-fuss method to access the stage-and-commit/transaction feature in FortiOS.

      You write your configuration in the code lab, execute it, which creates a transaction, and then you can commit it. The great thing is that the transactions are not immediately being written to memory, i.e. the running configuration, so you can do several configurations and only commit them if you are happy with the outcome. This helps when you need to make changes that, if done individually, would cut off your access, for example. There are ways around that, like scripts, but the code lab gives immediate feedback if something wouldn’t work, and it’s not committed immediately.

      CLI Code Lab transaction start
      CLI Code Lab failure

      Here is an example flow of me changing the hostname, it not immediately being reflected in the CLI, showing the transaction information, and verifying the new hostname on the CLI after commiting the changes.

      CLI Code Lab transaction
      FGT-80-REVIEW(Primary) # get system global | grep hostname
      gui-display-hostname: disable
      hostname            : FGT-80-REVIEW
      
      FGT-80-REVIEW(Primary) # diagnose sys config-transaction show txn-info
      txn_id=4, expire=11 seconds, user='admin', userfrom='https(192.168.1.101)', clicmd_fpath='/dev/cmdb_priv/txn/4_ELjGss.conf'
      
      FGT-80-REVIEW(Primary) # diagnose sys config-transaction show txn-cli-commands 4
        config system global
            set hostname "CLI-CODE-LAB"
        end
      
      COMMIT CHANGES IN THE GUI
      
      CLI-CODE-LAB(Primary) # get system global | grep hostname
      gui-display-hostname: disable
      hostname            : CLI-CODE-LAB

      The way the code lab does its thing, i.e. using transactions, isn’t anything new. In the background, it uses workspace mode, introduced in 6.2, and the corresponding transaction process, but now we have a GUI way to use this feature, making it more approachable. Again. Awesome. One thing that I hope the code lab gets in the future is syntax autocomplete, like FortiManager.

      Use AI and ML to detect malicious activity in DNS traffic

      New features or enhancements | FortiGate / FortiOS 8.0.0 | Fortinet Document Library (Feature ID 1157379)

      DNS is an often-used candidate for data exfiltration and command-and-control traffic, so this new feature, if it works well, is an easy win.

      This feature also comes with a whitelist of some domains that are exempted from this type of scanning, which I’m not a fan of.

      There doesn’t seem to be any information on how it works, or if you can configure it in any way, so we just have to rely on it working correctly.

      MCP and A2A detection support

      Agentic AI protocol support in FortiOS | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      Model Context Protocol (MCP) is a protocol that connects AI applications to external systems, so that the individual applications don’t need individual methods to access external systems. The MCP server acts as a kind of translator so the two sides can communicate with each other.

      The Agent2Agent (A2A) Protocol is used by AI agents to communicate with each other using a shared language.

      FortiOS 8.0 can now detect applications using these protocols and also display detailed information about what is happening in these communications.

      Imagine a developer using Visual Studio Code connected to an MCP server. If the developer interacts with the GitHub repository through the MCP server, the accessed files, issues, etc., can be recognized in logs now.

      With the growing adoption of MCP, this functionality becomes more and more important.

      Rapid-fire new features

      Let’s hammer out some new features quickly, because I do believe they are important, but there isn’t much to say about these.

      Managed FortiSwitch trusted hosts and Private Data Encryption

      Support trusted host settings for managed switches | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      Provide private data encryption for managed switches | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      Both trusted hosts and Private Data Encryption aren’t new to FortiSwitchOS, but before 8.0, you had to painstakingly configure these manually on each FortiSwitch or by using custom commands. With FortiOS 8.0, you can now do this using the switch controller feature, making securing your FortiSwitches that much easier.

      HA role is visible in CLI prompts

      HA role shown in CLI prompt | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      A feature I loved on Cisco ASAs, and missed on FortiGates, is knowing immediately, on the CLI, if the current device is a primary or a secondary. Now this is possible, and using get system status or get system ha status is no longer necessary for this.

      Log summary panel

      Summary panel in Log Details | FortiGate / FortiOS 8.0.0 | Fortinet Document Library

      Forward Traffic logs now display a summary panel in the details that gives you a quick overview of the most important things, like the policy ID, policy action and why something was blocked, including UTM profile and action. This feature got lost in a previous version (I want to say 7.2) due to other changes, and now we have it back.

      Disallowed login methods per administrator

      Administrator access hardening | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      You can now restrict certain administrators from using specific login methods. This feature is especially useful because you can restrict console logins, meaning that you can make a break-glass administrator account that can only use the console and disallow it for all others.

      FortiAP IPsec traffic offloading

      Support IPsec traffic offloading from the FortiAP | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      Managed FortiAPs, by default, send their data traffic in cleartext. Previously, if you enabled a secure channel for this type of traffic, it wasn’t offloaded. With IPsec, it is now, increasing security at little to no loss in data throughput.

      FortiClient can send security posture tags to FortiOS directly for VPN connections

      Send security posture tags from FortiClient to FortiOS directly as JWT | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      FortiClient 7.4.5 and FortiClient EMS 7.4.4 introduced the concept of the ZTNA Token, which improves ZTNA tag resiliency because the tags could be sent to a FortiGate in JSON Web Token (JWT) format, and if EMS is not available, tags could still function. The feature can now be used for VPN connections.

      Application sanctioning

      New classification framework for application signatures and FortiView support | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      Applications can now be marked as sanctioned and unsanctioned. This status can be used in Application and Filter Overrides to easily allow only specific applications, especially since you can change the default application classification to unsanctioned. That way, you have to manually sanction applications.

      The negative one: Changes to revisions

      Configuration revisions and logout backup default changes | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      It’s not all good in FortiOS 8.0. The changes to revisions on FortiGates are simply bad.

      You can no longer use revisions in any way if your FortiGate does not have a hard disk.

      I assume this was done for performance reasons, but it still sucks to see it.

      The most important one: Custom GUI themes

      Custom GUI themes and admin‑level personalization | FortiGate / FortiOS 8.0.0 | Fortinet Document Library 

      No longer do we have to rely on Fortinet-provided themes for the GUI. With a full palette of colors and our own hex code combinations, we can display our FortiGate in our colors and give every administrator the choice to pick their own theme.

      Take this moment to join me in a hot pink FortiOS.

      Hot pink
      config system theme
          edit "HOT-PINK"
              set border-radius enable
              set header-color "#ff46a2"
              set selected-color "#ff46a2"
              set call-to-action-color "#ff46a2"
              set accent-color "#ff46a2"
              set banner-msg ''
          next
      end

      Jokes aside, this feature was implemented because customers wanted specific colors to classify their firewalls for security and practical reasons. Edge firewalls get a theme, internal firewalls another, testing environments this one, production environments that one, etc.

      Wrapping up

      [Update 2026-04-27]: Since writing this post, Fortinet has released its own overview video, which covers only a few of the features mentioned in this post. Maybe give it a watch: What’s New in FortiOS 8.0 | Product Overview

      Historically, the X.0.0 release gave the direction of where FortiOS is headed and further releases, especially X.0.1, brought lots more interesting features to solidify the direction. At this moment, we can only imagine what such a release holds for us, and I have my own wish list for this, and I’m sure you, dear reader, have your own.

      With that, look at the other new features of FortiOS 8.0, and maybe you will find one that you think is just the best.

    • End-to-end FortiGate ZTP with FortiManager for SD-WAN and ADVPN

      Zero Touch Provisioning (ZTP) is a wonderful thing that can turn hours into minutes, reduce errors, and help with standardization, among other things. In this post, dear reader, I want to go through the entire process, starting with a factory-reset FortiGate, to one that is fully integrated into an Auto-Discover VPN (ADVPN) environment with SD-WAN. We’ll see FortiZTP, FortiManager, Jinja, and so much more along the way. Read on, and I hope you find this entire process just as cool as me.

      Acknowledgement

      Before I get into the topic proper, there is one person I need to thank. Dennis Holzmann has given me some valuable information and tips in the past on this topic, and some of them are reflected in this post, as well as my work. This post would look different, and worse, without his input, so thank you, Dennis!

      The setup

      For this post, I have the following components:

      • A FortiGate VM running 7.6.6, serving as a hub
      • A FortiGate 70G running at 7.6.6, serving as a branch
      • FortiManager running 7.6.6
      • FortiZTP, a free cloud service

      The GitHub repository

      The purpose of this post isn’t the actual configuration that is being done, but the process. I will highlight parts of the configuration where I think it’s useful, but for the most part, the configuration of the deployment is up to you. If you are still interested in the configuration I am using, you can check out my Fortinet resources GitHub repository. The finished FortiGate configurations, exports of FortiManager templates, the ZTP pre-run script, and more are all there.

      A high-level look at ZTP

      Breaking down ZTP provisioning of FortiGates in the Fortinet ecosystem in its most basic steps is as follows:

      1. A factory default branch FortiGate gets an IP, and thus a WAN connection, via DHCP
      2. The branch FortiGate contacts FortiZTP for partial provisioning
      3. A VPN tunnel, as well as a BGP neighborship, gets established between the branch and the hub
      4. The branch FortiGate connects to FortiManager via the VPN tunnel
      5. FortiManager fully provisions the FortiGate
      Diagram of ZTP flow

      The process itself is simple, but there are a decent amount of pieces we first have to prepare so our ZTP workflow runs smoothly.

      Let’s first look at the general FortiManager pieces.

      Preparing FortiManager for ZTP

      On the hub, we need to prepare four things before we dive into templates:

      1. Device groups
      2. Metadata variables
      3. Policy packages
      4. Blueprints

      Let’s take it from the top.

      Device groups

      To make sure that our FortiGates get the correct configuration, we will target groups as much as possible. How many groups you want to create depends on your needs, but I usually see the need for four:

      1. Hubs
      2. Branches
      3. Branches with single WAN
      4. Branches with dual WAN
      Created device groups

      I think the groups are self-explanatory.

      A FortiGate might start in a single WAN group, and if it ever gets a second WAN connection, you can simply move the device to the dual WAN group and all the necessary things, the second IPsec tunnel and second SD-WAN member, for example, get automatically provisioned and installed on the next configuration push.

      The branches group is useful if you ever need to run a script against all branches and don’t want to pick the WAN-related groups, and it is also used for the template group.

      Metadata variables

      A magical aspect of FortiManager are metadata variables. You can create these variables, assign mappings for devices and/or groups, with an optional default value, and use them in a whole host of configurations in FortiManager. You see the possibility of using metadata variables in fields by the looking glass symbol of a dollar sign Icon to show metadata variable possibility, or you know that you can use them in CLI templates and scripts when using both CLI and Jinja.

      Again, how many and what metadata variables you need is up to your needs. I need the following for this post:

      • branch_id
      • cluster
      • hostname
      • wan1_port
      • wan2_port

      branch_id is a variable that will be used for the site subnet and loopback IP for the most part. The cluster variable signalizes if this is an HA cluster, because some things will be different for these installations.

      hostname, wan1_port, and wan2_port are self-explanatory.

      Policy packages

      Without policies, not a whole lot will work, so we need policy packages for the hubs and our branches. It is recommended to create one policy package for all branches and try to make it as homogenous as possible, because branches should be treated like cattle, not pets.

      The branches only need one policy, which is a policy that uses the SD-WAN zone with the IPsec overlays as members, which will be created later, because otherwise the IPsec tunnel to the hub won’t ever come up. Everything else is up to you.

      The hub needs the standard policies for an SD-WAN/ADVPN deployment with BGP.

      1. Branch IPsec SD-WAN zone to the BGP loopback to allow BGP traffic
      2. Branch IPsec SD-WAN zone to the health check loopback to allow ping traffic for our SLA
      3. Branch IPsec SD-WAN zone to Branch IPsec SD-WAN zone for shortcut tunnels

      Again, everything else, including policies that allow traffic to LAN resources, is up to you. My policies are visible in the GitHub repository and here:

      Policy of the branches
      Policy of the hub

      Blueprints

      We don’t want to spend lots of time in a ZTP deployment creating model devices, or waiting for the actual device to come online, and assigning things. We use blueprints to speed up the process.

      Pick your device model, enable auto-linking, enforce a firmware version and the device configuration, assign groups, templates, a policy package and configure HA all in one fell swoop and reuse it hundreds of times. In the blueprint, we also define that we use the SD-WAN Manager for all branches, and the hub(s) will be added manually.

      The blueprint you create will then be used to create model devices that define how your FortiGate will look once FortiManager has provisioned it.

      I got one blueprint for single WAN, one for dual WAN and a third one for HA (more on HA later).

      With all our non-template pieces in place, we can now turn to templating, and we will start with the branches.

      Overlay Orchestration

      Just joking, one more point. I want to point out that the SD-WAN Manager offers a feature called Overlay Orchestration. This feature creates an SD-WAN Overlay Template, which builds the necessary templates for you, and you only need to supply some values regarding the configuration (topology, loopback IPs, AS number, groups, etc.). This is a quick and easy way to get started, but I wouldn’t recommend using it except to get the initial templates. The reason for that is that if you ever edit the overlay template, it will overwrite all custom settings you made in the templates.

      Once the Overlay Orchestration feature gets the capability to only edit its own settings in templates, it will be a great way to handle an entire rollout.

      Now onto the branches.

      Templating the branches

      I configured the following templates for branches:

      • A system template
      • CLI templates that are in a CLI template group
      • A BGP template
      • An IPsec tunnel template
      • An SD-WAN template

      Let’s look at each in turn. I will not spend much time on the configuration of the templates; see the GitHub repository for that, but only point out noteworthy points.

      System template

      The system template houses options like DNS and NTP servers, the hostname, SNMP, and logging. These options are, of course, specific to your deployment, but one you should keep in mind is the Log Settings option, where you can tell devices to log to FortiAnalyzer, and if FortiAnalyzer is managed by FortiManager, it’s even easier to do.

      CLI templates

      CLI templates, and the pre-run CLI templates, is where lots of magic happens, not just because you can use metadata variables in them, but because you can use Jinja to make configurations that have some logic in them, like if-else and for loops and functions, like ipmath. I want to highlight three things here:

      1. You can import CLI templates in other templates. At first reading, this doesn’t make much sense, but since you can use Jinja, a CLI template can be used to house variables that, after an import, can be used in other CLI templates.

      Consider the classic case of creating VLAN interfaces on branches. It’s not rare for branches to have 5+ VLAN interfaces, maybe one is for management and needs different management access, and some with DHCP relay enabled. You can create a CLI template and write out the configuration as is, but this definitely isn’t cool, and the cool-factor has a certain charm.

      Instead, create a CLI template that holds a variable with your VLAN interface information, import it in another CLI template, loop through your interfaces, and make the individual changes as you need them. If you ever need to add another VLAN interface, just copy the line and replace the values.

      In my configuration, I have done just that, so take a look:

      {# PROJECT VARIABLES #}
      {# INTERFACES #}
      {% set interfaces = {
        'vlan_interfaces' :[
          { "id": 100, "alias": "TEST100", "suboctet": 100, "dhcp_relay": true},
          { "id": 101, "alias": "TEST101", "suboctet": 101, "dhcp_relay": false}
        ]
        }
      %}
      {# CREATE INTERFACES #}
      {%- import 'CLI-000-PROJECT-VARIABLES' as project_variables with context -%}
      
      config system interface
        edit "LAG_INTERNAL"
          set vdom "root"
          set type aggregate
          set member "port1" "port2"
        next
      {%- for vlan_interface in project_variables.interfaces.vlan_interfaces %}
        edit "VL_{{vlan_interface.id}}"
          set vdom "root"
          set type vlan
          set vlanid {{vlan_interface.id}}
          set interface LAG_INTERNAL
          set alias {{vlan_interface.alias}}
          set ip 192.168.{{vlan_interface.suboctet}}.1/24
          {%- if vlan_interface.id == 100 %}
          set allowaccess ping https ssh snmp fgfm
          {%- else %}
          set allowaccess ping
          {%- endif %}
          {%- if vlan_interface.dhcp_relay %}
          set dhcp-relay-service enable
          set dhcp-relay-ip "192.0.2.1"
          {%- endif %}
        next
      {%- endfor %}
      end

      2. FortiManager comes equipped with some predefined variables you can use in Jinja. You can use these to target only specific models, using {{DVMDB.platform}}, loop through interfaces using {{DEVDB_system_interface}}, and much more. I am using just such a variable in the interface CLI template from point one to check if a second IPsec interface exists to set the management access on the interface.


      3. The Pre-Run CLI Template is referenced in the blueprint and runs only once. You can use this for whatever you want, but I like to use it to initially define a password for administrators, because a factory-reset FortiGate has an empty password for the admin user, which is a security risk.

      BGP template

      There isn’t much to say about BGP templates. It’s a standard configuration for BGP on loopback, which is the recommended deployment. The only thing of note is that I use a route map, referencing a prefix list, to make sure only the local summary subnet can get announced, e.g. branch with ID 101 announces only 10.101.0.0/16. This saves you from some misconfiguration, but how you do this is up to you. You can announce all connected routes if you want, but even then, I’d restrict it to the private IP address space using a prefix list, because otherwise, you might also announce the public IP space that is on the branch’s WAN port.

      IPsec tunnel template

      IPsec is, again, pretty standard with a BGP on loopback configuration using network IDs. I have two tunnels in the template. One for overlay 1, using the wan1 port, and overlay 2, using wan2, and the tunnels target different device groups. Single and dual WAN for both overlays, and only the dual WAN group gets overlay 2.

      IPsec installation targets

      One part that might be interesting is that I have enabled mode config on both tunnels despite using BGP on loopback. The reason is that when the hub gives out IP addresses to the branch IPsec interface, we can do two things:

      1. Enable management access on the IPsec interface, and the hub can then connect to the branch via the tunnel, which can be helpful in a pinch. Just make sure the hub is allowed according to trusted hosts and local-in policies.
      2. It saves you from having to define the source IP for all your possible local-out traffic (FortiAnalyzer, DNS, NTP, RADIUS, etc.), because FortiOS, unless defined otherwise, will use the IP of the tunnel interface as the first choice to source this traffic.

      SD-WAN template

      In the SD-WAN template, we configure ADVPN 2.0, without transport groups, and use embedded SLA information in ICMP probes.

      There are two SD-WAN zones:

      1. SDWAN-HUBS, with both overlays
      2. SDWAN-OUTSIDE, with both WAN ports

      Both these zones have both members defined, but the installation target narrows down who gets what.

      SD-WAN member installation targets

      In the SD-WAN Rules, both members are defined, and we install these rules on the branch group, not the single/dual WAN groups. FortiManager is smart enough to only push the SD-WAN members that actually exist on the device in these rules.

      SD-WAN rules on FortiManager and FortiGate

      Templating the hub

      The hub, already registered and synchronized in FortiManager, performs the following functions:

      • Terminates dial-up IPsec connections of branch devices
      • Forms a BGP neighborship with all branch devices
      • Acts as a BGP route reflector (optional)
        • If you use dynamic BGP for your spokes, you don’t need the hub to be a route reflector
      • Serves as the ADVPN hub

      The templates for the hub are very standard, so I can keep the noteworthy things short:

      • CLI templates: As necessary
      • BGP templates: Very standard, except here I’m using a route map with a prefix list to allow only private IP addresses from the RFC1918 space
      • IPsec tunnel templates: It’s a dial-up hub with two dial-up tunnels and mode config enabled to hand out IPs, as mentioned in the branch section and it also gets an IP address just outside of that range (the following picture is edited, and the Tunnel Interface Setup section is further down)
      IPsec mode-cfg and IP on the hub
      • SD-WAN: Uses a remote performance SLA with priority IN-SLA and priority OUT-SLA values to dynamically steer branch traffic.
      Remote SLA on the hub

      Now that we have all our templates, we can create the template groups and assign them to our groups.

      Template groups

      FortiZTP

      This is the thing that starts the entire process in earnest. FortiZTP is a free service Fortinet offers to facilitate Zero Touch Provisioning of FortiGates, FortiAPs, FortiSwitches, and FortiExtenders. It is available in the Services section of support.fortinet.com (click on “Show More”) or at https://fortiztp.forticloud.com. As soon as a supported device is an asset of your FortiCloud account, it will show up in FortiZTP ready to be provisioned.

      ZTP assets

      If a FortiGate starts from a factory default/reset state, it will:

      1. Try to pull an IP address using DHCP on ports that have the DHCP client enabled. The ports vary by model, but for the smaller devices, the WAN ports have it enabled.
      2. Try to connect to FortiCloud and look if it is provisioned for ZTP
      3. Connect to a selected target:
        1. FortiGate Cloud
        2. FortiManager Cloud
        3. An on-premise FortiManager
      4. Execute an optional pre-run CLI script

      [Update 2026-04-08]: I wrote factory default/reset state, and this is important because if the configuration has been touched, the ZTP process will never be started by the FortiGate. You can check the status using thediagnose fdsm fmg-auto-discovery-status command. If the output shows config-touched=1, no ZTP will happen. Thanks to Chris Eddisford from Fortinet for reminding me about this command.

      FortiGate config touched

      The pre-run CLI script is the first thing we define. It has all the information the FortiGate needs to connect to FortiManager. At its most basic, and keeping the BGP on loopback configuration in mind, we need six things:

      1. The loopback interface for BGP
      2. An IPsec configuration
      3. A BGP configuration
      4. An SD-WAN configuration for our member and zone
      5. A policy that uses the SD-WAN zone so that the tunnel establishes
      6. The FortiManager configuration

      An optional seventh step is the breaking of the default switch interface, as well as the deletion of the fortilink interface. This is included in my pre-run CLI script. Note that the name of the switch might be different for your model.

      My somewhat overblown, pre-run CLI script is here:

      ZTP pre-run CLI script

      config system interface
      edit "LO_BGP"
      set vdom "root"
      set ip 172.25.1.252 255.255.255.255
      set allowaccess ping
      set type loopback
      next
      end
      config vpn ipsec phase1-interface
      edit "HUB1-O1"
      set interface "wan1"
      set ike-version 2
      set dhgrp 32
      set proposal aes256gcm-prfsha512
      set keylife 28800
      set peertype any
      set mode-cfg enable
      set remote-gw 192.0.2.1
      set idle-timeout enable
      set idle-timeoutinterval 10
      set net-device enable
      set add-route disable
      set exchange-interface-ip enable
      set exchange-ip-addr4 172.25.1.252
      set auto-discovery-receiver enable
      set psksecret ENC JVojDRO7n0Wkft6+Ts9vwzs0m891XrhCH5/N/fU5xATS+bTAflRfv/tS/vFpjOyZ2+1FZcg+aniAcpQ0uUsUCrBIF6jPfVEfPHSXsZW5KE+DiDoOBWIFp7mgkgdZppgPZWZbaErBlQ0R6koV1N0bCgByN4otCT+nBSU1DorQBa5Z5jifcYIiTtdUk5RulqIjBRcnbFlmMjY3dkVA
      set network-overlay enable
      set network-id 10
      set auto-discovery-shortcuts dependent
      set exchange-fgt-device-id enable
      set transport auto
      set shared-idle-timeout enable
      next
      end
      config vpn ipsec phase2-interface
      edit "HUB1-O1"
      set phase1name "HUB1-O1"
      set proposal aes256gcm
      set auto-negotiate enable
      set dhgrp 32
      set keylifeseconds 3600
      next
      end
      config router prefix-list
      edit "PL_RFC1918"
      config rule
      edit 1
      set prefix 192.168.0.0 255.255.0.0
      unset ge
      set le 32
      next
      edit 2
      set prefix 172.16.0.0 255.240.0.0
      unset ge
      set le 32
      next
      edit 3
      set prefix 10.0.0.0 255.0.0.0
      unset ge
      set le 32
      next
      end
      next
      end
      config router route-map
      edit "RM_SDWAN-OUT"
      config rule
      edit 1
      set match-ip-address "PL_RFC1918"
      next
      end
      next
      end
      config router bgp
      set as "65123"
      set router-id 172.25.1.252
      set keepalive-timer 5
      set holdtime-timer 15
      set ibgp-multipath enable
      set additional-path enable
      set scan-time 15
      set graceful-restart enable
      set recursive-next-hop enable
      set tag-resolve-mode merge
      set recursive-inherit-priority enable
      config neighbor
      edit "172.25.1.254"
      set advertisement-interval 1
      set activate6 disable
      set capability-graceful-restart enable
      set link-down-failover enable
      set soft-reconfiguration enable
      set interface "LO_BGP"
      set remote-as "65123"
      set route-map-out "RM_SDWAN-OUT"
      set keep-alive-timer 5
      set holdtime-timer 3
      set connect-timer 15
      set update-source "LO_BGP"
      set additional-path both
      next
      end
      end
      config system sdwan
      set status enable
      config zone
      edit "SDWAN-HUBS"
      next
      end
      config members
      edit 1
      set interface "HUB1-O1"
      set zone "SDWAN-HUBS"
      next
      end
      end
      config system dhcp server
      purge
      end
      config system ntp
      set server-mode disable
      end
      config firewall address
      del lan
      del internal
      end
      config firewall policy
      purge
      end
      config system virtual-switch
      del lan
      del internal
      end
      config system interface
      del fortilink
      end
      config firewall policy
      edit 0
      set name "DUMMY VPN"
      set srcintf "LO_BGP"
      set dstintf "SDWAN-HUBS"
      set action deny
      set srcaddr "all"
      set dstaddr "all"
      set schedule "always"
      set service "ALL"
      set logtraffic all
      next
      end
      config system central-management
      set type fortimanager
      set serial-number {{fmg-sn}}
      set fmg {{fmg-ip}}
      end

      Note: The interface binding for the pre-run script varies model by model. A 50G only has wan, for example, instead of wan1 and wan2 for the 70G. Keep this in mind when making your scripts and create several scripts that map to your devices.

      I have purposefully removed all leading tabs and whitespaces from the script to reduce possible errors when the script runs.

      With the pre-run script, we can define our FortiManager and attach the script.

      FortiManager in FortiZTP

      Now we can go to our Assets, select our FortiGate(s), and provision them to target our FortiManager. Here you can also select a different script if the default one attached to your FortiManager isn’t the one you need.

      ZTP settings for FortiGate

      Now our FortiGate is ready to be provisioned by ZTP.

      ZTP provisioning waiting status

      The CSV file

      The last piece of, technically optional, preparation we need for ZTP is a CSV file to create our model devices. The CSV file, at a minimum, needs the serial number, the name of a blueprint, and the name the device will have in FortiManager.

      You can, and probably will, add several metadata variables in addition to that to the CSV. With the CSV file in hand, you can use it to create your model devices, look over the assigned values, and continue with your import.

      If you ever need to give the creation of the CSV off to someone else who might not be as technically inclined as you, consider creating an Excel sheet to easily fill out and select values. After that, it can be exported to a CSV.

      An example CSV file is provided in the GitHub repository.

      Auto-linking

      The process of connecting an incoming FortiGate, using its serial number, to a model device is called auto-linking. After a successful link, the real FortiGate will get handled according to the blueprint, meaning the firmware might get upgraded, it gets added to groups, a policy package gets pushed, etc.

      You can see the status of auto-link in the corresponding column. It will go from Enabled to Auto-linking and disappear once it is done.

      Auto-link enabled status
      Auto-link running status

      Auto-linking creates two tasks on FortiManager:

      1. Push config to device: This signalizes the pushing of the entire configuration to the FortiGate
      2. Install Package ‘<PACKAGE-NAME’>: Installs the policy package
      Auto-link success

      If either of these tasks fails, look at FortiManager’s Task Monitor and the installation reports in the tasks to find out where your errors are. In the following picture, I forgot to first install the device configuration to the model device, so the SDWAN-OUTSIDE zone was missing, and that is needed in the policy package.

      Failure of auto-linking in Task Monitor

      Now we finally got everything, so let’s look at the process from start to finish.

      The end-to-end ZTP process

      Let’s now go through the entire process of how to ZTP a FortiGate.

      1. Provision the FortiGate on FortiZTP
      2. Create a model device on FortiManager using the CSV
      3. Do a Quick Install on the model device
        1. This is necessary if you are enforcing the device configuration in the blueprint, because the installed configuration is needed for everything else to correctly install, like firewall policies
        1. Connect your FortiGate, which is factory reset or booting for the first time, on ports that have DHCP client enabled, to a modem/router for internet access
        2. The FortiGate will try to contact FortiZTP to check for any provisioning
        3. The pre-run CLI script will be downloaded and run
        4. The FortiGate will connect to FortiManager
        5. FortiManager will auto-link the FortiGate to the model device
        6. If enforced in the blueprint, the firmware of the FortiGate will be upgraded
        7. If the firmware was upgraded, signatures and databases like the ISDB, IPS, and application control might get upgraded as well
        8. The full configuration, including the policy package, gets pushed to the FortiGate
        9. On FortiManager, the config status becomes Synchronized, the policy package status gets a green check mark, and on FortiZTP, we see Provisioned
        10. ZTP is done
        Update of signatures during auto-link
        Auto-link success
        ZTP provisioned

        I have made a short video about the entire process if you want to see it in action:

        Note: In some cases, I’ve seen an “Out of Sync” message shortly after the ZTP process is done, which can be related to things like FortiTokens that get provisioned after the device comes online. These things should get corrected with the next install.

        The process is now explained, and hopefully clear, but we’re not done.

        ZTP with FortiGate clusters/HA

        Most ZTP installations I’ve seen are single devices, but there is a need to create clusters during ZTP as well. This has some caveats, but it’s nothing insurmountable.

        It all starts with the blueprint, where, after enabling the HA option, you can fill out information relating to the monitor and heartbeat interfaces and assign a password for the cluster.

        Next, you need a CSV that handles the cluster configuration. This CSV has some more necessary values in addition to the serial number, blueprint, and the name. The new values are:

        • cluster id: Defines the group ID of the cluster, which is important if you ever have two FortiGate clusters in the same layer 2 network
        • cluster name: Self-explanatory
        • Priority: Sets the cluster priority of each FortiGate in the cluster, and needs to be different between the two units 
        • Mode: Either AP for Active-Passive or AA for Active-Active

        Note: The name value needs to be different for each device in the CSV; otherwise, the import will not work correctly.

        An example CSV for a cluster is available in the GitHub repository.

        Once the model HA device cluster has been imported, the process is the same as for a single device; you just need to make sure that your FortiGates are correctly cabled up.

        If you ever import multiple clusters, the cluster ID is responsible for differentiating between them, and this is color-coded in the import.

        Importing multiple HA clusters

        Important point: I have not found a way to supply FortiManager metadata variables using the CSV for an HA model device, so this is something you have to do manually after the import. This is easily done by right-clicking on the model device and selecting “Edit Variable Mapping”. If someone knows how to do this, please tell me.

        The hostnames of the HA devices will look a bit strange after the import, and in my configuration, the hostname also won’t get changed by a CLI or System template, because HA hostnames are individual to each cluster member.

        HA model device hostname issue

        When it comes to FortiZTP, it is important to have a second pre-run CLI script for the second member in a cluster, because of the overlapping IPs and router IDs, which create a problem during the ZTP process on FortiManager. So you need to adjust the following in the second CLI script:

        • The IP on the loopback interface
        • The BGP router ID
        • The exchange IP address in the IPsec phase 1 configuration

        In FortiZTP, you then provision the first member with the regular pre-run script and the second member with the second script.

        Scaling beyond 253 branches

        This is a niche case, but if you ever have more than 253 branches and only one hub, the regular way to use the branch_id metadata variable for your loopback IP and other things breaks down. If your loopback IP with the variable looks like 10.10.0.$(branch_id) and branch_id is 300, this obviously cannot work.

        If this case applies to you, you will have to use Jinja CLI templates to set these things instead of regular CLI templates and set these values in IPsec, BGP and SD-WAN templates. FortiManager luckily comes with the Jinja ipmath function that helps us here.

        By using {{'10.10.0.0'|ipmath(branch_id|int)}}, you can scale to much more than 253 branches, for example. With the above branch_id of 300, this would lead to the IP 10.10.1.44.

        How you handle the addressing for your networks, and thus the BGP announcements, in such a case is up to you. At that point, you might have to create a metadata variable that defines the local network as a summary and map it to each device. In my CLI template, I have created an if condition that only creates a network entry in BGP when the branch ID is lower than 256, because everything else would result in an error, and it also uses a route map to filter to RFC1918 space instead of only the local network summary.

        In the GitHub repository, you can find a CLI template called “CLI-004-IPMATH” that has all the settings where you, at a minimum, need to use the ipmath function if this case applies to you.

        Additional resources

        I am, of course, not the first person to create content about this topic, and this post isn’t about the configuration itself, but the process. If you would like some more information on how configurations can work, please check out these two resources by Fortinet themselves:

        • 4-D SD-WAN resources: Includes documents detailing the topologies, deployment guides, explanations, and GitHub repositories with working example configurations. Thanks to Cameron Keighley and kli who are the GitHub contributors!
        • The Jinja Orchestrator: A repository containing best practice SD-WAN/ADVPN configurations and templates, you can deploy with or without FortiManager. It’s an extensive and fantastic resource. Thanks to Dmitry Perets and Boris Henicker, who are the GitHub contributors!

        Enough ZTP

        I think I have touched on everything I wanted to cover regarding the current state of Zero Touch Provisioning with FortiGates and FortiManager. It might seem like it’s a lot, but the process is quite simple once you’ve seen it.

        With all this information, I wish you, dear reader, happy zero touch provisioning.