Tag: forticlient

  • 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.

  • Exploring the FortiClient EMS API

    I am not a fan of the official FortiClient EMS API documentation that is available on the Fortinet Developer Network (FNDN). It is a bare-bones documentation that is sparse on explaining how to interact with the API, has very few examples, no responses, lacks a lot of API endpoints, and the endpoints that exist are badly documented (I challenge you to try creating a ZTNA tag with an associated rule using only the documentation).

    To offer a bit of help to you, dear reader, in this regard, I went through the most common configurations when working with EMS, how you accomplish these tasks using the API, and I will also show you how you can help yourself when working with the API.

    The setup

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

    All the API examples I give will be done using Python. Everything featured in this post will also be on the Fortinet resources GitHub repository.

    I am using an on-prem EMS. Cloud EMS is a bit different, especially when it comes to the login, and you have to adapt your own scripts accordingly. See the official documentation for Cloud EMS.

    The most important thing: Reverse engineering the API

    I want to give the best advice first, and in this case it’s getting comfortable with reverse engineering API calls.

    In practice, this means that you do something in the GUI and simultaneously use the browser tools to find out what happens, because most actions in the GUI create the exact API call you need to accomplish this in your API tool of choice, be it Python, cURL, or anything of the like.

    Let’s take the example mentioned in the intro: Creating a ZTNA tag with an associated rule

    EMS Security Posture Tag information

    As we can see, I have this ZTNA tag named BROWSER-TAG-NAME, with a user notification message, a comment, and a rule that validates on FortiClient that the user is part of the AD group ZTNA_USERS.

    Once I hit save on this and with my browser tools open and recording, I can see, on Chrome, in the Network tab, a create call. In the Headers tab of this call, I can see the Request URL https://192.168.1.208/api/v1/tags/zero_trust/create and the Request Method of POST.

    Browser tools headers for reverse engineering

    On the Payload tab, after clicking on View Source, I can see the full JSON payload.

    Browser tools payload for reverse engineering

    The Response tab also shows the response, which isn’t that important.

    With these three pieces of information, the URL, method and payload, I can recreate this API call and find out what all the information in the JSON payload means.

    This approach of reverse engineering is, with the current state of the official documentation, invaluable.

    Now let’s get to the meat of this post.

    AI usage disclosure

    There is a function called deep_merge, which I will call out when it is used, that is purely written by Claude Sonnet 4.6. It is used to update a dictionary with new information, also called a deep merge. I did this because it would have taken me too much time to do it myself, and on that day, it was too late, and I just wanted to get this part done.

    Every other piece of code you see in this post is written by me.

    How I structure this

    Just so I don’t have to repeat myself, I will explain how I approach each section concerning the example I give.

    I will start with an explanation of what is being done and some additional information where required, embed the Python script I have made (again, check the GitHub repository if you want to have it all in one place), post the response EMS gives for the relevant API call, and write some more about it, if there is anything to write about.

    All the Python scripts are, hopefully, well commented. Most of the things being done are basic, and only some parts require special attention, which are explained in more detail.

    I am not good at Python, so if you believe any of my scripts are bad, please keep that in mind.

    Logging in, getting your token and logging out.

    Keep in mind that this is for on-prem EMS. Cloud EMS handles logging in differently.

    The first step you need to make when you want to work with the FortiClient EMS API is logging in, and for that, you need an administrator. There is nothing special about this administrator, so on EMS, head to Administration -> Admin Users and add your user with the permissions you need. Setting Trusted Hosts is a good idea.

    Once you log in to the API, you get a Cross-Site Request Forgery (CSRF) token in your cookies. It is important to know that this token is in your cookies; it is not sent in the response you get after logging in.

    This token has to be used for various API endpoints when you create, update or delete information. Endpoints where you only GET information usually don’t need this token.

    After you are done with whatever you need to do using the API, it is common courtesy to perform a logout, where you supply your token.

    Login, get token, logout

    '''
    ems_login_token_logout.py
    Perform a login on the FortiClient EMS API, get the CSRF token and then logout
    '''
    
    import requests
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)

    Login, get token, logout response

    {
       "result":{
          "retval":1,
          "message":"Login successful."
       },
       "data":{
          "login_domain":null,
          "is_password_insecure":false,
          "site":"Default"
       }
    },
    {
       "result":{
          "retval":1,
          "message":"Logout successful."
       }
    }

    Authorizing and editing a FortiGate

    In order for FortiClient EMS to share endpoint information and ZTNA tag information with a FortiGate, the FortiGate needs to first get authorized. After that, you have to decide what tags you want to share and from which endpoints. Often, you want to share Security Posture Tags, and you want to Share All FortiClients, and that is what this script does.

    Authorize and edit FortiGate

    '''
    ems_fgt_authorization_share_clients.py
    Authorize a FortiGate and set properties on it using the FortiClient EMS API
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    fgt_serial = 'FGT70GSERIAL'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    fgt_authorization_url = f'{api_url_prefix}/client_certificates/set'
    fgt_properties_url = f'{api_url_prefix}/fabric_device_auth/{fgt_serial}/update'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    fgt_authorization_data = {"filters": {"management_mode": "standalone", "cns": [f"{fgt_serial}"]}, "properties": {"authorized": True}}
    
    #share_mode 0 is "Only share FortiClients connected to this fabric device (Recommended)"
    #share_mode 1 is "Share all FortiClients"
    #share_tag_types 1 is "Security Posture Tags", 2 is "Outbreak Tags", 3 is "Classification Tags", 4 is "Fabric Tags"
    fgt_properties_data = {"share_mode":1,"selected_cn_list":[],"share_tag_types":[1],"alias":"FGT-EMSAPI"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Authorize the FortiGate and set properties
    session.patch(url=fgt_authorization_url, json=fgt_authorization_data, headers=change_headers, verify=False, timeout=30)
    session.patch(url=fgt_properties_url, json=fgt_properties_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Authorize and edit FortiGate response

    {
       "result":{
          "retval":1,
          "message":null
       },
       "data":{
          "cns":[
             "FGT70GSERIAL"
          ]
       }
    },
    {
       "result":{
          "retval":1,
          "message":"Fabric device successfully updated."
       }
    }

    Creating a domain import

    Skipping creating an authentication server, eh? Yes. The reason is that creating an authentication server is form-based and I couldn’t find a way to do this using the API, especially when it comes to AD with LDAPS with a certificate upload. Sorry.

    But I know how to create a domain import, which isn’t that easy either, thanks to how EMS handles authentication servers. A short explanation of this.

    EMS collects all authentication servers as Identity Providers (IDPs) in the /api/v1/idps/index endpoint, which you can perform a GET on, but creating it is done via a form on the /api/v1/idps/adfs/create endpoint using a POST.

    Authentication server creation form

    Each IDP has a GUID, and you need this GUID to perform a domain import, because you first have to perform a walk on the directory structure to get the top-level objects (OUs and containers in most cases) and their information (GUID, name, DN and path). If your journey stops there and you just want to import from these top-level objects, you’re done. If you want to import individual groups, for example, in an OU, you have to go down a rabbit hole, because you have to get the GUID of that OU, perform a directory walk on that OU, get the information of the groups therein and get the information for the import (the same as for the OU).

    In an ordered list:

    1. Get IDP/authentication server GUID
    2. Perform a directory walk using the IDP GUID to find top-level objects
    3. Find information on relevant top-level objects 
    4. Optionally, perform a directory walk of top-level objects to find sublevel objects
    5. Optionally, get information on relevant sublevel objects

    The script handles this case, and there is an example directory structure given, and if you reverse engineer the API process, it will start to make more sense than what I have presented here.

    Create domain import

    '''
    ems_create_domain_import.py
    Create a domain import using the FortiClient EMS API
    '''
    
    import urllib.parse
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    idp_name = "ad.labdomain.com"
    
    #Adding groups and OUs to a policy is a bit complicated, so here is an example.
    #Consider the following AD structure:
    
    #ad.labdomain.com
    #└── CLIENTS
    #└── GROUPS
    #   ├── VPN_USERS
    #   ├── ZTNA_USERS
    #└── SERVERS
    #   └── PROD
    
    #If you want to import the entire CLIENTS OU add the name to the assigned_ous list
    #The script will get the required information of the OU and create a dictionary for the import
    
    #If you want to import the VPN_USERS group, and using the structure from above, you first have add the parent OU to the parent_ous list
    #In the example the parent OU is GROUP
    #Then add the VPN_USERS group to the assigned_group_names list
    
    assigned_ous = ['CLIENTS','SERVERS']
    parent_ous = ['GROUPS']
    assigned_group_names = ['VPN_USERS','ZTNA_USERS']
    assigned_ous_groups = []
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    idps_url = f'{api_url_prefix}/idps/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get authentication servers data
    response = session.get(url=idps_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get all the necessary information directly from the IDP and create the data for the domain import
    for idp in response_decoded['data']:
        if idp['domain_info']['name'] == idp_name:
            idp_id = idp['domain_info']['guid']
            #The DN has to be URL encoded
            idp_dn_urlencoded = urllib.parse.quote(idp['connection_info']['basedn'], safe="")
            domain_import_url = f'{api_url_prefix}/idps/adfs/{idp_id}/patch'
            #The live navigation is used to get the information regarding the directory structure straight from the authentication server
            live_navigation_url = f'{api_url_prefix}/idps/{idp_id}/live_navigate?dn={idp_dn_urlencoded}'
    
            #The response from the live navigation contains the directory structure with all OUs, but not groups
            response = session.get(url=live_navigation_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
    
            #The two ifs are used to search for the OU name in the assigned OUs, if we want to import an entire OU, and search through the parent OUs if we need to import groups in OUs
            for ou in response_decoded['data']:
                if ou['name'] in assigned_ous:
                    #A temporary dictionary is used to store the information for the to-be-imported object
                    temp_dict = {"guid": ou['guid'], "name": ou['name'], "dn": ou['dn'], "path": ou['canonical_name'], }
                    #The temporary dictionary gets added to a list
                    assigned_ous_groups.append(temp_dict)
                #Much the same is done for groups, except we first have to go through the OUs, like with the initial authentication server
                if ou['name'] in parent_ous:
                    ou_dn_urlencoded = urllib.parse.quote(ou['dn'], safe="")
                    groups_live_navigation_url = f'{api_url_prefix}/idps/{idp_id}/live_navigate?dn={ou_dn_urlencoded}'
                    response = session.get(url=groups_live_navigation_url, headers=api_headers, verify=False, timeout=30)
                    response_decoded = json.loads(response.content.decode('utf-8'))
                    for group in response_decoded['data']:
                        if group['name'] in assigned_group_names:
                            temp_dict = {"guid": group['guid'], "name": group['name'], "dn": group['dn'], "path": group['canonical_name'], }
                            assigned_ous_groups.append(temp_dict)
    
            #With all the dictionaries in the list we can assemble the JSON payload
            domain_data = {
                "sync_mins":60,
                "is_imported": True,
                "selected_group_containers": assigned_ous_groups
            }
    
            #Create domain import
            session.patch(url=domain_import_url, json=domain_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create domain import result

    {
       "result":{
          "retval":1,
          "message":"IDP updated successfully."
       },
       "data":{
          "guid":"c95b36ba-5adb-480d-b8da-87fc7598d2da",
          "domain_name":"ad.labdomain.com",
          "selected_group_containers":[
             {
                "guid":"d9b2392d-01e5-4e8b-98bf-26fb85a3399c",
                "path":"ad.labdomain.com/CLIENTS",
                "name":"CLIENTS",
                "dn":"OU=CLIENTS,DC=ad,DC=labdomain,DC=com"
             },
             {
                "guid":"a79fcd99-07e2-4c75-a615-58043fd244da",
                "path":"ad.labdomain.com/GROUPS/VPN_USERS",
                "name":"VPN_USERS",
                "dn":"CN=VPN_USERS,OU=GROUPS,DC=ad,DC=labdomain,DC=com"
             },
             {
                "guid":"83658fae-a526-4d9e-8030-8193a7fa0198",
                "path":"ad.labdomain.com/GROUPS/ZTNA_USERS",
                "name":"ZTNA_USERS",
                "dn":"CN=ZTNA_USERS,OU=GROUPS,DC=ad,DC=labdomain,DC=com"
             },
             {
                "guid":"015eb230-26f7-4a10-bde7-df92b0891848",
                "path":"ad.labdomain.com/SERVERS",
                "name":"SERVERS",
                "dn":"OU=SERVERS,DC=ad,DC=labdomain,DC=com"
             }
          ],
          "sync_mins":60
       }
    }

    Getting imported objects

    Not a very important piece, but if you want the information on all the objects your domain import has imported, you can use this script.

    GET imported objects

    '''
    ems_get_imported_ad_objects.py
    Get imported groups of AD server using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    idp_name = "ad.labdomain.com"
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    idps_url = f'{api_url_prefix}/idps/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get authentication servers data
    response = session.get(url=idps_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get imported OUs from IDP
    for idp in response_decoded['data']:
        if idp['domain_info']['name'] == idp_name:
            idp_id = idp['domain_info']['guid']
            idp_ous_url = f'{api_url_prefix}/idps/adfs/{idp_id}/imported_ous'
            response = session.get(url=idp_ous_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
            print(response_decoded['data']['group_containers'])
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    GET imported objects response

    [
       {
          "id":223,
          "name":"CLIENTS",
          "type":2,
          "dn":"OU=CLIENTS,DC=ad,DC=labdomain,DC=com",
          "is_user_selected":true,
          "guid":"d9b2392d-01e5-4e8b-98bf-26fb85a3399c",
          "full_path":"ad.labdomain.com/CLIENTS",
          "parent_ids":[
             227
          ],
          "has_child":0,
          "blocked":false,
          "policy_id":"None",
          "domain_id":3,
          "policy_name":"None",
          "telemetry_server_list_id":"None",
          "telemetry_server_list_name":"None",
          "total_devices":2,
          "domain_type":1
       },
       {
          "id":224,
          "name":"VPN_USERS",
          "type":4,
          "dn":"CN=VPN_USERS,OU=GROUPS,DC=ad,DC=labdomain,DC=com",
          "is_user_selected":true,
          "guid":"a79fcd99-07e2-4c75-a615-58043fd244da",
          "full_path":"ad.labdomain.com/GROUPS/VPN_USERS",
          "parent_ids":[
             227
          ],
          "has_child":0,
          "blocked":false,
          "policy_id":"None",
          "domain_id":3,
          "policy_name":"None",
          "telemetry_server_list_id":"None",
          "telemetry_server_list_name":"None",
          "total_devices":0,
          "domain_type":1
       },
       {
          "id":225,
          "name":"ZTNA_USERS",
          "type":4,
          "dn":"CN=ZTNA_USERS,OU=GROUPS,DC=ad,DC=labdomain,DC=com",
          "is_user_selected":true,
          "guid":"83658fae-a526-4d9e-8030-8193a7fa0198",
          "full_path":"ad.labdomain.com/GROUPS/ZTNA_USERS",
          "parent_ids":[
             227
          ],
          "has_child":0,
          "blocked":false,
          "policy_id":"None",
          "domain_id":3,
          "policy_name":"None",
          "telemetry_server_list_id":"None",
          "telemetry_server_list_name":"None",
          "total_devices":0,
          "domain_type":1
       },
       {
          "id":226,
          "name":"SERVERS",
          "type":2,
          "dn":"OU=SERVERS,DC=ad,DC=labdomain,DC=com",
          "is_user_selected":true,
          "guid":"015eb230-26f7-4a10-bde7-df92b0891848",
          "full_path":"ad.labdomain.com/SERVERS",
          "parent_ids":[
             227
          ],
          "has_child":1,
          "blocked":false,
          "policy_id":"None",
          "domain_id":3,
          "policy_name":"None",
          "telemetry_server_list_id":"None",
          "telemetry_server_list_name":"None",
          "total_devices":1,
          "domain_type":1
       }
    ]

    Creating a system and remote access profile

    I am only focusing on these two types of profiles because they are probably the most used ones, and going through all of them would take too long. If you want to know how to interact with the other types, you can reverse engineer them.

    Both profile data variables contain most, if not all, options you can set. You can omit options if you want the defaults (most options are already the defaults).

    Create system and remote access profile

    '''
    ems_create_profiles.py
    Create a system and a remote access VPN profile using the FortiClient EMS API
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    temp_password = 'Start123$'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    system_profile_url = f'{api_url_prefix}/profiles/system/create'
    vpn_profile_url = f'{api_url_prefix}/profiles/vpn/create'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    system_profile_data = {
        "name": "SYS_EMS-API",
        "is_chromebook": False,
        "enabled": True,
        "display_enabled": True,
        "clone_from": None,
        "json": {
            "system": {
                "ui": {
                    "disable_backup": 0,
                    "hide_user_info": 0,
                    "hide_system_tray_icon": 0,
                    "show_host_tag": 1,
                    "password": f"{temp_password}",
                    "lock": f"{temp_password}",
                    "unreg_pwd": f"{temp_password}",
                    "culture_code": "os-default",
                    "default_tab": "VPN",
                    "allow_shutdown_when_registered": 0
                },
                "log_settings": {
                    "onnet_local_logging": 1,
                    "level": 6,
                    "log_events": "antiexploit,antiransomware,av,cloudscan,endpoint,firewall,fssoma,ipsecvpn,pam,sandboxing,sslvpn,update,vuln,webfilter,ztna,configd,scheduler,shield,wanacc",
                    "remote_logging": {
                        "log_upload_enabled": 0,
                        "log_retention_days": 90,
                        "log_upload_freq_minutes": 60,
                        "send_software_inventory": 0,
                        "send_os_events": {
                            "enabled": 1,
                            "interval": 120
                        },
                        "log_upload_server": "",
                        "log_upload_ssl_enabled": 1,
                        "log_generation_timeout_secs": 900,
                        "log_compressed": 0,
                        "netlog_categories": 32
                    }
                },
                "proc_protect": 1,
                "proxy": {
                    "update": 0,
                    "fail_over_to_fdn": 0,
                    "online_scep": 0,
                    "virus_submission": 0,
                    "type": "http",
                    "address": None,
                    "port": "80",
                    "username": None,
                    "password": ""
                },
                "update": {
                    "use_custom_server": 0,
                    "timeout": 60,
                    "failoverport": 8000,
                    "auto_patch": 0,
                    "update_action": "disable",
                    "scheduled_update": {
                        "enabled": 1,
                        "type": "interval",
                        "daily_at": "00:00",
                        "update_interval_in_hours": 1
                    },
                    "submit_virus_info_to_fds": 1,
                    "submit_vuln_info_to_fds": 1,
                    "use_legacy_fdn": 0,
                    "server": "",
                    "port": 80,
                    "fail_over_to_fdn": 0,
                    "restrict_services_to_regions": "",
                    "ocsp_mode": 0
                },
                "fortiproxy": {
                    "enabled": 1,
                    "enable_https_proxy": 1,
                    "http_timeout": 60,
                    "client_comforting": {
                        "pop3_client": 1,
                        "pop3_server": 1,
                        "smtp": 1
                    },
                    "selftest": {
                        "enabled": 1,
                        "last_port": 65535,
                        "notify": 1
                    }
                },
                "certificates": [31],
                "user_identity": {
                    "enable_manually_entering": 0,
                    "enable_linkedin": 0,
                    "enable_google": 0,
                    "enable_salesforce": 0,
                    "notify_user": 0
                },
                "installer": {
                    "allow_admin_uninstall_when_locked": 1
                },
                "cryptography": {
                    "drbg_reseed_minutes": 1440
                }
            },
            "extra": {
                "trigger_vuln_scan": True
            },
            "endpoint_control": {
                "ui": {
                    "hide_compliance_warning": 0
                },
                "notify_fgt_on_logoff": 0,
                "forensics_license": 1,
                "enable_dem": 0,
                "disable_unregister": 1,
                "disable_fgt_switch": 0,
                "show_bubble_notifications": 1,
                "send_software_inventory": 0,
                "invalid_cert_action": "warn",
                "edr_collector": 1,
                "enable_dns_cache": 0,
                "auto_start": 0
            },
            "fssoma": {
                "enabled": 0,
                "serveraddress": "",
                "presharedkey": ""
            },
            "wan_optimization": {
                "enabled": 0,
                "max_disk_cache_size_mb": 512,
                "support_http": 1,
                "support_cifs": 1,
                "support_mapi": 1,
                "support_ftp": 1
            },
            "pam": {
                "enabled": 0,
                "default_port": 9191
            }
        }
    }
    
    vpn_profile_data = {
        "name": "VPN_EMS-API",
        "is_chromebook": False,
        "enabled": True,
        "display_enabled": True,
        "clone_from": None,
        "json": {
            "vpn": {
                "display_vpn": 1,
                "enabled": 1,
                "sslvpn": {
                    "options": {
                        "enabled": 0,
                        "prefer_sslvpn_dns": 1,
                        "disallow_invalid_server_certificate": 0,
                        "warn_invalid_server_certificate": 1,
                        "preferred_dtls_tunnel": 0,
                        "show_auth_cert_only": 0,
                        "use_gui_saml_auth": 0,
                        "block_ipv6": 1,
                        "dnscache_service_control": 0,
                        "no_dns_registration": 0,
                        "negative_split_tunnel_metric": None,
                        "mtu_size": 1300,
                        "dtls_mtu": 1100
                    },
                    "connections": []
                },
                "ipsecvpn": {
                    "options": {
                        "enabled": 1,
                        "use_win_current_user_cert": 1,
                        "use_win_local_computer_cert": 1,
                        "beep_if_error": 0,
                        "usewincert": 1,
                        "usesmcardcert": 1,
                        "use_gui_saml_auth": 0,
                        "block_ipv6": 1,
                        "enable_udp_checksum": 0,
                        "disable_default_route": 0,
                        "show_auth_cert_only": 0,
                        "check_for_cert_private_key": 0,
                        "enhanced_key_usage_mandatory": 0,
                        "disallow_invalid_server_certificate": 0,
                        "prefer_ipsecvpn_dns": 1,
                        "no_dns_registration": 0,
                        "mtu_size": 1280
                    },
                    "connections": [
                        {
                            "name": "API-IPSEC-VPN",
                            "pinned": 0,
                            "dns_priority": 1,
                            "machine": None,
                            "keep_running": 0,
                            "traffic_keep_strategy": 0,
                            "traffic_keep_timer": 5000,
                            "disclaimer_msg": "",
                            "single_user_mode": 0,
                            "ui": {
                                "show_remember_password": 0,
                                "show_alwaysup": 0,
                                "show_autoconnect": 0,
                                "show_passcode": 0,
                                "save_username": 0
                            },
                            "traffic_control": {
                                "enabled": 0,
                                "mode": 1,
                                "apps": [],
                                "fqdns": [],
                                "isdb_objects": [],
                                "vsdb_objects": []
                            },
                            "redundant_sort_method": 0,
                            "tags": {
                                "allowed": "",
                                "prohibited": ""
                            },
                            "host_check_fail_warning": "",
                            "ike_settings": {
                                "server": "192.0.2.1",
                                "authentication_method": "Preshared Key",
                                "auth_data": f"{temp_password}",
                                "transport_mode": 0,
                                "tcp_port": 443,
                                "udp_port": 500,
                                "cert_subjectcheck": 0,
                                "prompt_certificate": 1,
                                "xauth_timeout": 120,
                                "xauth": {
                                    "use_otp": 0,
                                    "enabled": 0,
                                    "prompt_username": 0
                                },
                                "version": 2,
                                "mode": "aggressive",
                                "dhgroup": [31],
                                "key_life": 28800,
                                "localid": None,
                                "networkid": 0,
                                "eap_method": 1,
                                "implied_SPDO": 0,
                                "implied_SPDO_timeout": 60,
                                "nat_traversal": 1,
                                "enable_local_lan": 1,
                                "session_resume": 0,
                                "enable_ike_fragmentation": 1,
                                "mode_config": 1,
                                "modeconfig_type": 0,
                                "dpd": 1,
                                "proposals": [
                                    {
                                        "encryption": "AES128",
                                        "authentication": "SHA256"
                                    },
                                    {
                                        "encryption": "AES256",
                                        "authentication": "SHA256"
                                    }
                                ],
                                "run_fcauth_system": 0,
                                "failover_sslvpn_connection": None,
                                "sso_enabled": 0,
                                "use_external_browser": 0,
                                "ike_saml_port": 443,
                                "keep_fqdn_resolution_consistency": 0,
                                "no_vnic_dns_server": 0,
                                "azure_auto_login": {
                                    "enabled": 0,
                                    "azure_app": {
                                        "tenant_name": "",
                                        "client_id": ""
                                    }
                                }
                            },
                            "ipsec_settings": {
                                "remote_networks": [
                                    {
                                        "addr": "0.0.0.0",
                                        "mask": "0.0.0.0"
                                    },
                                    {
                                        "addr": "::/0",
                                        "mask": "::/0"
                                    }
                                ],
                                "dhgroup": 31,
                                "key_life_type": "seconds",
                                "key_life_seconds": 3600,
                                "key_life_Kbytes": 5200,
                                "replay_detection": 1,
                                "pfs": 1,
                                "virtualip": {
                                    "type": "modeconfig",
                                    "ip": "0.0.0.0",
                                    "mask": "0.0.0.0",
                                    "dnsserver": "0.0.0.0",
                                    "winserver": "0.0.0.0"
                                },
                                "proposals": [
                                    {
                                        "encryption": "AES128GCM",
                                        "authentication": "NONE"
                                    },
                                    {
                                        "encryption": "AES256",
                                        "authentication": "SHA256"
                                    }
                                ],
                                "ipv4_split_exclude_networks": []
                            },
                            "on_connect": [
                                {
                                    "os": "windows",
                                    "script": ""
                                },
                                {
                                    "os": "MacOSX",
                                    "script": ""
                                }
                            ],
                            "on_disconnect": [
                                {
                                    "os": "windows",
                                    "script": ""
                                },
                                {
                                    "os": "MacOSX",
                                    "script": ""
                                }
                            ],
                            "android_cert_path": ""
                        }
                    ]
                },
                "lockdown": {
                    "enabled": 0,
                    "grace_period": 120,
                    "max_attempts": 3,
                    "exceptions": {
                        "apps": None,
                        "ips": None,
                        "domains": None,
                        "icdb_domains": []
                    },
                    "detect_captive_portal": {
                        "enabled": 0,
                        "os_active_probing": 1
                    }
                },
                "options": {
                    "current_connection_name": "",
                    "current_connection_type": None,
                    "autoconnect_tunnel": "",
                    "vendor_id": None,
                    "on_os_start_connect": "",
                    "on_os_start_connect_has_priority": 0,
                    "show_vpn_before_logon": 1,
                    "minimize_window_on_connect": 1,
                    "use_windows_credentials": 0,
                    "suppress_vpn_notification": 0,
                    "secure_remote_access": 0,
                    "certs_require_keyspec": 0,
                    "disable_internet_check": 1,
                    "use_webview2_saml_auth": 0,
                    "enable_multi_vpn": 0,
                    "enforce_disabling_smartdns": 0,
                    "enable_view_selected_vpns": 0,
                    "keep_running_max_tries": 0,
                    "after_logon_saml_auth": 0,
                    "before_logon_saml_auth": 1,
                    "allow_personal_vpns": 1,
                    "disable_connect_disconnect": 0,
                    "autoconnect_on_install": 0,
                    "autoconnect_only_when_offnet": 0,
                    "temp_password": f"{temp_password}"
                }
            }
        }
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Create system profile
    session.post(url=system_profile_url, json=system_profile_data, headers=change_headers, verify=False, timeout=30)
    
    #Create VPN profile
    session.post(url=vpn_profile_url, json=vpn_profile_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create system and remote access profile response

    {
       "result":{
          "retval":1,
          "message":"Profile component created succesfully."
       },
       "data":{
          "id":68,
          "json":{
             "fssoma":{
                "enabled":0,
                "presharedkey":"",
                "serveraddress":""
             },
             "version":"5.6.0",
             "pam":{
                "enabled":0,
                "default_port":9191
             },
             "endpoint_control":{
                "forensics_license":1,
                "enable_dns_cache":0,
                "enable_dem":0,
                "send_software_inventory":0,
                "disable_fgt_switch":0,
                "auto_start":0,
                "disable_unregister":1,
                "notify_fgt_on_logoff":0,
                "invalid_cert_action":"warn",
                "ui":{
                   "hide_compliance_warning":0
                },
                "show_bubble_notifications":1,
                "edr_collector":1
             },
             "system":{
                "certificates":[
                   
                ],
                "cryptography":{
                   "drbg_reseed_minutes":1440
                },
                "update":{
                   "timeout":60,
                   "auto_patch":0,
                   "fail_over_to_fdn":0,
                   "restrict_services_to_regions":"",
                   "scheduled_update":{
                      "type":"interval",
                      "enabled":1,
                      "daily_at":"00:00",
                      "update_interval_in_hours":1
                   },
                   "submit_virus_info_to_fds":1,
                   "port":80,
                   "update_action":"disable",
                   "failoverport":8000,
                   "use_legacy_fdn":0,
                   "ocsp_mode":0,
                   "submit_vuln_info_to_fds":1,
                   "server":"",
                   "use_custom_server":0
                },
                "proxy":{
                   "username":null,
                   "update":0,
                   "fail_over_to_fdn":0,
                   "port":80,
                   "virus_submission":0,
                   "type":"http",
                   "password":"",
                   "address":null,
                   "online_scep":0
                },
                "installer":{
                   "allow_admin_uninstall_when_locked":1
                },
                "proc_protect":1,
                "user_identity":{
                   "enable_google":0,
                   "enable_linkedin":0,
                   "enable_manually_entering":0,
                   "notify_user":0,
                   "enable_salesforce":0
                },
                "log_settings":{
                   "onnet_local_logging":1,
                   "level":6,
                   "log_events":"antiexploit,antiransomware,av,cloudscan,endpoint,firewall,fssoma,ipsecvpn,pam,sandboxing,sslvpn,update,vuln,webfilter,ztna,configd,scheduler,shield,wanacc",
                   "remote_logging":{
                      "send_os_events":{
                         "enabled":1,
                         "interval":120
                      },
                      "log_upload_enabled":0,
                      "log_retention_days":90,
                      "log_upload_ssl_enabled":1,
                      "send_software_inventory":0,
                      "log_upload_server":"",
                      "log_generation_timeout_secs":900,
                      "netlog_categories":32,
                      "log_upload_freq_minutes":60,
                      "log_compressed":0
                   }
                },
                "fortiproxy":{
                   "http_timeout":60,
                   "selftest":{
                      "notify":1,
                      "enabled":1,
                      "last_port":65535
                   },
                   "client_comforting":{
                      "pop3_client":1,
                      "pop3_server":1,
                      "smtp":1
                   },
                   "enable_https_proxy":1,
                   "enabled":1
                },
                "ui":{
                   "allow_shutdown_when_registered":0,
                   "disable_backup":0,
                   "unreg_pwd":"QyCr5xV6oIlvsBXpKY0WOZBgz7m3EtM4lYbtuuNudjSPW4meHcM6EHXKBb1XzeMcW4yVBnnVmBCcW2EDcwP7GevjLB27aE1Ht6PHPIEa0PzI1HdVrB8M6wewPN4B91Jb$OXwDzKLMons/0ycr3vyNBQQ53CCqovZM3N7geRo1J9wyGYGDehQ/pwjRehPmKcmhUfcchXh/83ucpm+uX7BONA==",
                   "lock":"Enc 94150c56da89b13d9645ed3cd5928f88764097e7f638f57aebdf9e07050cddc07d0a71024d30b4a834e32df39b63c2ca1b7516e79895cfda4556f2dd7a8151507d59b62b3e9dabdfdc7fb1dae92467fcc94b05106769a7ba",
                   "password":"Enc fee42e05beef862d7dd22b0e497534ebcc6840941f7ffdc6567519915361f10e87043d14c05c140e4ab9e49ee7a6328f53ee8ea9aed16eb8",
                   "culture_code":"os-default",
                   "hide_user_info":0,
                   "default_tab":"VPN",
                   "hide_system_tray_icon":0,
                   "show_host_tag":1
                }
             },
             "wan_optimization":{
                "support_cifs":1,
                "support_http":1,
                "support_ftp":1,
                "enabled":0,
                "support_mapi":1,
                "max_disk_cache_size_mb":512
             }
          }
       }
    },
    {
       "result":{
          "retval":1,
          "message":"Profile component created succesfully."
       },
       "data":{
          "id":69,
          "json":{
             "vpn":{
                "sslvpn":{
                   "connections":[
                      
                   ],
                   "options":{
                      "dtls_mtu":1100,
                      "no_dns_registration":0,
                      "show_auth_cert_only":0,
                      "disallow_invalid_server_certificate":0,
                      "use_gui_saml_auth":0,
                      "warn_invalid_server_certificate":1,
                      "block_ipv6":1,
                      "negative_split_tunnel_metric":null,
                      "preferred_dtls_tunnel":0,
                      "mtu_size":1300,
                      "dnscache_service_control":0,
                      "enabled":0,
                      "prefer_sslvpn_dns":1
                   }
                },
                "ipsecvpn":{
                   "connections":[
                      {
                         "name":"API-IPSEC-VPN",
                         "pinned":0,
                         "dns_priority":1,
                         "machine":0,
                         "keep_running":0,
                         "traffic_keep_strategy":0,
                         "traffic_keep_timer":5000,
                         "disclaimer_msg":"",
                         "single_user_mode":0,
                         "ui":{
                            "show_remember_password":0,
                            "show_alwaysup":0,
                            "show_autoconnect":0,
                            "show_passcode":0,
                            "save_username":0
                         },
                         "traffic_control":{
                            "enabled":0,
                            "mode":1,
                            "apps":[
                               
                            ],
                            "fqdns":[
                               
                            ],
                            "isdb_objects":[
                               
                            ],
                            "vsdb_objects":[
                               
                            ]
                         },
                         "redundant_sort_method":0,
                         "tags":{
                            "allowed":"",
                            "prohibited":""
                         },
                         "host_check_fail_warning":"",
                         "ike_settings":{
                            "server":"192.0.2.1",
                            "authentication_method":"Preshared Key",
                            "auth_data":"Enc 564234f7087b4c8c602610fb456a4af44209e584441c59506e86588d8d018f97c1",
                            "transport_mode":0,
                            "tcp_port":443,
                            "udp_port":500,
                            "cert_subjectcheck":0,
                            "prompt_certificate":0,
                            "xauth_timeout":120,
                            "xauth":{
                               "use_otp":0,
                               "enabled":0,
                               "prompt_username":0,
                               "username":"",
                               "password":""
                            },
                            "version":2,
                            "mode":"aggressive",
                            "dhgroup":[
                               31
                            ],
                            "key_life":28800,
                            "localid":"",
                            "networkid":0,
                            "eap_method":1,
                            "implied_SPDO":0,
                            "implied_SPDO_timeout":60,
                            "nat_traversal":1,
                            "enable_local_lan":1,
                            "session_resume":0,
                            "enable_ike_fragmentation":1,
                            "mode_config":1,
                            "modeconfig_type":0,
                            "dpd":1,
                            "proposals":[
                               {
                                  "encryption":"AES128",
                                  "authentication":"SHA256"
                               },
                               {
                                  "encryption":"AES256",
                                  "authentication":"SHA256"
                               }
                            ],
                            "run_fcauth_system":0,
                            "failover_sslvpn_connection":"",
                            "sso_enabled":0,
                            "use_external_browser":0,
                            "ike_saml_port":443,
                            "keep_fqdn_resolution_consistency":0,
                            "no_vnic_dns_server":0,
                            "azure_auto_login":{
                               "enabled":0,
                               "azure_app":{
                                  "tenant_name":"",
                                  "client_id":""
                               }
                            },
                            "fgt":1,
                            "dpd_retry_count":3,
                            "dpd_retry_interval":20,
                            "certificate":null,
                            "nat_alive_freq":10
                         },
                         "ipsec_settings":{
                            "remote_networks":[
                               {
                                  "addr":"0.0.0.0",
                                  "mask":"0.0.0.0"
                               },
                               {
                                  "addr":"::/0",
                                  "mask":"::/0"
                               }
                            ],
                            "dhgroup":31,
                            "key_life_type":"seconds",
                            "key_life_seconds":3600,
                            "key_life_Kbytes":5200,
                            "replay_detection":1,
                            "pfs":1,
                            "virtualip":{
                               "type":"modeconfig",
                               "ip":"0.0.0.0",
                               "mask":"0.0.0.0",
                               "dnsserver":"0.0.0.0",
                               "winserver":"0.0.0.0"
                            },
                            "proposals":[
                               {
                                  "encryption":"AES128GCM",
                                  "authentication":"NONE"
                               },
                               {
                                  "encryption":"AES256",
                                  "authentication":"SHA256"
                               }
                            ],
                            "ipv4_split_exclude_networks":[
                               
                            ],
                            "use_vip":1
                         },
                         "on_connect":[
                            {
                               "os":"windows",
                               "script":""
                            },
                            {
                               "os":"MacOSX",
                               "script":""
                            }
                         ],
                         "on_disconnect":[
                            {
                               "os":"windows",
                               "script":""
                            },
                            {
                               "os":"MacOSX",
                               "script":""
                            }
                         ],
                         "android_cert_path":"",
                         "uid":"00C4F2CA-5814-4612-8F26-3838D8746782",
                         "warn_invalid_server_certificate":1,
                         "type":"manual"
                      }
                   ],
                   "options":{
                      "disable_default_route":0,
                      "block_ipv6":1,
                      "use_win_local_computer_cert":1,
                      "check_for_cert_private_key":0,
                      "mtu_size":1280,
                      "usesmcardcert":1,
                      "beep_if_error":0,
                      "enhanced_key_usage_mandatory":0,
                      "no_dns_registration":0,
                      "show_auth_cert_only":0,
                      "disallow_invalid_server_certificate":0,
                      "prefer_ipsecvpn_dns":1,
                      "use_gui_saml_auth":0,
                      "use_win_current_user_cert":1,
                      "enable_udp_checksum":0,
                      "usewincert":1,
                      "enabled":1
                   }
                },
                "enabled":1,
                "lockdown":{
                   "grace_period":120,
                   "max_attempts":3,
                   "detect_captive_portal":{
                      "enabled":0,
                      "os_active_probing":1
                   },
                   "exceptions":{
                      "domains":[
                         
                      ],
                      "ips":[
                         
                      ],
                      "apps":[
                         
                      ],
                      "icdb_domains":[
                         
                      ]
                   },
                   "enabled":0
                },
                "options":{
                   "after_logon_saml_auth":0,
                   "temp_password":"Start123$",
                   "enable_view_selected_vpns":0,
                   "minimize_window_on_connect":1,
                   "enable_multi_vpn":0,
                   "autoconnect_tunnel":"",
                   "show_vpn_before_logon":1,
                   "on_os_start_connect":"",
                   "secure_remote_access":0,
                   "current_connection_type":"",
                   "certs_require_keyspec":0,
                   "disable_internet_check":1,
                   "autoconnect_on_install":0,
                   "allow_personal_vpns":1,
                   "disconnect_password":"",
                   "vendor_id":"",
                   "use_windows_credentials":0,
                   "autoconnect_only_when_offnet":0,
                   "current_connection_name":"",
                   "before_logon_saml_auth":1,
                   "disable_connect_disconnect":0,
                   "on_os_start_connect_has_priority":0,
                   "suppress_vpn_notification":0,
                   "use_webview2_saml_auth":0,
                   "keep_running_max_tries":0,
                   "enforce_disabling_smartdns":0
                },
                "display_vpn":1
             }
          }
       }
    }

    Updating a system and remote access profile

    When it comes to updating things in EMS, you have to be careful because you often cannot just do a PATCH. Most endpoints for updating use a PUT, and when you do that, you are only setting the options you supply, and for everything else, the defaults are used.

    In the example script, I disable security postage tags on the FortiClient GUI and disable bubble notifications in the system settings profile. If I were to send just this to the /api/v1/profiles/system/{ID}/update endpoint, it would set these two things, but it would set everything else to the default values. I handle this case by first performing a GET on the existing profile, saving the information in a variable and performing a deep merge with the updated values. The resulting variable, including the old and updated information, then gets used for the PUT.

    This update procedure is also where the deep_merge function is used, which Claude has written.

    Update system and remote access profile using deep merge

    '''
    ems_update_profiles_deep_merge.py
    Update a system and a remote access VPN profile by first importing the existing configuration using the FortiClient EMS API
    
    This script updates the system settings profile created in ems_create_profiles.py by disabling security posting tags on the GUI and disabling bubble notifications
    This script updates the VPN profile created in ems_create_profiles.py by
    * Enabling the save username option
    * Changing the remote gateway
    * Changing the phase 1 DH group
    * Enabling session resume
    * Changing the phase 2 proposals
    * Disabling personal VPNs
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    def deep_merge(base, override):
        """
        Function written by Claude Sonnet 4.6
    
        Recursively merge `override` into `base`.
        - Dicts: merged recursively.
        - Lists of dicts with a 'name' key: matched by name, then merged recursively.
        - Everything else: override replaces base.
        """
        if isinstance(base, dict) and isinstance(override, dict):
            result = base.copy()
            for key, override_val in override.items():
                base_val = result.get(key)
                result[key] = deep_merge(base_val, override_val)
            return result
    
        if (
            isinstance(base, list)
            and isinstance(override, list)
            and all(isinstance(i, dict) and "name" in i for i in base + override)
        ):
            # Match connection objects by 'name', merge matched pairs
            base_by_name = {item["name"]: item for item in base}
            result = []
            for override_item in override:
                name = override_item["name"]
                if name in base_by_name:
                    result.append(deep_merge(base_by_name[name], override_item))
                else:
                    result.append(override_item)  # new entry, add as-is
            # Preserve base entries not present in override
            override_names = {item["name"] for item in override}
            for base_item in base:
                if base_item["name"] not in override_names:
                    result.append(base_item)
            return result
    
        # Scalar, plain list, or mismatched types: override wins
        return override
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    temp_password = 'Start123$'
    system_profile_name = "SYS_EMS-API"
    vpn_profile_name = "VPN_EMS-API"
    vpn_ipsec_connection_name = "API-IPSEC-VPN"
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    system_profiles_get_url = f'{api_url_prefix}/profiles/system/index'
    vpn_profiles_get_url = f'{api_url_prefix}/profiles/vpn/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    updated_system_profile_data = {
        "json": {
            "system": {
                "ui": {
                    "show_host_tag": 0,
                }
            },
            "endpoint_control": {
                "show_bubble_notifications": 0,
            }
        }
    }
    
    updated_vpn_profile_data = {
        "name": f"{vpn_profile_name}",
        "json": {
            "vpn": {
                "ipsecvpn": {
                    "connections": [
                        {
                            "name": f"{vpn_ipsec_connection_name}",
                            "ui": {
                                "save_username": 1
                            },
                            "ike_settings": {
                                "server": "192.0.2.254",
                                "dhgroup": [21],
                                "session_resume": 1,
                            },
                            "ipsec_settings": {
                                "proposals": [
                                    {
                                        "encryption": "AES256GCM",
                                        "authentication": "NONE"
                                    },
                                    {
                                        "encryption": "AES256",
                                        "authentication": "SHA512"
                                    }
                                ]
                            },
                        }
                    ]
                },
                "options": {
                    "allow_personal_vpns": 0,
                }
            }
        }
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get system profiles
    response = session.get(url=system_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == system_profile_name:
            system_profile_get_url = f'{api_url_prefix}/profiles/system/{profile["id"]}/get'
            response = session.get(url=system_profile_get_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
            system_profile_data = response_decoded['data']
    
            #Update system_profile_data with the updated information
            system_profile_data = deep_merge(system_profile_data, updated_system_profile_data)
    
            #Set the correct URL for updating the system profile using the profile ID
            system_profile_update_url = f'{api_url_prefix}/profiles/system/{profile["id"]}/update'
            #Update system profile
            session.put(url=system_profile_update_url, json=system_profile_data, headers=change_headers, verify=False, timeout=30)
    
    #Get VPN profiles
    response = session.get(url=vpn_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == vpn_profile_name:
            vpn_profile_get_url = f'{api_url_prefix}/profiles/vpn/{profile["id"]}/get'
            response = session.get(url=vpn_profile_get_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
            vpn_profile_data = response_decoded['data']
    
            #Update vpn_profile_data with the updated information
            #update_dictionary(vpn_profile_data['json']['vpn'], updated_vpn_profile_data['json']['vpn'])
            vpn_profile_data = deep_merge(vpn_profile_data, updated_vpn_profile_data)
    
            #Set the correct URL for updating the VPN profile using the profile ID
            vpn_profile_update_url = f'{api_url_prefix}/profiles/vpn/{profile["id"]}/update'
            #Update VPN profile
            session.put(url=vpn_profile_update_url, json=vpn_profile_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Update system and remote access profile using deep merge response

    {
       "result":{
          "retval":1,
          "message":"Profile component updated succesfully."
       },
       "data":{
          "id":70,
          "json":{
             "fssoma":{
                "enabled":0,
                "presharedkey":"",
                "serveraddress":""
             },
             "version":"5.6.0",
             "pam":{
                "enabled":0,
                "default_port":9191
             },
             "endpoint_control":{
                "forensics_license":1,
                "enable_dns_cache":0,
                "enable_dem":0,
                "send_software_inventory":0,
                "disable_fgt_switch":0,
                "auto_start":0,
                "disable_unregister":1,
                "notify_fgt_on_logoff":0,
                "invalid_cert_action":"warn",
                "ui":{
                   "hide_compliance_warning":0
                },
                "show_bubble_notifications":0,
                "edr_collector":1
             },
             "system":{
                "certificates":[
                   
                ],
                "cryptography":{
                   "drbg_reseed_minutes":1440
                },
                "update":{
                   "timeout":60,
                   "auto_patch":0,
                   "fail_over_to_fdn":0,
                   "restrict_services_to_regions":"",
                   "scheduled_update":{
                      "type":"interval",
                      "enabled":1,
                      "daily_at":"00:00",
                      "update_interval_in_hours":1
                   },
                   "submit_virus_info_to_fds":1,
                   "port":80,
                   "update_action":"disable",
                   "failoverport":8000,
                   "use_legacy_fdn":0,
                   "ocsp_mode":0,
                   "submit_vuln_info_to_fds":1,
                   "server":"",
                   "use_custom_server":0
                },
                "proxy":{
                   "username":null,
                   "update":0,
                   "fail_over_to_fdn":0,
                   "port":80,
                   "virus_submission":0,
                   "type":"http",
                   "password":"",
                   "address":null,
                   "online_scep":0
                },
                "installer":{
                   "allow_admin_uninstall_when_locked":1
                },
                "proc_protect":1,
                "user_identity":{
                   "enable_google":0,
                   "enable_linkedin":0,
                   "enable_manually_entering":0,
                   "notify_user":0,
                   "enable_salesforce":0
                },
                "log_settings":{
                   "onnet_local_logging":1,
                   "level":6,
                   "log_events":"antiexploit,antiransomware,av,cloudscan,endpoint,firewall,fssoma,ipsecvpn,pam,sandboxing,sslvpn,update,vuln,webfilter,ztna,configd,scheduler,shield,wanacc",
                   "remote_logging":{
                      "send_os_events":{
                         "enabled":1,
                         "interval":120
                      },
                      "log_upload_enabled":0,
                      "log_retention_days":90,
                      "log_upload_ssl_enabled":1,
                      "send_software_inventory":0,
                      "log_upload_server":"",
                      "log_generation_timeout_secs":900,
                      "netlog_categories":32,
                      "log_upload_freq_minutes":60,
                      "log_compressed":0
                   }
                },
                "fortiproxy":{
                   "http_timeout":60,
                   "selftest":{
                      "notify":1,
                      "enabled":1,
                      "last_port":65535
                   },
                   "client_comforting":{
                      "pop3_client":1,
                      "pop3_server":1,
                      "smtp":1
                   },
                   "enable_https_proxy":1,
                   "enabled":1
                },
                "ui":{
                   "allow_shutdown_when_registered":0,
                   "disable_backup":0,
                   "unreg_pwd":"goBsfMBfsTRbI3732fJADZ2gJEqSFB7zeBKnNb2U3WRDVgKla5vP6XG4xPpK85mDIpq2xSvSfyagMMdQ7Hl73WiOMntt5LdOOgUvUi0MT8Oq74vif2XsiFd3tf57SFV7$TQJ1qTiQelcT/qoHkN4laeQI6jvKLeoe+1WevZOq9oBktCLyBB4goInKHqF9jiPGDNhrT7rToVWNEXKeqF8myQ==",
                   "lock":"Enc 283431078f7ac0acbb5fd257d3279947f5cd01bbe523458b9d50b1a1d5a8bf373b784abec45318e4646034cf13d32e00df29a194c3a247b9db53f6e768362e37239b7005d02266424b0efd63aac9c86669a86c0a0368caa7",
                   "password":"Enc 68be3193bbd992ab167dd677ba394598d6c827dcedc1d91d842296f9fcfb9cc97b6d62fc1e38124e0b1ea9dc2233665e1a76f192808a1f09",
                   "culture_code":"os-default",
                   "hide_user_info":0,
                   "default_tab":"VPN",
                   "hide_system_tray_icon":0,
                   "show_host_tag":0
                }
             },
             "wan_optimization":{
                "support_cifs":1,
                "support_http":1,
                "support_ftp":1,
                "enabled":0,
                "support_mapi":1,
                "max_disk_cache_size_mb":512
             }
          }
       }
    },
    {
       "result":{
          "retval":1,
          "message":"Profile component updated succesfully."
       },
       "data":{
          "id":70,
          "json":{
             "vpn":{
                "sslvpn":{
                   "connections":[
                      
                   ],
                   "options":{
                      "dtls_mtu":1100,
                      "no_dns_registration":0,
                      "show_auth_cert_only":0,
                      "disallow_invalid_server_certificate":0,
                      "use_gui_saml_auth":0,
                      "warn_invalid_server_certificate":1,
                      "block_ipv6":1,
                      "negative_split_tunnel_metric":null,
                      "preferred_dtls_tunnel":0,
                      "mtu_size":1300,
                      "dnscache_service_control":0,
                      "enabled":0,
                      "prefer_sslvpn_dns":1
                   }
                },
                "ipsecvpn":{
                   "connections":[
                      {
                         "name":"API-IPSEC-VPN",
                         "pinned":0,
                         "dns_priority":1,
                         "machine":0,
                         "keep_running":0,
                         "traffic_keep_strategy":0,
                         "traffic_keep_timer":5000,
                         "disclaimer_msg":"",
                         "single_user_mode":0,
                         "ui":{
                            "show_remember_password":0,
                            "show_alwaysup":0,
                            "show_autoconnect":0,
                            "show_passcode":0,
                            "save_username":1
                         },
                         "traffic_control":{
                            "enabled":0,
                            "mode":1,
                            "apps":[
                               
                            ],
                            "fqdns":[
                               
                            ],
                            "isdb_objects":[
                               
                            ],
                            "vsdb_objects":[
                               
                            ]
                         },
                         "redundant_sort_method":0,
                         "tags":{
                            "allowed":"",
                            "prohibited":""
                         },
                         "host_check_fail_warning":"",
                         "ike_settings":{
                            "server":"192.0.2.254",
                            "authentication_method":"Preshared Key",
                            "auth_data":"Enc f7446dfccd809fbf6486f27f81c515eafa55049574aae9672f18533318cdb297e0",
                            "transport_mode":0,
                            "tcp_port":443,
                            "udp_port":500,
                            "cert_subjectcheck":0,
                            "prompt_certificate":0,
                            "xauth_timeout":120,
                            "xauth":{
                               "use_otp":0,
                               "enabled":0,
                               "prompt_username":0,
                               "username":"",
                               "password":""
                            },
                            "version":2,
                            "mode":"aggressive",
                            "dhgroup":[
                               21
                            ],
                            "key_life":28800,
                            "localid":"",
                            "networkid":0,
                            "eap_method":1,
                            "implied_SPDO":0,
                            "implied_SPDO_timeout":60,
                            "nat_traversal":1,
                            "enable_local_lan":1,
                            "session_resume":1,
                            "enable_ike_fragmentation":1,
                            "mode_config":1,
                            "modeconfig_type":0,
                            "dpd":1,
                            "proposals":[
                               {
                                  "encryption":"AES128",
                                  "authentication":"SHA256"
                               },
                               {
                                  "encryption":"AES256",
                                  "authentication":"SHA256"
                               }
                            ],
                            "run_fcauth_system":0,
                            "failover_sslvpn_connection":"",
                            "sso_enabled":0,
                            "use_external_browser":0,
                            "ike_saml_port":443,
                            "keep_fqdn_resolution_consistency":0,
                            "no_vnic_dns_server":0,
                            "azure_auto_login":{
                               "enabled":0,
                               "azure_app":{
                                  "tenant_name":"",
                                  "client_id":""
                               }
                            },
                            "fgt":1,
                            "dpd_retry_count":3,
                            "dpd_retry_interval":20,
                            "certificate":null,
                            "nat_alive_freq":10
                         },
                         "ipsec_settings":{
                            "remote_networks":[
                               {
                                  "addr":"0.0.0.0",
                                  "mask":"0.0.0.0"
                               },
                               {
                                  "addr":"::/0",
                                  "mask":"::/0"
                               }
                            ],
                            "dhgroup":31,
                            "key_life_type":"seconds",
                            "key_life_seconds":3600,
                            "key_life_Kbytes":5200,
                            "replay_detection":1,
                            "pfs":1,
                            "virtualip":{
                               "type":"modeconfig",
                               "ip":"0.0.0.0",
                               "mask":"0.0.0.0",
                               "dnsserver":"0.0.0.0",
                               "winserver":"0.0.0.0"
                            },
                            "proposals":[
                               {
                                  "encryption":"AES256GCM",
                                  "authentication":"NONE"
                               },
                               {
                                  "encryption":"AES256",
                                  "authentication":"SHA512"
                               }
                            ],
                            "ipv4_split_exclude_networks":[
                               
                            ],
                            "use_vip":1
                         },
                         "on_connect":[
                            {
                               "os":"windows",
                               "script":""
                            },
                            {
                               "os":"MacOSX",
                               "script":""
                            }
                         ],
                         "on_disconnect":[
                            {
                               "os":"windows",
                               "script":""
                            },
                            {
                               "os":"MacOSX",
                               "script":""
                            }
                         ],
                         "android_cert_path":"",
                         "uid":"B0A6E39F-F101-46DC-B94E-2F42A375ECF7",
                         "warn_invalid_server_certificate":1,
                         "type":"manual"
                      }
                   ],
                   "options":{
                      "disable_default_route":0,
                      "block_ipv6":1,
                      "use_win_local_computer_cert":1,
                      "check_for_cert_private_key":0,
                      "mtu_size":1280,
                      "usesmcardcert":1,
                      "beep_if_error":0,
                      "enhanced_key_usage_mandatory":0,
                      "no_dns_registration":0,
                      "show_auth_cert_only":0,
                      "disallow_invalid_server_certificate":0,
                      "prefer_ipsecvpn_dns":1,
                      "use_gui_saml_auth":0,
                      "use_win_current_user_cert":1,
                      "enable_udp_checksum":0,
                      "usewincert":1,
                      "enabled":1
                   }
                },
                "enabled":1,
                "lockdown":{
                   "grace_period":120,
                   "max_attempts":3,
                   "detect_captive_portal":{
                      "enabled":0,
                      "os_active_probing":1
                   },
                   "exceptions":{
                      "domains":[
                         
                      ],
                      "ips":[
                         
                      ],
                      "apps":[
                         
                      ],
                      "icdb_domains":[
                         
                      ]
                   },
                   "enabled":0
                },
                "options":{
                   "after_logon_saml_auth":0,
                   "temp_password":"Start123$",
                   "enable_view_selected_vpns":0,
                   "minimize_window_on_connect":1,
                   "enable_multi_vpn":0,
                   "autoconnect_tunnel":"",
                   "show_vpn_before_logon":1,
                   "on_os_start_connect":"",
                   "secure_remote_access":0,
                   "current_connection_type":"",
                   "certs_require_keyspec":0,
                   "disable_internet_check":1,
                   "autoconnect_on_install":0,
                   "allow_personal_vpns":0,
                   "disconnect_password":"",
                   "vendor_id":"",
                   "use_windows_credentials":0,
                   "autoconnect_only_when_offnet":0,
                   "current_connection_name":"",
                   "before_logon_saml_auth":1,
                   "disable_connect_disconnect":0,
                   "on_os_start_connect_has_priority":0,
                   "suppress_vpn_notification":0,
                   "use_webview2_saml_auth":0,
                   "keep_running_max_tries":0,
                   "enforce_disabling_smartdns":0
                },
                "display_vpn":1
             }
          }
       }
    }

    In the GitHub repository, there is also a script called ems_update_profiles_entire_data.py, which has the entire data as a variable and sends it without first copying the existing profile. This is an alternative and is almost the same as creating a profile, except it is still an update.

    Creating security posture/ZTNA tags and rules

    This is the one that, I believe, is impossible to do with the official documentation, because the documentation, when it comes to rules, just says what type of values (string, integer or boolean) you have to supply, but it is not possible to know what any value should actually be. Reverse engineering luckily solves this problem.

    If you want to employ custom logic for rules, good luck with only the documentation, because all you get is string for what you need to supply.

    Both these cases are covered in the script, and there is some additional information in the comments regarding a few keys you use in rules.

    Create security posture/ZTNA tag and rules

    '''
    ems_create_ztna_tag_rule.py
    Create a ZTNA tag and associated rule using the FortiClient EMS API
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    ztna_url = f'{api_url_prefix}/tags/zero_trust/create'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #fct_based means the rule is evaluated on FortiClient directly, not on EMS
    #The id value in the rules key must be incremented for each rule of a tag
    #The type should be the position of the option in the GUI, meaning that the first element of the dropdown is 1, the second 2, etc.
    #Not all types where checked if this rule of numbering holds true and this can change in the future
    #The os number is in the order in the GUI from left to right, starting at 1 for Windows
    #The content is completely dependent on the type of rule used, but most often corresponds to whatever you would enter in a field or what you can select
    ztna_data = {
                    "name":"ZTNA-TAG_API",
                    "description":"This is the User Notification Message",
                    "status": True,
                    "comments":"Created using the API",
                    "rules":[
                        {
                            "negative": False,
                            "content":"GROUPS/ZTNA_USERS",
                            "domainName":"ad.labdomain.com",
                            "type":1,
                            "os":1,
                            "id":1,
                            "fct_based":True
                        },
                        {
                            "negative": False,
                            "content":"C:\\temp\\file1.txt",
                            "context":"",
                            "type":4,
                            "os":1,
                            "id":2,
                        }
                    ],
                    "use_custom_logic": False
                }
    
    ztna_data_custom_logic = {
                    "name":"ZTNA-TAG-CUSTOM-LOGIC_API",
                    "description":"This is the User Notification Message",
                    "status": True,
                    "comments":"Created using the API with custom logic",
                    "rules":[
                        {
                            "negative": False,
                            "content":"GROUPS/ZTNA_USERS",
                            "domainName":"ad.labdomain.com",
                            "type":1,
                            "os":1,
                            "id":1,
                            "fct_based":True
                        },
                        {
                            "negative": False,
                            "content":"C:\\temp\\file1.txt",
                            "context":"",
                            "type":4,
                            "os":1,
                            "id":2,
                        }
                    ],
                    "use_custom_logic": True,
                        "logic":{
                            "android": None,
                            "ios": None,
                            "linux": None,
                            "mac": None,
                            "windows":"{\"op\":\"or\",\"rules\":[{\"id\":1},{\"id\":2}]}"
                        }
                }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Create ZTNA tag and rule
    session.post(url=ztna_url, json=ztna_data, headers=change_headers, verify=False, timeout=30)
    
    #Create ZTNA tag and rule with custom logic
    session.post(url=ztna_url, json=ztna_data_custom_logic, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create security posture/ZTNA tag and rules response

    {
       "result":{
          "retval":1,
          "message":"Tag 'ZTNA-TAG_API' created successfully."
       },
       "data":{
          "id":81,
          "tag_uid":"8fa2287f-9c7e-43aa-a792-6cd32b16013f",
          "name":"ZTNA-TAG_API",
          "type":"zero_trust",
          "type_id":1,
          "detection_level":null,
          "description":"This is the User Notification Message",
          "update_time":"2026-06-28T14:24:12.973"
       }
    },
    {
       "result":{
          "retval":1,
          "message":"Tag 'ZTNA-TAG-CUSTOM-LOGIC_API' created successfully."
       },
       "data":{
          "id":84,
          "tag_uid":"53cf2c7d-32d7-4e6a-96d6-8ddc228dc742",
          "name":"ZTNA-TAG-CUSTOM-LOGIC_API",
          "type":"zero_trust",
          "type_id":1,
          "detection_level":null,
          "description":"This is the User Notification Message",
          "update_time":"2026-06-28T14:24:13.025"
       }
    }

    Updating security posture/ZTNA tags and rules

    If you want to update a tag and rule, you should just send the entire data to the /api/v1/tags/zero_trust/update_one endpoint as a POST, as an easy way. The problem is that a GET on /api/v1/tags/zero_trust/{ID}/get gives you different information from what the POST requires, so you’d first have to format the GET response, perform a deep merge with your new information and then send a POST. Maybe there is a better solution, but I can’t say I found one.

    Update security posture/ZTNA tag and rules

    '''
    ems_update_ztna_tag_rule.py
    Update a ZTNA tag and associated rule using the FortiClient EMS API
    
    This updates the first rule to target VPN_USERS (from ZTNA_USERS) and the second rule to search for file2.txt (from file1.txt)
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    ztna_tag_name = 'ZTNA-TAG_API'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    ztna_get_url = f'{api_url_prefix}/tags/zero_trust/index'
    ztna_update_url = f'{api_url_prefix}/tags/zero_trust/update_one'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #fct_based means the rule is evaluated on FortiClient directly, not on EMS
    #The id value in the rules key must be incremented for each rule of a tag
    #The type should be the position of the option in the GUI, meaning that the first element of the dropdown is 1, the second 2, etc.
    #Not all types where checked if this rule of numbering holds true and this can change in the future
    #The os number is in the order in the GUI from left to right, starting at 1 for Windows
    #The content is completely dependent on the type of rule used, but most often corresponds to whatever you would enter in a field or what you can select
    updated_ztna_data = {
                    "name":"ZTNA-TAG_API",
                    "description":"This is the User Notification Message",
                    "status": True,
                    "comments":"Created using the API",
                    "rules":[
                        {
                            "negative": False,
                            "content":"GROUPS/VPN_USERS",
                            "domainName":"ad.labdomain.com",
                            "type":1,
                            "os":1,
                            "id":1,
                            "fct_based":True
                        },
                        {
                            "negative": False,
                            "content":"C:\\temp\\file2.txt",
                            "context":"",
                            "type":4,
                            "os":1,
                            "id":2,
                        }
                    ],
                    "use_custom_logic": False
                }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get ZTNA tag information
    response = session.get(url=ztna_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for ztna_tag in response_decoded['data']['tags']:
        if ztna_tag['name'] == ztna_tag_name:
            updated_ztna_data['id'] = ztna_tag['id']
            session.post(url=ztna_update_url, json=updated_ztna_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Update security posture/ZTNA tag and rules response

    {
       "result":{
          "retval":1,
          "message":"Tag 'ZTNA-TAG_API' updated successfully."
       },
       "data":{
          "id":81,
          "name":"ZTNA-TAG_API"
       }
    }

    Compare the data of the updated_ztna_data variable with the data from a GET request on that tag, and you see the problem.

    GET security posture/ZTNA tag and rules response

    {
       "result":{
          "retval":1,
          "message":null
       },
       "data":{
          "tag_name":"ZTNA-TAG_API",
          "tag_type":1,
          "client_count":0,
          "description":"This is the User Notification Message",
          "detection_level":"",
          "tag_id":81,
          "status":true,
          "comments":"Created using the API",
          "use_custom_logic":false,
          "logic_windows":"{\"op\": \"and\", \"rules\": [{\"id\": 1}, {\"id\": 2}]}",
          "logic_mac":null,
          "logic_linux":null,
          "logic_ios":null,
          "logic_android":null,
          "error_msg":null,
          "tag_detection_type":null,
          "rules":[
             {
                "id":1,
                "os":1,
                "type":1,
                "negative":false,
                "content":"VPN_USERS",
                "context":"a79fcd99-07e2-4c75-a615-58043fd244da",
                "fct_based":true,
                "domain_name":"ad.labdomain.com"
             },
             {
                "id":2,
                "os":1,
                "type":4,
                "negative":false,
                "content":"C:\\temp\\file2.txt",
                "context":""
             }
          ]
       }
    }

    tag_id instead of id, the logic parts are completely different, and you have additional key-value pairs. It’s not impossible to do a nice update, but I didn’t create such a function for this post.

    Create an on-fabric detection rule

    This API endpoint is completely missing from the documentation, by the way.

    A funny thing about the Local IP/Subnet type. In the GUI, you are restricted in what you can enter, and you can only enter private IP ranges. If you use the API, you can set any and all subnets and IPs. You cannot edit the specific rule after this in the GUI, however.

    Create on-fabric detection rule

    '''
    ems_create_on_net_rule.py
    Create on-net rule using the FortiClient EMS API
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    on_net_url = f'{api_url_prefix}/on_net_rules/create'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    on_net_data = {
        "name":"ON-NET_API",
        "enabled": True,
        "comments":"On-net rule created via the API",
        "local_ip":"192.0.2.0/24",
        "dns_server_ip":"198.51.100.1",
        "public_ip":"203.0.113.1"
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Create on-net rule
    session.post(url=on_net_url, json=on_net_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create on-fabric detection rule response

    {
       "result":{
          "retval":1,
          "message":"On-fabric Detection Rule created successfully."
       },
       "data":"ON-NET_API"
    }

    Updating an on-fabric detection rule

    Much like with the ZTNA tag, the GET response is different from what you need for an update, so while /api/v1/on_net_rules/{ID}/update exists as an endpoint with a PATCH method, it is easier to send the data for the entire rule again.

    Update on-fabric detection rule

    '''
    ems_update_on_net_rule.py
    Update on-net rule using the FortiClient EMS API
    
    This updates the on-fabric rule with a new DNS server and public IP
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    on_net_rule_name = "ON-NET_API"
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    on_net_get_url = f'{api_url_prefix}/on_net_rules/index'
    on_net_update_url = f'{api_url_prefix}/on_net_rules/5/update'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    updated_on_net_data = {
        "name":f"{on_net_rule_name}",
        "enabled": True,
        "comments":"On-net rule create via the API",
        "local_ip":"192.0.2.0/24",
        "dns_server_ip":"198.51.100.100",
        "public_ip":"203.0.113.100"
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get on-net rules information
    response = session.get(url=on_net_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get ID of on-net rule, set URL accordingly and update rule
    for rule in response_decoded['data']['rule_sets']:
        if rule['name'] == on_net_rule_name:
            on_net_update_url = f'{api_url_prefix}/on_net_rules/{rule['id']}/update'
            session.patch(url=on_net_update_url, json=updated_on_net_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Update on-fabric detection rule

    '''
    ems_update_on_net_rule.py
    Update on-net rule using the FortiClient EMS API
    
    This updates the on-fabric rule with a new DNS server and public IP
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    on_net_url = f'{api_url_prefix}/on_net_rules/5/update'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    updated_on_net_data = {
        "name":"ON-NET_API",
        "enabled": True,
        "comments":"On-net rule create via the API",
        "local_ip":"192.0.2.0/24",
        "dns_server_ip":"198.51.100.100",
        "public_ip":"203.0.113.100"
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Update on-net rule
    session.patch(url=on_net_url, json=updated_on_net_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Compare the data of the updated_on_net_data variable with the data from a GET request on the rule, and you, again, see the problem.

    GET on-fabric detection rule response

    {
       "result":{
          "retval":1,
          "message":null
       },
       "data":[
          {
             "id":19,
             "rule_number":0,
             "type":8,
             "content":"198.51.100.1",
             "rule_set_id":6,
             "vdom_id":1
          },
          {
             "id":20,
             "rule_number":1,
             "type":11,
             "content":"1.1.1.0/24",
             "rule_set_id":6,
             "vdom_id":1
          },
          {
             "id":21,
             "rule_number":2,
             "type":14,
             "content":"203.0.113.1",
             "rule_set_id":6,
             "vdom_id":1
          }
       ]
    }

    We have a completely different structure here. Maybe there is a better solution, but I don’t know it.

    Creating a policy

    This is probably the most complicated piece that I cover, because a lot goes into a policy. 

    You need:

    • The assignment of imported OUs and groups, which is only a bit less difficult than the domain import
    • Get the required on-fabric detection rule ID
    • Get the IDs of each profile you want to assign

    This makes this script the one I spent the most time on, but it works.

    Create policy

    '''
    ems_create_policy.py
    Create a policy with profiles for AD OUs and groups and an on-net rule using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    idp_name = "ad.labdomain.com"
    
    #Adding groups and OUs to a policy is a bit complicated, so here is an example.
    #Consider the following AD structure:
    
    #ad.labdomain.com
    #└── CLIENTS
    #└── GROUPS
    #   ├── VPN_USERS
    #   ├── ZTNA_USERS
    #└── SERVERS
    #   └── PROD
    
    #If you want to assign the entire CLIENTS OU to a policy add the name to the assigned_ous list
    #The script will get the ID of the OU and add it to the list of IDs that should get added to the policy
    
    #If you want to add the VPN_USERS group to a policy and using the structure from above you first have to get the GUID of the GROUPS OU
    #With the GUID you can then look up all the groups in that OU and get the ID for the group and add that to the list of IDs that should get added to the policy
    #In order to facilitate this add the parent OU to the parent_ous list and the groups you want to assign, that are in the parent OU, to the assigned_group_names list
    
    assigned_ous = ['CLIENTS']
    parent_ous = ['GROUPS', 'SERVERS']
    assigned_group_names = ['PROD', 'VPN_USERS']
    
    on_net_rule_name = "ON-NET_API"
    on_net_rule_id = 0
    ou_group_ids = []
    
    #You just need to supply the name for the desired profile
    on_net_profiles = {
            "vpn": {'name':'Default'},
            "ztna": {'name':'Default'},
            "webfilter": {'name':'Default'},
            "videofilter": {'name':'Default'},
            "vulnerability_scan": {'name':'Default'},
            "malware": {'name':'Default'},
            "sandbox": {'name':'Default'},
            "firewall": {'name':'Default'},
            "ftdata_scan": {'name':'Default'},
            "system": {'name':'SYS_EMS-API'}
    }
    off_net_profiles = {
            "vpn": {'name':'VPN_EMS-API'},
            "ztna": {'name':'Default'},
            "webfilter": {'name':'Default'},
            "videofilter": {'name':'Default'},
            "vulnerability_scan": {'name':'Default'},
            "malware": {'name':'Default'},
            "sandbox": {'name':'Default'},
            "firewall": {'name':'Default'},
            "ftdata_scan": {'name':'Default'},
            "system": {'name':'SYS_EMS-API'}
    }
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    policy_url = f'{api_url_prefix}/endpoint_policies/create'
    idps_url = f'{api_url_prefix}/idps/index'
    on_net_url = f'{api_url_prefix}/on_net_rules/index'
    
    #This list is used later to loop through so we don't need to reuse code
    profile_type_names = ['vpn', 'ztna','webfilter','videofilter','vulnerability_scan','malware','sandbox','firewall','ftdata_scan','system']
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get authentication servers data
    response = session.get(url=idps_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get imported OUs from IDPS and add the IDs to the ou_group_ids list
    for idp in response_decoded['data']:
        if idp['domain_info']['name'] == idp_name:
            idp_id = idp['domain_info']['guid']
            idp_groups_url = f'{api_url_prefix}/idps/adfs/{idp_id}/imported_ous'
            response = session.get(url=idp_groups_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
    
            #The two ifs are used to search for the OU name in the assigned OUs, if we want to assign an entire OU to a policy and search through the parent OUs if we need to assign groups in OUs
            for ou in response_decoded['data']['group_containers']:
                if ou['name'] in assigned_ous or ou['name'] in assigned_group_names:
                    ou_group_ids.append(ou['id'])
                if ou['name'] in parent_ous:
                    ou_groups_url = f'{api_url_prefix}/idps/adfs/{ou['guid']}/imported_ous'
                    response = session.get(url=ou_groups_url, headers=api_headers, verify=False, timeout=30)
                    response_decoded = json.loads(response.content.decode('utf-8'))
                    for group in response_decoded['data']['group_containers']:
                        if group['name'] in assigned_group_names:
                            ou_group_ids.append(group['id'])
    
    #We create a list that will hold dictionaries with each ID that should be assigned to the policy
    endpoint_group_ids = []
    
    for id_entry in ou_group_ids:
        id_dict = {"id":id_entry}
        endpoint_group_ids.append(id_dict)
    
    #Get on-net rules and set ID based on given name
    response = session.get(url=on_net_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for rule in response_decoded['data']['rule_sets']:
        if rule['name'] == on_net_rule_name:
            on_net_rule_id = rule['id']
    
    #Loop through all profile types and add the ID for each named profile to the dictionary
    for profile_type in profile_type_names:
        profile_url = f'{api_url_prefix}/profiles/{profile_type}/index'
        response = session.get(url=profile_url, headers=api_headers, verify=False, timeout=30)
        response_decoded = json.loads(response.content.decode('utf-8'))
    
        for profile in response_decoded['data']['local']:
            if profile['name'] == on_net_profiles[f'{profile_type}']['name']:
                on_net_profiles[f'{profile_type}']['id'] = profile['id']
            if profile['name'] == off_net_profiles[f'{profile_type}']['name']:
                off_net_profiles[f'{profile_type}']['id'] = profile['id']
    
    policy_data = {
        "name":"POLICY_API",
        "endpoint_groups":endpoint_group_ids,
        "enable_on_off_net":True,
        "profile_components":{
            "vpn":{
                "id":on_net_profiles['vpn']['id'],
            },
            "ztna":{
                "id":on_net_profiles['ztna']['id'],
            },
            "webfilter":{
                "id":on_net_profiles['webfilter']['id'],
            },
            "videofilter":{
                "id":on_net_profiles['videofilter']['id'],
            },
            "vulnerability_scan":{
                "id":on_net_profiles['vulnerability_scan']['id'],
            },
            "malware":{
                "id":on_net_profiles['malware']['id'],
            },
            "sandbox":{
                "id":on_net_profiles['sandbox']['id'],
            },
            "firewall":{
                "id":on_net_profiles['firewall']['id'],
            },
            "ftdata_scan":{
                "id":on_net_profiles['ftdata_scan']['id'],
            },
            "system":{
                "id":on_net_profiles['system']['id'],
            }
        },
        "off_net_profile_components":{
            "vpn":{
                "id":off_net_profiles['vpn']['id'],
            },
            "ztna":{
                "id":off_net_profiles['ztna']['id'],
            },
            "webfilter":{
                "id":off_net_profiles['webfilter']['id'],
            },
            "videofilter":{
                "id":off_net_profiles['videofilter']['id'],
            },
            "vulnerability_scan":{
                "id":off_net_profiles['vulnerability_scan']['id'],
            },
            "malware":{
                "id":off_net_profiles['malware']['id'],
            },
            "sandbox":{
                "id":off_net_profiles['sandbox']['id'],
            },
            "firewall":{
                "id":off_net_profiles['firewall']['id'],
            },
            "ftdata_scan":{
                "id":off_net_profiles['ftdata_scan']['id'],
            },
            "system":{
                "id":off_net_profiles['system']['id'],
            }
        },
        "telemetry_server_list":None,
        "on_net_rules":[
            {
                "id":on_net_rule_id,
            }
        ],
        "comments":"Policy created using the API",
        "enabled":True,
    }
    
    #Create policy
    session.post(url=policy_url, json=policy_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create policy

    {
       "result":{
          "retval":1,
          "message":"Endpoint policy created successfully."
       },
       "data":{
          "warning":null
       }
    }

    Updating a policy

    Third time’s the charm, and updating a policy has the same problem as ZTNA tags and on-fabric detection rules. The GET response is different from what you need, so just send the entire data again for an update instead of updating just the desired components.

    Update policy

    '''
    ems_update_policy.py
    Update a policy with profiles for AD OUs and groups and an on-net rule using the FortiClient EMS API
    This script updates the policy by setting different groups (from VPN_USERS to ZTNA_USERS) and a different VPN profile in the off-net profile components (from VPN_EMS-API to Default)
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    idp_name = "ad.labdomain.com"
    policy_name = "POLICY_API"
    
    #Adding groups and OUs to a policy is a bit complicated, so here is an example.
    #Consider the following AD structure:
    
    #ad.labdomain.com
    #└── CLIENTS
    #└── GROUPS
    #   ├── VPN_USERS
    #   ├── ZTNA_USERS
    #└── SERVERS
    #   └── PROD
    
    #If you want to assign the entire CLIENTS OU to a policy add the name to the assigned_ous list
    #The script will get the ID of the OU and add it to the list of IDs that should get added to the policy
    
    #If you want to add the VPN_USERS group to a policy and using the structure from above you first have to get the GUID of the GROUPS OU
    #With the GUID you can then look up all the groups in that OU and get the ID for the group and add that to the list of IDs that should get added to the policy
    #In order to facilitate this add the parent OU to the parent_ous list and the groups you want to assign, that are in the parent OU, to the assigned_group_names list
    
    assigned_ous = ["CLIENTS"]
    parent_ous = ['GROUPS', 'SERVERS']
    assigned_group_names = ['PROD', 'ZTNA_USERS']
    
    on_net_rule_name = "ON-NET_API"
    ou_group_ids = []
    on_net_rule_id = 0
    
    #You just need to supply the name for the desired profile
    on_net_profiles = {
            "vpn": {'name':'Default'},
            "ztna": {'name':'Default'},
            "webfilter": {'name':'Default'},
            "videofilter": {'name':'Default'},
            "vulnerability_scan": {'name':'Default'},
            "malware": {'name':'Default'},
            "sandbox": {'name':'Default'},
            "firewall": {'name':'Default'},
            "ftdata_scan": {'name':'Default'},
            "system": {'name':'SYS_EMS-API'}
    }
    off_net_profiles = {
            "vpn": {'name':'Default'},
            "ztna": {'name':'Default'},
            "webfilter": {'name':'Default'},
            "videofilter": {'name':'Default'},
            "vulnerability_scan": {'name':'Default'},
            "malware": {'name':'Default'},
            "sandbox": {'name':'Default'},
            "firewall": {'name':'Default'},
            "ftdata_scan": {'name':'Default'},
            "system": {'name':'SYS_EMS-API'}
    }
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    policy_get_url = f'{api_url_prefix}/endpoint_policies/index'
    idps_url = f'{api_url_prefix}/idps/index'
    on_net_url = f'{api_url_prefix}/on_net_rules/index'
    
    #This list is used later to loop through so we don't need to reuse code
    profile_type_names = ['vpn', 'ztna','webfilter','videofilter','vulnerability_scan','malware','sandbox','firewall','ftdata_scan','system']
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get authentication servers data
    response = session.get(url=idps_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get imported OUs from IDPS and add the IDs to the ou_group_ids list
    for idp in response_decoded['data']:
        if idp['domain_info']['name'] == idp_name:
            idp_id = idp['domain_info']['guid']
            idp_groups_url = f'{api_url_prefix}/idps/adfs/{idp_id}/imported_ous'
            response = session.get(url=idp_groups_url, headers=api_headers, verify=False, timeout=30)
            response_decoded = json.loads(response.content.decode('utf-8'))
    
            #The two ifs are used to search for the OU name in the assigned OUs, if we want to assign an entire OU to a policy and search through the parent OUs if we need to assign groups in OUs
            for ou in response_decoded['data']['group_containers']:
                if ou['name'] in assigned_ous or ou['name'] in assigned_group_names:
                    ou_group_ids.append(ou['id'])
                if ou['name'] in parent_ous:
                    ou_groups_url = f'{api_url_prefix}/idps/adfs/{ou['guid']}/imported_ous'
                    response = session.get(url=ou_groups_url, headers=api_headers, verify=False, timeout=30)
                    response_decoded = json.loads(response.content.decode('utf-8'))
                    for group in response_decoded['data']['group_containers']:
                        if group['name'] in assigned_group_names:
                            ou_group_ids.append(group['id'])
    
    #We create a list that will hold dictionaries with each ID that should be assigned to the policy
    endpoint_group_ids = []
    
    for id_entry in ou_group_ids:
        id_dict = {"id":id_entry}
        endpoint_group_ids.append(id_dict)
    
    #Get on-net rules and set ID based on given name
    response = session.get(url=on_net_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for rule in response_decoded['data']['rule_sets']:
        if rule['name'] == on_net_rule_name:
            on_net_rule_id = rule['id']
    
    #Loop through all profile types and add the ID for each named profile to the dictionary
    for profile_type in profile_type_names:
        profile_url = f'{api_url_prefix}/profiles/{profile_type}/index'
        response = session.get(url=profile_url, headers=api_headers, verify=False, timeout=30)
        response_decoded = json.loads(response.content.decode('utf-8'))
    
        for profile in response_decoded['data']['local']:
            if profile['name'] == on_net_profiles[f'{profile_type}']['name']:
                on_net_profiles[f'{profile_type}']['id'] = profile['id']
            if profile['name'] == off_net_profiles[f'{profile_type}']['name']:
                off_net_profiles[f'{profile_type}']['id'] = profile['id']
    
    updated_policy_data = {
        "name":"POLICY_API",
        "endpoint_groups":endpoint_group_ids,
        "enable_on_off_net":True,
        "profile_components":{
            "vpn":{
                "id":on_net_profiles['vpn']['id'],
            },
            "ztna":{
                "id":on_net_profiles['ztna']['id'],
            },
            "webfilter":{
                "id":on_net_profiles['webfilter']['id'],
            },
            "videofilter":{
                "id":on_net_profiles['videofilter']['id'],
            },
            "vulnerability_scan":{
                "id":on_net_profiles['vulnerability_scan']['id'],
            },
            "malware":{
                "id":on_net_profiles['malware']['id'],
            },
            "sandbox":{
                "id":on_net_profiles['sandbox']['id'],
            },
            "firewall":{
                "id":on_net_profiles['firewall']['id'],
            },
            "ftdata_scan":{
                "id":on_net_profiles['ftdata_scan']['id'],
            },
            "system":{
                "id":on_net_profiles['system']['id'],
            }
        },
        "off_net_profile_components":{
            "vpn":{
                "id":off_net_profiles['vpn']['id'],
            },
            "ztna":{
                "id":off_net_profiles['ztna']['id'],
            },
            "webfilter":{
                "id":off_net_profiles['webfilter']['id'],
            },
            "videofilter":{
                "id":off_net_profiles['videofilter']['id'],
            },
            "vulnerability_scan":{
                "id":off_net_profiles['vulnerability_scan']['id'],
            },
            "malware":{
                "id":off_net_profiles['malware']['id'],
            },
            "sandbox":{
                "id":off_net_profiles['sandbox']['id'],
            },
            "firewall":{
                "id":off_net_profiles['firewall']['id'],
            },
            "ftdata_scan":{
                "id":off_net_profiles['ftdata_scan']['id'],
            },
            "system":{
                "id":off_net_profiles['system']['id'],
            }
        },
        "telemetry_server_list":None,
        "on_net_rules":[
            {
                "id":on_net_rule_id,
            }
        ],
        "comments":"Test",
        "enabled":True,
    }
    
    #Get policy ID, set patch URL and update policy
    response = session.get(url=policy_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for policy in response_decoded['data']:
        if policy['name'] == policy_name:
            policy_update_url = f'{api_url_prefix}/endpoint_policies/{policy['id']}/update'
    
            #Update policy
            session.patch(url=policy_update_url, json=updated_policy_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Update policy response

    {
       "result":{
          "retval":1,
          "message":"Endpoint policy updated successfully."
       },
       "data":{
          "warning":"None"
       }
    }

    Look at the GET from a policy and compare it with the updated_policy_data variable.

    GET policy response

    {
       "result":{
          "retval":1,
          "message":"None"
       },
       "data":[
          {
             "id":49,
             "name":"POLICY_API",
             "enable_on_off_net":true,
             "comments":"Policy created using the API",
             "enabled":true,
             "is_default":false,
             "is_sase":false,
             "priority":1,
             "groups":{
                "195":{
                   "name":"CLIENTS",
                   "path":"ad.labdomain.com/CLIENTS"
                },
                "200":{
                   "name":"PROD",
                   "path":"ad.labdomain.com/SERVERS/PROD"
                }
             },
             "rule_sets":{
                "6":{
                   "name":"ON-NET_API",
                   "enabled":true
                }
             },
             "users":{
                
             },
             "fct_users_count":{
                "synced":1,
                "unseen":0,
                "out_of_sync":0
             },
             "profile_components":{
                "malware":{
                   "id":1,
                   "name":"Default"
                },
                "sandbox":{
                   "id":1,
                   "name":"Default"
                },
                "webfilter":{
                   "id":1,
                   "name":"Default",
                   "fp_name":"None"
                },
                "firewall":{
                   "id":1,
                   "name":"Default"
                },
                "vpn":{
                   "id":1,
                   "name":"Default"
                },
                "vulnerability_scan":{
                   "id":1,
                   "name":"Default"
                },
                "system":{
                   "id":70,
                   "name":"SYS_EMS-API"
                },
                "ztna":{
                   "id":1,
                   "name":"Default"
                },
                "videofilter":{
                   "id":1,
                   "name":"Default"
                },
                "ftdata_scan":{
                   "id":1,
                   "name":"Default"
                }
             },
             "off_net_profile_components":{
                "malware":{
                   "id":1,
                   "name":"Default"
                },
                "sandbox":{
                   "id":1,
                   "name":"Default"
                },
                "webfilter":{
                   "id":1,
                   "name":"Default",
                   "fp_name":"None"
                },
                "firewall":{
                   "id":1,
                   "name":"Default"
                },
                "vpn":{
                   "id":70,
                   "name":"VPN_EMS-API"
                },
                "vulnerability_scan":{
                   "id":1,
                   "name":"Default"
                },
                "system":{
                   "id":70,
                   "name":"SYS_EMS-API"
                },
                "ztna":{
                   "id":1,
                   "name":"Default"
                },
                "videofilter":{
                   "id":1,
                   "name":"Default"
                },
                "ftdata_scan":{
                   "id":1,
                   "name":"Default"
                }
             }
          },
          {
             "id":1,
             "name":"Default",
             "enable_on_off_net":false,
             "comments":"",
             "enabled":true,
             "is_default":true,
             "is_sase":false,
             "priority":2,
             "groups":{
                
             },
             "rule_sets":{
                
             },
             "users":{
                
             },
             "fct_users_count":{
                "synced":0,
                "unseen":0,
                "out_of_sync":0
             },
             "profile_components":{
                "malware":{
                   "id":1,
                   "name":"Default"
                },
                "sandbox":{
                   "id":1,
                   "name":"Default"
                },
                "webfilter":{
                   "id":1,
                   "name":"Default",
                   "fp_name":"None"
                },
                "firewall":{
                   "id":1,
                   "name":"Default"
                },
                "vpn":{
                   "id":1,
                   "name":"Default"
                },
                "vulnerability_scan":{
                   "id":1,
                   "name":"Default"
                },
                "system":{
                   "id":34,
                   "name":"LAB-SYS"
                },
                "ztna":{
                   "id":1,
                   "name":"Default"
                },
                "videofilter":{
                   "id":1,
                   "name":"Default"
                },
                "ftdata_scan":{
                   "id":1,
                   "name":"Default"
                }
             },
             "off_net_profile_components":{
                
             }
          }
       ]
    }

    groups instead of endpoint_groups in a different format and rule_sets instead of on_net_rules, again, in a different format.

    Creating an installer

    All installer API endpoints are missing from the documentation.

    Creating an installer isn’t that difficult, and most options are self-explanatory, but you have to keep the three variables for the version and the features in mind when working with it.

    Note: Feature 15, EDR, is only available in Cloud EMS. If you have an on-prem EMS, like me, you have to remove this feature. I have commented it out in my script. Also, installer names are kept track of internally, so you cannot create installers with the same name, even if you delete one. Maybe there is some cleanup on EMS upgrades or periodically, however.

    Create installer

    '''
    ems_create_installer.py
    Create FortiClient installer using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    version_major_minor = "7.4"
    installer_name = "7.4.7"
    fct_comparable = 7004007
    system_profile_name = "SYS_EMS-API"
    system_profile_id = 0
    vpn_profile_name = "VPN_EMS-API"
    vpn_profile_id = 0
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    installer_url = f'{api_url_prefix}/assignable_installers/create'
    system_profiles_get_url = f'{api_url_prefix}/profiles/system/index'
    vpn_profiles_get_url = f'{api_url_prefix}/profiles/vpn/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get system profiles
    response = session.get(url=system_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == system_profile_name:
            system_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            system_profile_id = list(system_profile_id)[0]
    
    #Get VPN profiles
    response = session.get(url=vpn_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == vpn_profile_name:
            vpn_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            vpn_profile_id = list(vpn_profile_id)[0]
    
    installer_data = {
        "name":"API-INSTALLER",
        "notes":"FortiClient installer created via API",
        "version_major_minor":version_major_minor,
        "auto_update":None,
        "installer_name":installer_name,
        "fct_comparable":fct_comparable,
        "windows_installer":True,
        "mac_installer":True,
        "linux_installer":True,
        "windows_arm_installer":False,
        "linux_arm_installer":False ,
        "features":[
            5, #Zero Trust Telemetry
            3, #Secure Access Architecture Components
            7, #Vulnerability Scan
            6, #Advanced Persistent Threat (APT) Components
            1, #AntiVirus, Anti-Exploit, Removable Media Access
            10, #Anti-Ransomware
            9, #Cloud Based Malware Outbreak Detection
            2, #Web and Video Filtering
            4, #Application Firewall
            8, #Single Sign-On Mobility Agent
            11, #Zero Trust Network Access
            13, #Privileged Access Agent
            16, #Data Protection
    		12#, #FIPS Certification
            #15, #EDR, only in cloud EMS, remove if on-prem EMS
        ],
        "auto_register":True,
        "desktop_shortcut":True,
        "start_menu_shortcut":False,
        "msi_files":True,
        "override_invitation_code":False,
        "group_assignment_rules_id":None,
        "vpn_profile_component_id":vpn_profile_id,
        "system_profile_component_id":system_profile_id,
        "invalid_cert_action":None,
        "telemetry_server_list_id":None
    }
    
    #Create installer
    response = session.post(url=installer_url, json=installer_data, headers=change_headers, verify=False, timeout=30)
    response_decoded = response.content.decode('utf-8')
    print(response_decoded)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create installer response

    {
       "result":{
          "retval":1,
          "message":null
       }
    }

    Updating an installer

    Like always at this point, the GET is different from what you need, so send the entire data as your payload again.

    Update installer

    '''
    ems_update_installer.py
    Update FortiClient installer using the FortiClient EMS API
    
    This updates the existing installer by:
    * Setting Mac and Linux installers to False
    * Removing the Data Protection and FIPS Certification features
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    installer_real_name = "API-INSTALLER"
    version_major_minor = "7.4"
    installer_name = "7.4.7"
    fct_comparable = 7004007
    system_profile_name = "SYS_EMS-API"
    system_profile_id = 0
    vpn_profile_name = "VPN_EMS-API"
    vpn_profile_id = 0
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    installer_index_url = f'{api_url_prefix}/assignable_installers/index'
    system_profiles_get_url = f'{api_url_prefix}/profiles/system/index'
    vpn_profiles_get_url = f'{api_url_prefix}/profiles/vpn/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get system profiles
    response = session.get(url=system_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == system_profile_name:
            system_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            system_profile_id = list(system_profile_id)[0]
    
    #Get VPN profiles
    response = session.get(url=vpn_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == vpn_profile_name:
            vpn_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            vpn_profile_id = list(vpn_profile_id)[0]
    
    updated_installer_data = {
        "name":"API-INSTALLER",
        "notes":"FortiClient installer updated via API",
        "version_major_minor":version_major_minor,
        "auto_update":None,
        "installer_name":installer_name,
        "fct_comparable":fct_comparable,
        "windows_installer":True,
        "mac_installer":False,
        "linux_installer":False,
        "windows_arm_installer":False,
        "linux_arm_installer":False ,
        "features":[
            5, #Zero Trust Telemetry
            3, #Secure Access Architecture Components
            7, #Vulnerability Scan
            6, #Advanced Persistent Threat (APT) Components
            1, #AntiVirus, Anti-Exploit, Removable Media Access
            10, #Anti-Ransomware
            9, #Cloud Based Malware Outbreak Detection
            2, #Web and Video Filtering
            4, #Application Firewall
            8, #Single Sign-On Mobility Agent
            11, #Zero Trust Network Access
            13, #Privileged Access Agent
        ],
        "auto_register":True,
        "desktop_shortcut":True,
        "start_menu_shortcut":False,
        "msi_files":True,
        "override_invitation_code":False,
        "group_assignment_rules_id":None,
        "vpn_profile_component_id":vpn_profile_id,
        "system_profile_component_id":system_profile_id,
        "invalid_cert_action":None,
        "telemetry_server_list_id":None
    }
    
    #Get installer ID
    response = session.get(url=installer_index_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for installers in response_decoded['data']['installers']:
        if installers['name'] == installer_real_name:
            installer_id = {installers["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            installer_id = list(installer_id)[0]
            #Assemble the URLs with the ID
            installer_update_url = f'{api_url_prefix}/assignable_installers/{installer_id}/update'
    
            #Update installer
            session.patch(url=installer_update_url, json=updated_installer_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Update installer response

    {
       "result":{
          "retval":1,
          "message":null
       }
    }

    If we look at the GET, we can see that there is a lot more information in an installer, like the entire VPN and system settings profile information, if you have attached one.

    GET installer response

    {
       "result":{
          "retval":1,
          "message":null
       },
       "data":{
          "id":35,
          "fds":true,
          "name":"API-INSTALLER",
          "notes":"FortiClient installer updated via API",
          "folder":"api-installer",
          "auto_register":true,
          "desktop_shortcut":true,
          "start_menu_shortcut":false,
          "msi_files":true,
          "override_invitation_code":false,
          "windows_installer":true,
          "windows_arm_installer":false,
          "linux_installer":false,
          "linux_arm_installer":false,
          "mac_installer":false,
          "invalid_cert_action":null,
          "features":[
             1,
             2,
             3,
             4,
             5,
             6,
             7,
             8,
             9,
             10,
             11,
             13
          ],
          "uninstaller":false,
          "installer_name":"7.4.7",
          "installer_win":{
             "id":1,
             "name":"7.4.7",
             "is_official":true,
             "os":"win",
             "custom":0,
             "version_major_minor":"7.4",
             "version":"7.4.7",
             "version_comparable":7004007
          },
          "installer_win_invalid":false,
          "installer_mac":{
             "id":1,
             "name":"7.4.7",
             "is_official":true,
             "os":"osx",
             "custom":0,
             "version_major_minor":"7.4",
             "version":"7.4.7",
             "version_comparable":7004007
          },
          "installer_mac_invalid":false,
          "installer_linux":{
             "id":1,
             "name":"7.4.7",
             "is_official":true,
             "os":"lin",
             "custom":0,
             "version_major_minor":"7.4",
             "version":"7.4.7",
             "version_comparable":7004007
          },
          "installer_linux_invalid":false,
          "telemetry_server_list":{
             
          },
          "group_assignment_rule":{
             
          },
          "supported":true,
          "standalone_invitation_code":null,
          "vpn_component":{
             "id":70,
             "name":"VPN_EMS-API",
             "json":{
                "vpn":{
                   "sslvpn":{
                      "connections":[
                         
                      ],
                      "options":{
                         "dtls_mtu":1100,
                         "no_dns_registration":0,
                         "show_auth_cert_only":0,
                         "disallow_invalid_server_certificate":0,
                         "use_gui_saml_auth":0,
                         "warn_invalid_server_certificate":1,
                         "block_ipv6":1,
                         "negative_split_tunnel_metric":null,
                         "preferred_dtls_tunnel":0,
                         "mtu_size":1300,
                         "dnscache_service_control":0,
                         "enabled":0,
                         "prefer_sslvpn_dns":1
                      }
                   },
                   "ipsecvpn":{
                      "connections":[
                         {
                            "name":"API-IPSEC-VPN",
                            "pinned":0,
                            "dns_priority":1,
                            "machine":0,
                            "keep_running":0,
                            "traffic_keep_strategy":0,
                            "traffic_keep_timer":5000,
                            "disclaimer_msg":"",
                            "single_user_mode":0,
                            "ui":{
                               "show_remember_password":0,
                               "show_alwaysup":0,
                               "show_autoconnect":0,
                               "show_passcode":0,
                               "save_username":1
                            },
                            "traffic_control":{
                               "enabled":0,
                               "mode":1,
                               "apps":[
                                  
                               ],
                               "fqdns":[
                                  
                               ],
                               "isdb_objects":[
                                  
                               ],
                               "vsdb_objects":[
                                  
                               ]
                            },
                            "redundant_sort_method":0,
                            "tags":{
                               "allowed":"",
                               "prohibited":""
                            },
                            "host_check_fail_warning":"",
                            "ike_settings":{
                               "server":"192.0.2.254",
                               "authentication_method":"Preshared Key",
                               "auth_data":"Enc f7446dfccd809fbf6486f27f81c515eafa55049574aae9672f18533318cdb297e0",
                               "transport_mode":0,
                               "tcp_port":443,
                               "udp_port":500,
                               "cert_subjectcheck":0,
                               "prompt_certificate":0,
                               "xauth_timeout":120,
                               "xauth":{
                                  "use_otp":0,
                                  "enabled":0,
                                  "prompt_username":0,
                                  "username":"",
                                  "password":""
                               },
                               "version":2,
                               "mode":"aggressive",
                               "dhgroup":[
                                  21
                               ],
                               "key_life":28800,
                               "localid":"",
                               "networkid":0,
                               "eap_method":1,
                               "implied_SPDO":0,
                               "implied_SPDO_timeout":60,
                               "nat_traversal":1,
                               "enable_local_lan":1,
                               "session_resume":1,
                               "enable_ike_fragmentation":1,
                               "mode_config":1,
                               "modeconfig_type":0,
                               "dpd":1,
                               "proposals":[
                                  {
                                     "encryption":"AES128",
                                     "authentication":"SHA256"
                                  },
                                  {
                                     "encryption":"AES256",
                                     "authentication":"SHA256"
                                  }
                               ],
                               "run_fcauth_system":0,
                               "failover_sslvpn_connection":"",
                               "sso_enabled":0,
                               "use_external_browser":0,
                               "ike_saml_port":443,
                               "keep_fqdn_resolution_consistency":0,
                               "no_vnic_dns_server":0,
                               "azure_auto_login":{
                                  "enabled":0,
                                  "azure_app":{
                                     "tenant_name":"",
                                     "client_id":""
                                  }
                               },
                               "fgt":1,
                               "dpd_retry_count":3,
                               "dpd_retry_interval":20,
                               "certificate":null,
                               "nat_alive_freq":10
                            },
                            "ipsec_settings":{
                               "remote_networks":[
                                  {
                                     "addr":"0.0.0.0",
                                     "mask":"0.0.0.0"
                                  },
                                  {
                                     "addr":"::/0",
                                     "mask":"::/0"
                                  }
                               ],
                               "dhgroup":31,
                               "key_life_type":"seconds",
                               "key_life_seconds":3600,
                               "key_life_Kbytes":5200,
                               "replay_detection":1,
                               "pfs":1,
                               "virtualip":{
                                  "type":"modeconfig",
                                  "ip":"0.0.0.0",
                                  "mask":"0.0.0.0",
                                  "dnsserver":"0.0.0.0",
                                  "winserver":"0.0.0.0"
                               },
                               "proposals":[
                                  {
                                     "encryption":"AES256GCM",
                                     "authentication":"NONE"
                                  },
                                  {
                                     "encryption":"AES256",
                                     "authentication":"SHA512"
                                  }
                               ],
                               "ipv4_split_exclude_networks":[
                                  
                               ],
                               "use_vip":1
                            },
                            "on_connect":[
                               {
                                  "os":"windows",
                                  "script":""
                               },
                               {
                                  "os":"MacOSX",
                                  "script":""
                               }
                            ],
                            "on_disconnect":[
                               {
                                  "os":"windows",
                                  "script":""
                               },
                               {
                                  "os":"MacOSX",
                                  "script":""
                               }
                            ],
                            "android_cert_path":"",
                            "uid":"B0A6E39F-F101-46DC-B94E-2F42A375ECF7",
                            "warn_invalid_server_certificate":1,
                            "type":"manual"
                         }
                      ],
                      "options":{
                         "disable_default_route":0,
                         "block_ipv6":1,
                         "use_win_local_computer_cert":1,
                         "check_for_cert_private_key":0,
                         "mtu_size":1280,
                         "usesmcardcert":1,
                         "beep_if_error":0,
                         "enhanced_key_usage_mandatory":0,
                         "no_dns_registration":0,
                         "show_auth_cert_only":0,
                         "disallow_invalid_server_certificate":0,
                         "prefer_ipsecvpn_dns":1,
                         "use_gui_saml_auth":0,
                         "use_win_current_user_cert":1,
                         "enable_udp_checksum":0,
                         "usewincert":1,
                         "enabled":1
                      }
                   },
                   "enabled":1,
                   "lockdown":{
                      "grace_period":120,
                      "max_attempts":3,
                      "detect_captive_portal":{
                         "enabled":0,
                         "os_active_probing":1
                      },
                      "exceptions":{
                         "domains":[
                            
                         ],
                         "ips":[
                            
                         ],
                         "apps":[
                            
                         ],
                         "icdb_domains":[
                            
                         ]
                      },
                      "enabled":0
                   },
                   "options":{
                      "after_logon_saml_auth":0,
                      "temp_password":"Start123$",
                      "enable_view_selected_vpns":0,
                      "minimize_window_on_connect":1,
                      "enable_multi_vpn":0,
                      "autoconnect_tunnel":"",
                      "show_vpn_before_logon":1,
                      "on_os_start_connect":"",
                      "secure_remote_access":0,
                      "current_connection_type":"",
                      "certs_require_keyspec":0,
                      "disable_internet_check":1,
                      "autoconnect_on_install":0,
                      "allow_personal_vpns":0,
                      "disconnect_password":"",
                      "vendor_id":"",
                      "use_windows_credentials":0,
                      "autoconnect_only_when_offnet":0,
                      "current_connection_name":"",
                      "before_logon_saml_auth":1,
                      "disable_connect_disconnect":0,
                      "on_os_start_connect_has_priority":0,
                      "suppress_vpn_notification":0,
                      "use_webview2_saml_auth":0,
                      "keep_running_max_tries":0,
                      "enforce_disabling_smartdns":0
                   },
                   "display_vpn":1
                }
             },
             "is_chromebook":false,
             "is_default":false,
             "is_sase":false,
             "update_time":"2026-06-28T14:13:37.294",
             "enabled":true,
             "display_enabled":true,
             "parser_error":null,
             "type":5
          },
          "system_component":{
             "id":70,
             "name":"SYS_EMS-API",
             "json":{
                "fssoma":{
                   "enabled":0,
                   "presharedkey":"",
                   "serveraddress":""
                },
                "version":"5.6.0",
                "pam":{
                   "enabled":0,
                   "default_port":9191
                },
                "endpoint_control":{
                   "forensics_license":1,
                   "enable_dns_cache":0,
                   "enable_dem":0,
                   "send_software_inventory":0,
                   "disable_fgt_switch":0,
                   "auto_start":0,
                   "disable_unregister":1,
                   "notify_fgt_on_logoff":0,
                   "invalid_cert_action":"warn",
                   "ui":{
                      "hide_compliance_warning":0
                   },
                   "show_bubble_notifications":0,
                   "edr_collector":1
                },
                "system":{
                   "certificates":[
                      
                   ],
                   "cryptography":{
                      "drbg_reseed_minutes":1440
                   },
                   "update":{
                      "timeout":60,
                      "auto_patch":0,
                      "fail_over_to_fdn":0,
                      "restrict_services_to_regions":"",
                      "scheduled_update":{
                         "type":"interval",
                         "enabled":1,
                         "daily_at":"00:00",
                         "update_interval_in_hours":1
                      },
                      "submit_virus_info_to_fds":1,
                      "port":80,
                      "update_action":"disable",
                      "failoverport":8000,
                      "use_legacy_fdn":0,
                      "ocsp_mode":0,
                      "submit_vuln_info_to_fds":1,
                      "server":"",
                      "use_custom_server":0
                   },
                   "proxy":{
                      "username":null,
                      "update":0,
                      "fail_over_to_fdn":0,
                      "port":80,
                      "virus_submission":0,
                      "type":"http",
                      "password":"",
                      "address":null,
                      "online_scep":0
                   },
                   "installer":{
                      "allow_admin_uninstall_when_locked":1
                   },
                   "proc_protect":1,
                   "user_identity":{
                      "enable_google":0,
                      "enable_linkedin":0,
                      "enable_manually_entering":0,
                      "notify_user":0,
                      "enable_salesforce":0
                   },
                   "log_settings":{
                      "onnet_local_logging":1,
                      "level":6,
                      "log_events":"antiexploit,antiransomware,av,cloudscan,endpoint,firewall,fssoma,ipsecvpn,pam,sandboxing,sslvpn,update,vuln,webfilter,ztna,configd,scheduler,shield,wanacc",
                      "remote_logging":{
                         "send_os_events":{
                            "enabled":1,
                            "interval":120
                         },
                         "log_upload_enabled":0,
                         "log_retention_days":90,
                         "log_upload_ssl_enabled":1,
                         "send_software_inventory":0,
                         "log_upload_server":"",
                         "log_generation_timeout_secs":900,
                         "netlog_categories":32,
                         "log_upload_freq_minutes":60,
                         "log_compressed":0
                      }
                   },
                   "fortiproxy":{
                      "http_timeout":60,
                      "selftest":{
                         "notify":1,
                         "enabled":1,
                         "last_port":65535
                      },
                      "client_comforting":{
                         "pop3_client":1,
                         "pop3_server":1,
                         "smtp":1
                      },
                      "enable_https_proxy":1,
                      "enabled":1
                   },
                   "ui":{
                      "allow_shutdown_when_registered":0,
                      "disable_backup":0,
                      "unreg_pwd":"goBsfMBfsTRbI3732fJADZ2gJEqSFB7zeBKnNb2U3WRDVgKla5vP6XG4xPpK85mDIpq2xSvSfyagMMdQ7Hl73WiOMntt5LdOOgUvUi0MT8Oq74vif2XsiFd3tf57SFV7$TQJ1qTiQelcT/qoHkN4laeQI6jvKLeoe+1WevZOq9oBktCLyBB4goInKHqF9jiPGDNhrT7rToVWNEXKeqF8myQ==",
                      "lock":"Enc 283431078f7ac0acbb5fd257d3279947f5cd01bbe523458b9d50b1a1d5a8bf373b784abec45318e4646034cf13d32e00df29a194c3a247b9db53f6e768362e37239b7005d02266424b0efd63aac9c86669a86c0a0368caa7",
                      "password":"Enc 68be3193bbd992ab167dd677ba394598d6c827dcedc1d91d842296f9fcfb9cc97b6d62fc1e38124e0b1ea9dc2233665e1a76f192808a1f09",
                      "culture_code":"os-default",
                      "hide_user_info":0,
                      "default_tab":"VPN",
                      "hide_system_tray_icon":0,
                      "show_host_tag":0
                   }
                },
                "wan_optimization":{
                   "support_cifs":1,
                   "support_http":1,
                   "support_ftp":1,
                   "enabled":0,
                   "support_mapi":1,
                   "max_disk_cache_size_mb":512
                },
                "extra":{
                   "trigger_vuln_scan":true
                }
             },
             "is_chromebook":false,
             "is_default":false,
             "is_sase":false,
             "update_time":"2026-06-28T14:13:37.243",
             "enabled":true,
             "display_enabled":true,
             "parser_error":null,
             "type":7
          },
          "assembly_error_type":0,
          "assembly_state":2,
          "assembly_progress":100,
          "hotfix_name":null,
          "hotfix_details":[
             
          ],
          "auto_update":null
       }
    }

    Creating an invitation

    If you work with Cloud EMS, note that the API endpoints use /cloud/invitations instead of the on-prem /api/v1/invitation

    Invitations are relatively straightforward, and the only thing of note is that if you want to have an installer attached, you need to create it with the invitation. You cannot create an invitation and later attach an installer.

    Create invitation

    '''
    ems_create_invitation.py
    Create invitation with domain verification and installer using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    version_major_minor = "7.4"
    installer_name = "7.4.7"
    fct_comparable = 7004007
    system_profile_name = "SYS_EMS-API"
    system_profile_id = 0
    vpn_profile_name = "VPN_EMS-API"
    vpn_profile_id = 0
    idp_name = "ad.labdomain.com"
    idp_id = 0
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    invitation_url = f'{api_url_prefix}/invitation/create'
    system_profiles_get_url = f'{api_url_prefix}/profiles/system/index'
    vpn_profiles_get_url = f'{api_url_prefix}/profiles/vpn/index'
    idps_url = f'{api_url_prefix}/idps/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get system profiles
    response = session.get(url=system_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == system_profile_name:
            system_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            system_profile_id = list(system_profile_id)[0]
    
    #Get VPN profiles
    response = session.get(url=vpn_profiles_get_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    for profile in response_decoded['data']['local']:
        if profile['name'] == vpn_profile_name:
            vpn_profile_id = {profile["id"]}
            #The id is a set, so we convert it to a list and get the only element from it
            vpn_profile_id = list(vpn_profile_id)[0]
    
    #Get authentication servers data
    response = session.get(url=idps_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get all the necessary information directly from the IDP and create the data for the domain import
    for idp in response_decoded['data']:
        if idp['domain_info']['name'] == idp_name:
            idp_id = idp['domain_info']['guid']
    
    #Use None for expiry_date to have no expiration
    #The mapping for the authentication type is 0=None, 1=Local, 2=Domain, 3=SAML
    invitation_data = {
        "name":"API-INVITATION-INSTALLER",
        "comments":"Invitation created via API",
        "has_email_notifications":False,
        "expiry_date":"2026-12-31",
        "authentication_type":2,
        "email_template_id":1,
        "is_bulk":True,
        "listen_address":f"{ems_server}:8013",
        "assignable_installer":{
            "name":"INVITATION-INSTALLER",
            "notes":"FortiClient invitation installer created via API",
            "version_major_minor":version_major_minor,
            "auto_update":None,
            "installer_name":installer_name,
            "fct_comparable":fct_comparable,
            "windows_installer":True,
            "mac_installer":True,
            "linux_installer":True,
            "windows_arm_installer":False,
            "linux_arm_installer":False ,
            "features":[
                5, #Zero Trust Telemetry
                3, #Secure Access Architecture Components
                7, #Vulnerability Scan
                6, #Advanced Persistent Threat (APT) Components
                1, #AntiVirus, Anti-Exploit, Removable Media Access
                10, #Anti-Ransomware
                9, #Cloud Based Malware Outbreak Detection
                2, #Web and Video Filtering
                4, #Application Firewall
                8, #Single Sign-On Mobility Agent
                11, #Zero Trust Network Access
                13, #Privileged Access Agent
                16, #Data Protection
                12#, #FIPS Certification
                #15, #EDR, only in cloud EMS, remove if on-prem EMS
            ],
            "auto_register":True,
            "desktop_shortcut":True,
            "start_menu_shortcut":False,
            "msi_files":True,
            "override_invitation_code":False,
            "group_assignment_rules_id":None,
            "vpn_profile_component_id":vpn_profile_id,
            "system_profile_component_id":system_profile_id,
            "invalid_cert_action":None,
            "telemetry_server_list_id":None
        },
        "domain_guid":idp_id,
        "user_id":None,
        "saml_config_id":None
    }
    
    #Create invitation
    session.post(url=invitation_url, json=invitation_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Create invitation response

    {
       "result":{
          "retval":1,
          "message":null
       },
       "data":{
          "id":7,
          "uid":"6f868940-eca1-4880-8ac0-21f8ba350158",
          "name":"API-INVITATION-INSTALLER",
          "invitation_code":"_VjE6MTkyLjE2OC4xLjIwODo4MDEzOmRlZmF1bHQ6NmY4Njg5NDAtZWNhMS00ODgwLThhYzAtMjFmOGJhMzUwMTU4"
       }
    }

    Updating an invitation

    This process is pointless. Apparently, you can only change the name, EMS listen address, and comment of an existing invitation, even if you do update it. You can send more information, like changing the Verification Type, but it doesn’t actually update it. There is an update script in the repository, but I don’t bother posting one here.

    Getting endpoint data and matching on the endpoint name

    Probably the most fundamental API call and it’s almost at the end.

    The script gets all endpoint data using the API and performs a match on a specific endpoint using the endpoint’s name.

    GET endpoint data and match on endpoint name

    '''
    ems_get_endpoint_data.py
    Get endpoint data and ID for named endpoint using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    endpoint_name = "WIN11-CLIENT"
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    endpoints_url = f'{api_url_prefix}/endpoints/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get endpoint data
    response = session.get(url=endpoints_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    print(response_decoded)
    
    #Get endpoint ID with name match
    for endpoint in response_decoded['data']['endpoints']:
        if endpoint['name'] == endpoint_name:
            print(f"Endpoint ID for {endpoint_name}: {endpoint['device_id']}")
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    GET endpoint data and match on endpoint name response

    {
       "result":{
          "retval":1,
          "message":"None"
       },
       "data":{
          "endpoints":[
             {
                "device_id":55,
                "host":"FCXLAB",
                "name":"FCXLAB",
                "ip_addr":"None",
                "os_version":"Microsoft Windows 11 Pro",
                "model":"",
                "vendor":"",
                "cpu":"",
                "memory":0,
                "sn":"",
                "hdd":"None",
                "public_ip_addr":"None",
                "domain_id":2,
                "installer_name":"None",
                "deployment_state":"None",
                "fdc_campaign_deployment_status":0,
                "fgt_sn":"None",
                "forticlient_id":"None",
                "uid":"None",
                "fct_version":"None",
                "diskenc":"None",
                "av_product":"None",
                "is_installed":false,
                "is_managed":false,
                "is_migrating":false,
                "is_ems_registered":"None",
                "can_quarantine":"None",
                "is_ems_online":false,
                "is_ems_onnet":"None",
                "is_excluded":false,
                "is_quarantined":0,
                "quarantine_access_code":"None",
                "invitation_name":"None",
                "invitation_code":"None",
                "invitation_version":"None",
                "comparable_fct_version":"None",
                "last_seen":"None",
                "last_seen_fct_user_id":"None",
                "endpoint_policy_name":"None",
                "endpoint_policy_id":"None",
                "ip_list_name":"None",
                "orig_groups":[
                   
                ],
                "av_enabled":"None",
                "rtp_enabled":"None",
                "ae_enabled":"None",
                "cs_enabled":"None",
                "rm_enabled":"None",
                "fw_enabled":"None",
                "wf_enabled":"None",
                "vf_enabled":"None",
                "vpn_enabled":"None",
                "vuln_enabled":"None",
                "ssoma_enabled":"None",
                "sb_enabled":"None",
                "sb_cloud_enabled":"None",
                "fd_enabled":"None",
                "rs_enabled":"None",
                "edr_installed":"None",
                "edr_app_enabled":"None",
                "edr_feature_enabled":"None",
                "onboarding_supported":"None",
                "client_version_up_to_date":true,
                "client_av_sig_version_up_to_date":true,
                "client_policy_synced":true,
                "client_policy_primary_synced":true,
                "client_policy_offnet_synced":true,
                "client_policy_iplist_synced":true,
                "client_policy_onnet_rule_synced":true,
                "client_policy_verification_rule_synced":true,
                "sys_events_count":0,
                "av_events_count":0,
                "wf_events_count":0,
                "vf_events_count":0,
                "fw_events_count":0,
                "sb_events_count":0,
                "fd_events_count":0,
                "ae_events_count":0,
                "rm_events_count":0,
                "cs_events_count":0,
                "unreg_events_count":0,
                "rs_events_count":0,
                "nwifsc_events_count":0,
                "owmsg_events_count":0,
                "reg_events_count":0,
                "ztna_sign_events_count":0,
                "ztna_revoke_events_count":0,
                "pam_events_count":0,
                "vcm_events_count":0,
                "vpn_events_count":0,
                "vuln_events_count":0,
                "pua_events_count":0,
                "vuln_events_max_severity":"None",
                "fdc_events_count":0,
                "forensics_enabled":false,
                "groups":[
                   {
                      "group_id":195,
                      "group_name":"CLIENTS",
                      "group_path":"ad.labdomain.com/CLIENTS"
                   }
                ],
                "profile_components":{
                   
                },
                "off_net_profile_components":{
                   
                },
                "health_warning_count":0,
                "health_error_count":0
             },
             {
                "device_id":1,
                "host":"WIN11-CLIENT",
                "name":"WIN11-CLIENT",
                "ip_addr":"192.168.1.231",
                "os_version":"Microsoft Windows 11 Professional Edition, 64-bit (build 26200)",
                "model":"VMware20,1",
                "vendor":"VMware, Inc.",
                "cpu":"Intel(R) Core(TM) i7-14700K",
                "memory":8190,
                "sn":"VMware-56 4d e8 7f a0 41 62 67-9d a7 24 c8 bb e7 ff 97",
                "hdd":63,
                "public_ip_addr":"",
                "domain_id":2,
                "installer_name":"None",
                "deployment_state":"None",
                "fdc_campaign_deployment_status":"None",
                "fgt_sn":"None",
                "forticlient_id":1,
                "uid":"6E23E7C3ABBF4FE797BB811A42F0A6F3",
                "fct_version":"7.4.7.2003",
                "diskenc":"",
                "av_product":"Antivirus Microsoft Defender",
                "is_installed":true,
                "is_managed":true,
                "is_migrating":false,
                "is_ems_registered":true,
                "can_quarantine":true,
                "is_ems_online":true,
                "is_ems_onnet":false,
                "is_excluded":false,
                "is_quarantined":0,
                "quarantine_access_code":"None",
                "invitation_name":"None",
                "invitation_code":"None",
                "invitation_version":"None",
                "comparable_fct_version":7004007,
                "last_seen":"2026-06-29T16:20:38",
                "last_seen_fct_user_id":35,
                "endpoint_policy_name":"POLICY_API",
                "endpoint_policy_id":53,
                "ip_list_name":"None",
                "orig_groups":[
                   
                ],
                "av_enabled":false,
                "rtp_enabled":false,
                "ae_enabled":false,
                "cs_enabled":false,
                "rm_enabled":false,
                "fw_enabled":false,
                "wf_enabled":false,
                "vf_enabled":false,
                "vpn_enabled":true,
                "vuln_enabled":true,
                "ssoma_enabled":false,
                "sb_enabled":false,
                "sb_cloud_enabled":false,
                "fd_enabled":false,
                "rs_enabled":false,
                "edr_installed":false,
                "edr_app_enabled":false,
                "edr_feature_enabled":false,
                "onboarding_supported":true,
                "client_version_up_to_date":true,
                "client_av_sig_version_up_to_date":true,
                "client_policy_synced":true,
                "client_policy_primary_synced":true,
                "client_policy_offnet_synced":true,
                "client_policy_iplist_synced":true,
                "client_policy_onnet_rule_synced":true,
                "client_policy_verification_rule_synced":true,
                "sys_events_count":110,
                "av_events_count":0,
                "wf_events_count":0,
                "vf_events_count":0,
                "fw_events_count":0,
                "sb_events_count":0,
                "fd_events_count":0,
                "ae_events_count":0,
                "rm_events_count":0,
                "cs_events_count":0,
                "unreg_events_count":0,
                "rs_events_count":0,
                "nwifsc_events_count":101,
                "owmsg_events_count":0,
                "reg_events_count":0,
                "ztna_sign_events_count":0,
                "ztna_revoke_events_count":1,
                "pam_events_count":0,
                "vcm_events_count":0,
                "vpn_events_count":0,
                "vuln_events_count":1,
                "pua_events_count":0,
                "vuln_events_max_severity":7.800000190734863,
                "fdc_events_count":0,
                "forensics_enabled":false,
                "groups":[
                   {
                      "group_id":195,
                      "group_name":"CLIENTS",
                      "group_path":"ad.labdomain.com/CLIENTS"
                   }
                ],
                "profile_components":{
                   "malware":{
                      "id":1,
                      "name":"Default"
                   },
                   "sandbox":{
                      "id":1,
                      "name":"Default"
                   },
                   "webfilter":{
                      "id":1,
                      "name":"Default",
                      "fp_name":"None"
                   },
                   "firewall":{
                      "id":1,
                      "name":"Default"
                   },
                   "vpn":{
                      "id":1,
                      "name":"Default"
                   },
                   "vulnerability_scan":{
                      "id":1,
                      "name":"Default"
                   },
                   "system":{
                      "id":70,
                      "name":"SYS_EMS-API"
                   },
                   "ztna":{
                      "id":1,
                      "name":"Default"
                   },
                   "videofilter":{
                      "id":1,
                      "name":"Default"
                   },
                   "ftdata_scan":{
                      "id":1,
                      "name":"Default"
                   }
                },
                "off_net_profile_components":{
                   "malware":{
                      "id":1,
                      "name":"Default"
                   },
                   "sandbox":{
                      "id":1,
                      "name":"Default"
                   },
                   "webfilter":{
                      "id":1,
                      "name":"Default",
                      "fp_name":"None"
                   },
                   "firewall":{
                      "id":1,
                      "name":"Default"
                   },
                   "vpn":{
                      "id":1,
                      "name":"Default"
                   },
                   "vulnerability_scan":{
                      "id":1,
                      "name":"Default"
                   },
                   "system":{
                      "id":70,
                      "name":"SYS_EMS-API"
                   },
                   "ztna":{
                      "id":1,
                      "name":"Default"
                   },
                   "videofilter":{
                      "id":1,
                      "name":"Default"
                   },
                   "ftdata_scan":{
                      "id":1,
                      "name":"Default"
                   }
                },
                "health_warning_count":1,
                "health_error_count":0,
                "fct_users":[
                   {
                      "auth_user_name":"None",
                      "machine_user_name":"labuser",
                      "auth_user_id":"None",
                      "machine_user_id":105,
                      "auth_domain":"None",
                      "machine_domain":"ad.labdomain.com",
                      "avatar":"None",
                      "fct_user_id":35,
                      "client_id":1,
                      "last_seen":"2026-06-29T16:20:38",
                      "is_authenticated":true,
                      "is_latest":true,
                      "display_name":"labuser",
                      "user_email":"labuser@ad.labdomain.com",
                      "user_phone":"None",
                      "row_no":1
                   },
                   {
                      "auth_user_name":"None",
                      "machine_user_name":"adkevin",
                      "auth_user_id":"None",
                      "machine_user_id":35,
                      "auth_domain":"None",
                      "machine_domain":"ad.labdomain.com",
                      "avatar":"None",
                      "fct_user_id":34,
                      "client_id":1,
                      "last_seen":"2026-06-28T12:17:49",
                      "is_authenticated":true,
                      "is_latest":false,
                      "display_name":"adkevin",
                      "user_email":"labuser@ad.labdomain.com",
                      "user_phone":"None",
                      "row_no":2
                   },
                   {
                      "auth_user_name":"None",
                      "machine_user_name":"adkevin",
                      "auth_user_id":"None",
                      "machine_user_id":2,
                      "auth_domain":"None",
                      "machine_domain":"ad.labdomain.com",
                      "avatar":"None",
                      "fct_user_id":1,
                      "client_id":1,
                      "last_seen":"2026-06-19T08:38:38",
                      "is_authenticated":false,
                      "is_latest":false,
                      "display_name":"adkevin",
                      "user_email":"labuser@ad.labdomain.com",
                      "user_phone":"",
                      "row_no":3
                   }
                ]
             },
             {
                "device_id":56,
                "host":"WIN-SERVER",
                "name":"WIN-SERVER",
                "ip_addr":"None",
                "os_version":"Microsoft Windows Server 2022 Datacenter Evaluation",
                "model":"",
                "vendor":"",
                "cpu":"",
                "memory":0,
                "sn":"",
                "hdd":"None",
                "public_ip_addr":"None",
                "domain_id":2,
                "installer_name":"None",
                "deployment_state":"None",
                "fdc_campaign_deployment_status":0,
                "fgt_sn":"None",
                "forticlient_id":"None",
                "uid":"None",
                "fct_version":"None",
                "diskenc":"None",
                "av_product":"None",
                "is_installed":false,
                "is_managed":false,
                "is_migrating":false,
                "is_ems_registered":"None",
                "can_quarantine":"None",
                "is_ems_online":false,
                "is_ems_onnet":"None",
                "is_excluded":false,
                "is_quarantined":0,
                "quarantine_access_code":"None",
                "invitation_name":"None",
                "invitation_code":"None",
                "invitation_version":"None",
                "comparable_fct_version":"None",
                "last_seen":"None",
                "last_seen_fct_user_id":"None",
                "endpoint_policy_name":"None",
                "endpoint_policy_id":"None",
                "ip_list_name":"None",
                "orig_groups":[
                   
                ],
                "av_enabled":"None",
                "rtp_enabled":"None",
                "ae_enabled":"None",
                "cs_enabled":"None",
                "rm_enabled":"None",
                "fw_enabled":"None",
                "wf_enabled":"None",
                "vf_enabled":"None",
                "vpn_enabled":"None",
                "vuln_enabled":"None",
                "ssoma_enabled":"None",
                "sb_enabled":"None",
                "sb_cloud_enabled":"None",
                "fd_enabled":"None",
                "rs_enabled":"None",
                "edr_installed":"None",
                "edr_app_enabled":"None",
                "edr_feature_enabled":"None",
                "onboarding_supported":"None",
                "client_version_up_to_date":true,
                "client_av_sig_version_up_to_date":true,
                "client_policy_synced":true,
                "client_policy_primary_synced":true,
                "client_policy_offnet_synced":true,
                "client_policy_iplist_synced":true,
                "client_policy_onnet_rule_synced":true,
                "client_policy_verification_rule_synced":true,
                "sys_events_count":0,
                "av_events_count":0,
                "wf_events_count":0,
                "vf_events_count":0,
                "fw_events_count":0,
                "sb_events_count":0,
                "fd_events_count":0,
                "ae_events_count":0,
                "rm_events_count":0,
                "cs_events_count":0,
                "unreg_events_count":0,
                "rs_events_count":0,
                "nwifsc_events_count":0,
                "owmsg_events_count":0,
                "reg_events_count":0,
                "ztna_sign_events_count":0,
                "ztna_revoke_events_count":0,
                "pam_events_count":0,
                "vcm_events_count":0,
                "vpn_events_count":0,
                "vuln_events_count":0,
                "pua_events_count":0,
                "vuln_events_max_severity":"None",
                "fdc_events_count":0,
                "forensics_enabled":false,
                "groups":[
                   {
                      "group_id":198,
                      "group_name":"SERVERS",
                      "group_path":"ad.labdomain.com/SERVERS"
                   }
                ],
                "profile_components":{
                   
                },
                "off_net_profile_components":{
                   
                },
                "health_warning_count":0,
                "health_error_count":0
             }
          ],
          "total":3
       }
    }

    Getting endpoint(s) of a named user

    If you want to get all endpoints where a user is recorded as the last seen user, you can use this script.

    GET endpoint of named user

    '''
    ems_get_named_user_endpoint.py
    Get endpoint for named user using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    user_name = "labuser"
    user_id = [] #This is a list, because a user can be associated with multiple endpoints
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    endpoints_url = f'{api_url_prefix}/endpoints/index'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Get endpoint data
    response = session.get(url=endpoints_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get user FortiClient ID with name match, then get endpoint ID with user ID match
    for endpoint in response_decoded['data']['endpoints']:
        #This try exists, because fct_users is not available on an endpoint with no last seen user
        try:
            for fct_user in endpoint['fct_users']:
                if fct_user['machine_user_name'] == user_name:
                    user_id.append(fct_user['fct_user_id'])
        except KeyError:
            continue
        if endpoint['last_seen_fct_user_id'] in user_id:
            print(f"{user_name} is a last seen user on endpoint {endpoint['name']}, which has ID {endpoint['device_id']}.")
        else:
            print(f"{user_name} does not appear in the last seen users of any endpoint.")
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Deregistering endpoint(s) by name or ID

    If you want to deregister an endpoint because this endpoint should no longer use a license, for example, the following script does just that.

    You can add known IDs to the deregister_list or match on the name of one or multiple endpoints using the endpoint_names_list

    Deregister endpoint(s) by name or ID

    '''
    ems_deregister_endpoints.py
    Deregister named endpoint(s) or IDs using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    #Add the names of endpoints to this list
    endpoint_names_list = ["WIN11-CLIENT", "FCXLAB"]
    #Add known endpoint IDs you want to deregister to this list
    deregister_list = []
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    endpoints_url = f'{api_url_prefix}/endpoints/index'
    deregister_url = f'{api_url_prefix}/clients/deregister'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    deregister_data = {}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    response = session.get(url=endpoints_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get endpoint ID with name match and add to deregister list, then format data for API call
    for endpoint in response_decoded['data']['endpoints']:
        if endpoint['name'] in endpoint_names_list:
            deregister_list.append(endpoint['device_id'])
            deregister_data = {"ids": deregister_list}
    
    #Deregister endpoint(s)
    session.post(url=deregister_url, json=deregister_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Deregister endpoint(s) by name or ID response

    {
       "result":{
          "retval":1,
          "message":"Successfully deregistered 2 endpoint(s)."
       }
    }

    Deregister endpoint(s) by user name

    If you want to deregister all endpoints where a user name is in the last seen users, use this script.

    Deregister endpoint(s) by user name

    '''
    ems_deregister_endpoint_named_user.py
    Deregister endpoint using a user name, which is also the last seen user, on the endpoint using the FortiClient EMS API
    '''
    
    import json
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    user_name = "adkevin"
    user_id = [] #This is a list, because a user can be associated with multiple endpoints
    deregister_list = []
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    endpoints_url = f'{api_url_prefix}/endpoints/index'
    deregister_url = f'{api_url_prefix}/clients/deregister'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    deregister_data = {}
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    response = session.get(url=endpoints_url, headers=api_headers, verify=False, timeout=30)
    response_decoded = json.loads(response.content.decode('utf-8'))
    
    #Get user FortiClient ID with name match, then get endpoint ID with user ID match
    for endpoint in response_decoded['data']['endpoints']:
        #If there is no recorded user on an endpoint the fct_users key does not exist
        try:
            for fct_user in endpoint['fct_users']:
                if fct_user['machine_user_name'] == user_name:
                    #All user IDs get added to a list, because the ID can be associated with multiple endpoints
                    user_id.append(fct_user['fct_user_id'])
            #Only if the user ID is the last seen user on the endpoint, the endpoint will be deregistered
            if endpoint['last_seen_fct_user_id'] in user_id:
                deregister_list.append(endpoint['device_id'])
                deregister_data = {"ids": deregister_list}
        except KeyError:
            continue
    
    #Deregister endpoints that have the user name as the last seen user, which can be multiple
    response = session.post(url=deregister_url, json=deregister_data, headers=change_headers, verify=False, timeout=30)
    response_decoded = response.content.decode('utf-8')
    print(response_decoded)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Deregister endpoint(s) by user name response

    {
       "result":{
          "retval":1,
          "message":"Successfully deregistered 2 endpoint(s)."
       }
    }

    What about EMS Settings?

    After all this, one topic that is conspicuous by its absence is the EMS settings, i.e. the listen address, keepalives, license timeouts, etc.

    You can actually set these things using the API with the /api/v1/settings/server/set using a PATCH, but it doesn’t do anything, despite the response stating “Settings updated successfully”.

    Set EMS Settings

    '''
    ems_set_server_settings.py
    Set server data using the FortiClient EMS API
    '''
    
    import requests
    
    #Disable warnings
    requests.urllib3.disable_warnings()
    
    #Set credentials
    username = 'apiadmin'
    password = 'Start123$'
    
    #Set some variables for the API
    ems_server = '192.168.1.208'
    
    #Set all used URLs
    api_url_prefix = f'https://{ems_server}/api/v1'
    login_url = f'{api_url_prefix}/auth/signin'
    logout_url = f'{api_url_prefix}/auth/signout'
    server_settings_url = f'{api_url_prefix}/settings/server/set'
    
    #Variables for data and headers
    auth_data = {"name": f"{username}", "password": f"{password}"}
    api_headers = {"Content-type": "application/json"}
    
    server_data = {
        "chromebooks": {
            "enabled": False,
            "inactivity_timeout": 24,
            "update_interval": 300,
            "is_licensed": True,
            "service_account": "account-1@forticlientwebfilter.iam.gserviceaccount.com",
            "global_enabled": False,
            "listen_port": 8443,
            "ssl_name": "FCTEMSSERIAL.1.cert",
            "ssl_date": "2056-05-26 20:48:33",
            "chromebook_cert_id": 35,
            "acme_auto_renew": False
        },
        "scheduledBackup": {
            "scheduled_backup_enabled": False,
            "scheduled_backup_type": 1,
            "scheduled_backup_interval": 1,
            "scheduled_backup_start_time": "20:00",
            "scheduled_backup_protocol": 2,
            "scheduled_backup_remote_server_ip": None,
            "scheduled_backup_selected_days": [
                "1",
                "4"
            ],
            "scheduled_backup_password": None,
            "scheduled_backup_compress_type": "database",
            "scheduled_backup_path": "/home/ems/exchange",
            "scheduled_backup_server_type": "local",
            "scheduled_backup_remote_user": None,
            "scheduled_backup_remote_user_password": None,
            "scheduled_backup_retention_period": 15
        },
        "reset_deployment_interval": 12,
        "sws_enabled": False,
        "sws_server": None,
        "sws_cert_name": None,
        "sws_cert_date": None,
        "inv_only_reg_enforcement_type": 0,
        "pwd_changed_check_enforced": False,
        "onboarding_enforced": False,
        "user_auth_period": None,
        "fct_repackager_upload_region": "Europe",
        "endpoints": {
            "key": None,
            "keep_alive_interval": 30,
            "offline_timeout": 15,
            "tag_timeout": 1440,
            "delete_timeout": 30,
            "license_timeout": 0,
            "duplicate_onboarded_user_timeout": 7,
            "unauthed_user_timeout": 30,
            "password_lockout_attempt": 3,
            "password_lockout_period": 60,
            "ztna_token_support": True,
            "ztna_token_timeout": 1440,
            "avatar_upload_enabled": False,
            "snapshot_interval": None
        },
        "unauthed_fct_count": 1,
        "unsupported_fct_count": 0,
        "telemetry": {
            "show_fortigate_server_list": False
        },
        "ztna_cert_date_created": "2026-06-19T08:11:54.217",
        "ztna_cert_expiry_date": "2051-06-13T08:11:54.217",
        "ztna_cert_name": "default_ZTNARootCA.pem",
        "is_custom_ztna_cert": False,
        "custom_hostname": "",
        "public_address": "ems.ad.labdomain.com",
        "public_port": 443,
        "https_enabled": True,
        "https_redirect_enabled": True,
        "ssl_from_forti_care": False,
        "custom_ec_cert": True,
        "enable_persistent_connection": True,
        "installer_port_enabled": True,
        "fos_notify_server_port": 8015,
        "webserver_cert_id": 36,
        "ec_cert_id": 36,
        "auto_upgrade_enabled": True,
        "hostname": "EMS",
        "is_ip_invalid": False,
        "listen_port": 8013,
        "fqdn_enabled": True,
        "fqdn": "ems.ad.labdomain.com",
        "installer_ip": "fcems-server",
        "show_fortigate_server_list": False,
        "password_lockout_attempt": 3,
        "password_lockout_period": 60,
        "ha_alert_interval": 60,
        "acme_auto_renew": False,
        "predefined_hostname": "*,192.168.1.208",
        "https_port": 443,
        "sites_enabled": False,
        "is_fgt_connected": False,
        "login_banner": {
            "enabled": False,
            "message": ""
        },
        "ips": [
            "192.168.1.208"
        ],
        "listen_ip": "0.0.0.0",
        "installer_port": 10443
    }
    
    #Setup session, login to EMS, and set new headers with CSRF token and referer
    session = requests.Session()
    login_response = session.post(url=login_url, json=auth_data, headers=api_headers, verify=False, timeout=30)
    change_headers = {"Content-type": "application/json", "Referer": f"https://{ems_server}", "X-CSRFToken": f"{session.cookies["csrftoken"]}"}
    
    #Set server settings
    session.patch(url=server_settings_url, data=server_data, headers=change_headers, verify=False, timeout=30)
    
    #Perform a logout
    session.post(url=logout_url, headers=change_headers, verify=False, timeout=30)
    

    Set EMS Settings response

    {
       "result":{
          "retval":1,
          "message":"Settings updated successfully."
       }
    }

    This might be related to the fact that, if you reverse engineer it, you see that this is form-based, like authentication servers.

    Browser tools payload for EMS server settings

    A list of undocumented API endpoints

    This is a non-exhaustive list of API endpoints I noticed are missing from the official documentation while creating all of this. There are more, but I didn’t look very hard, and I didn’t bother configuring some things to check for endpoints.

    This list is valid as of 2026-07-01.

    • /api/v1/on_net_rules/index
    • /api/v1/on_net_rules/create
    • /api/v1/on_net_rules/{ID}/rules/get
    • /api/v1/on_net_rules/{ID}/rules/update
    • /api/v1/on_net_rules/{ID}/rules/delete
    • /api/v1/assignable_installers/index
    • /api/v1/assignable_installers/create
    • /api/v1/assignable_installers/{ID}/get
    • /api/v1/assignable_installers/{ID}/update
    • /api/v1/assignable_installers/{ID}/delete
    • /api/v1/group_containers/domains/index
    • /api/v1/idps/index
    • /api/v1/idps/{GUID}/delete
    • /api/v1/idps/adfs/test
    • /api/v1/idps/{GUID}/get
    • /api/v1/idps/adfs/{GUID}/update
    • /api/v1/idps/adfs/{GUID}/imported_ous
    • /api/v1/connectors/index
    • /api/v1/server_certificates/index
    • /api/v1/settings/server/get
    • /api/v1/settings/server/set
    • /api/v1/system/cloud/repackager/status
    • /api/v1/settings/server/addresses/get
    • /api/v1/admins/create
    • /api/v1/admins/{ID}/get
    • /api/v1/admins/{ID}/update
    • /api/v1/admins/{ID}/delete
    • /api/v1/profiles/{COMPONENT_TYPE}/index
    • /api/v1/profiles/{COMPONENT_TYPE}/{ID}/get
    • /api/v1/profiles/{COMPONENT_TYPE}/{ID}/delete
    • /api/v1/profiles/ztna/saas_applications
    • /api/v1/roles/index
    • /api/v1/client_certificates/group_index
    • /api/v1/client_certificates/index
    • /api/v1/client_certificates/set
    • /api/v1/oauth2_fabric_connectors/index
    • /api/v1/fabric_device_auth/{FGT_SERIAL}/update
    • /api/v1/client_certificates/delete
    • /api/v1/ztna_apps/index
    • /api/v1/troubleshoot/get
    • /api/v1/idps/{GUID}/live_navigate
    • /api/v1/system/info
    • /api/v1/endpoints/connection/donut
    • /api/v1/endpoints/management/donut
    • /api/v1/license/get
    • /api/v1/forti_care/get
    • /api/v1/logs/index
    • /api/v1/logs/count
    • /api/v1/users/local/index
    • /api/v1/users/local/create
    • /api/v1/users/local/{ID}/update
    • /api/v1/users/local/{ID}/delete
    • /api/v1/users/{ID}/endpoints/count

    Wrapping up

    A lot was covered today, but I still feel it is a light post, since most of the work was scripting and troubleshooting. I know this post comes across as negative, and I don’t feel positive about the API, but it’s not my intention to make anyone feel bad. If the documentation and the API get better because of this, I see that as a great success. If someone feels helped by this post, then I consider that an even greater success.

    Dear reader, do you feel helped?

    A shoutout to Maximilian Schiffner from Fortinet for this post. He is one of the greatest Fortinet engineers I know, and I don’t say that just because he’s Austrian. I got the idea for this post because of a single sentence in an email he wrote.

    And since this is the end and EMS was the topic, why not read about how you can use the certificate management capabilities of FortiClient EMS for VPN, full SSL/TLS inspection and 802.1X or how to connect to an HA EMS cluster from a FortiGate without an external load balancer? The EMS journey doesn’t stop!

  • 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.

    • 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.

    • Going towards the deep end of a Fortinet ZTNA connection (TCP forwarding)

      What is ZTNA even?

      Zero Trust Network Access (ZTNA) is a fun term that, in 2025, is barely a buzzword and is ready to join the modern, established terms like SD-WAN, 2FA, and IoT.

      There isn’t a real consensus in the networking and security industry on what defines ZTNA, but if you ask most vendors, you will probably get at least three points, in different words:

      • The client is initially not trusted, so he has no default access to anything. This is the Zero Trust part.
      • Connections are made on an application basis, meaning that a client requests access to a specific resource, not an entire subnet.
      • The client is checked before and during the connection to see if he is still allowed to access the resource. This is client posturing.

      I have a problem with the “Zero” part of ZTNA, because if there is no trust, there can be no connection. You need to trust someone to gain access to something. I once talked with Jaye Tillson, and he prefers to call it “Adaptive Trust Network Access”, which is a much better term, and it actually describes how it is being employed in the field.

      With that preamble out of the way, let’s see how Fortinet does ZTNA connections for TCP forwarding.

      I want to stress that this is not a configuration guide, and I assume that you, dear reader, know the basics of how Fortinet does ZTNA. Please refer to the documentation to see how to set all of this up yourself. I will focus on the actual traffic flow and the underlying process.

      The setup

      ZTNA lab setup

      I have:

      • A client outside that will be called FortiClient or client, which wants to access a resource.
      • A FortiGate that acts as the ZTNA access proxy, called ztnaproxy.ad.labdomain.com.
      • A FortiClient EMS server called ems.ad.labdomain.com, which is accessed via a regular DNAT/VIP.
      • A server on the LAN called win-server.ad.labdomain.com, which will get accessed via a ZTNA connection, as well as a server called win-ad.ad.labdomain.com, which is just there for a demonstration.

      As far as Fortinet versions are concerned, I’m using the, at the time of writing, latest and greatest versions. FortiOS 7.6.4, FortiClient 7.4.4, and FortiClient EMS 7.4.4.

      Let’s start, and we start where it all begins, the client.

      How FortiClient does ZTNA

      A FortiClient receives its ZTNA configuration from EMS, and the first decision that is made is what you’re trying to connect to, even a ZTNA destination? The way this happens is straightforward. FortiClient will check whether whatever the client is trying to access is in its ZTNA destination list, and if there is a match, it goes ahead with everything else.

      ZTNA destinations

      What we see here is ZTNA destinations with both FQDNs and an IP, which leads to the next question: Who does the DNS resolution for the FQDNs?

      The answer is FortiClient itself. 

      In the past, the hosts file was used, which was ugly, but with new versions of FortiClient, the FortiTCS (Tunnel Control Service) process takes care of this. It takes the ZTNA destination configuration from EMS, records the configured settings, assigns IP addresses for DNS resolution, and runs its own DNS proxy. It handles a lot more, but we’ll get to that.

      If you want to see FortiTCS at work, check out the log file at “C:\Program Files\Fortinet\FortiClient\logs\trace\fortitcs_X.log” (it’s a rolling log, so make sure you look at the most recent file). I will display logs from there extensively, starting with a ZTNA configuration update:

      FortiClient ZTNA configuration update

      [2025-11-30 17:36:24.4821744] [fortitcs] Configuration Changed!
      [2025-11-30 17:36:24.4824156] [fortitcs] PortalConfigChanged called due to config change!
      [2025-11-30 17:36:24.4830978] [fortitcs debug] DetectPAMRuleChanges::No changes detected: Old and new settings are identical
      [2025-11-30 17:36:24.4857283] [fortitcs] Configuration changed: bPortalChanged=false bConfigChanged=true force_reload_config=false
      [2025-11-30 17:36:24.4862689] [fortitcs] tcp forwarding enabled = 1
      [2025-11-30 17:36:24.4863362] [fortitcs] prefer_ipv4_over_ipv6: 0
      [2025-11-30 17:36:24.4864507] [fortitcs] enabled(set): false
      [2025-11-30 17:36:24.4864748] [fortitcs] newComponents32(set): 0
      [2025-11-30 17:36:24.4865335] [fortitcs] DisabledComponents(no set): 0
      [2025-11-30 17:36:24.4881639] [fortitcs] mask: 255.255.255.255 start: 192.168.1.241 end: 192.168.1.241
      [2025-11-30 17:36:24.4882016] [fortitcs] GetIpV4NetAddressRange-IpStart: 192.168.1.241
      [2025-11-30 17:36:24.4882213] [fortitcs] mask_changed: 255.255.255.255 start: 192.168.1.241 end: 192.168.1.241
      [2025-11-30 17:36:24.4883900] [fortitcs] orderRules:: No PAM Rule Change Identified, performing GW rules reload
      [2025-11-30 17:36:24.4884090] [fortitcs] ***
      [2025-11-30 17:36:24.4884208] [fortitcs] name: win-ad.ad.labdomain.com:3389
      [2025-11-30 17:36:24.4884311] [fortitcs] type: tcp-fwd
      [2025-11-30 17:36:24.4884412] [fortitcs] mode: transparent
      [2025-11-30 17:36:24.4884508] [fortitcs] destination: win-ad.ad.labdomain.com:3389
      [2025-11-30 17:36:24.4884600] [fortitcs] gateway: ztnaproxy.ad.labdomain.com:443
      [2025-11-30 17:36:24.4884705] [fortitcs] enable_udp: 0
      [2025-11-30 17:36:24.4884840] [fortitcs] latency: 8760h0m0s
      [2025-11-30 17:36:24.4884948] [fortitcs] encryption: 0
      [2025-11-30 17:36:24.4885051] [fortitcs] FQDN: win-ad.ad.labdomain.com
      [2025-11-30 17:36:24.4885156] [fortitcs] FQDN_flag: 0
      [2025-11-30 17:36:24.4885252] [fortitcs] IPStart: 10.235.0.1
      [2025-11-30 17:36:24.4885351] [fortitcs] IPEnd: 10.235.0.1
      [2025-11-30 17:36:24.4885452] [fortitcs] SubnetMask: 255.255.255.255
      [2025-11-30 17:36:24.4885551] [fortitcs] PortStart: 3389
      [2025-11-30 17:36:24.4885653] [fortitcs] PortEnd: 3389
      [2025-11-30 17:36:24.4885755] [fortitcs] Path:
      [2025-11-30 17:36:24.4885859] [fortitcs] ***
      [2025-11-30 17:36:24.4885970] [fortitcs] name: 192.168.1.241/255.255.255.255:3389
      [2025-11-30 17:36:24.4886958] [fortitcs] type: tcp-fwd
      [2025-11-30 17:36:24.4887140] [fortitcs] mode: transparent
      [2025-11-30 17:36:24.4887234] [fortitcs] destination: 192.168.1.241:3389
      [2025-11-30 17:36:24.4887332] [fortitcs] gateway: ztnaproxy.ad.labdomain.com:443
      [2025-11-30 17:36:24.4887983] [fortitcs] enable_udp: 0
      [2025-11-30 17:36:24.4888182] [fortitcs] latency: 8760h0m0s
      [2025-11-30 17:36:24.4888305] [fortitcs] encryption: 0
      [2025-11-30 17:36:24.4888411] [fortitcs] FQDN:
      [2025-11-30 17:36:24.4888511] [fortitcs] FQDN_flag: 0
      [2025-11-30 17:36:24.4888613] [fortitcs] IPStart: 192.168.1.241
      [2025-11-30 17:36:24.4888717] [fortitcs] IPEnd: 192.168.1.241
      [2025-11-30 17:36:24.4888824] [fortitcs] SubnetMask: 255.255.255.255
      [2025-11-30 17:36:24.4888926] [fortitcs] PortStart: 3389
      [2025-11-30 17:36:24.4889025] [fortitcs] PortEnd: 3389
      [2025-11-30 17:36:24.4889123] [fortitcs] Path:
      [2025-11-30 17:36:24.4889224] [fortitcs] ***
      [2025-11-30 17:36:24.4889318] [fortitcs] name: win-server.ad.labdomain.com:69
      [2025-11-30 17:36:24.4889417] [fortitcs] type: tcp-fwd
      [2025-11-30 17:36:24.4889516] [fortitcs] mode: transparent
      [2025-11-30 17:36:24.4889612] [fortitcs] destination: win-server.ad.labdomain.com:69
      [2025-11-30 17:36:24.4889705] [fortitcs] gateway: ztnaproxy.ad.labdomain.com:443
      [2025-11-30 17:36:24.4889805] [fortitcs] enable_udp: 1
      [2025-11-30 17:36:24.4889921] [fortitcs] latency: 8760h0m0s
      [2025-11-30 17:36:24.4890023] [fortitcs] encryption: 0
      [2025-11-30 17:36:24.4890114] [fortitcs] FQDN: win-server.ad.labdomain.com
      [2025-11-30 17:36:24.4890208] [fortitcs] FQDN_flag: 0
      [2025-11-30 17:36:24.4890300] [fortitcs] IPStart: 10.235.0.2
      [2025-11-30 17:36:24.4890450] [fortitcs] IPEnd: 10.235.0.2
      [2025-11-30 17:36:24.4890552] [fortitcs] SubnetMask: 255.255.255.255
      [2025-11-30 17:36:24.4890656] [fortitcs] PortStart: 69
      [2025-11-30 17:36:24.4890755] [fortitcs] PortEnd: 69
      [2025-11-30 17:36:24.4890855] [fortitcs] Path:
      [2025-11-30 17:36:24.4890956] [fortitcs] ***
      [2025-11-30 17:36:24.4891053] [fortitcs] name: win-server.ad.labdomain.com:3389
      [2025-11-30 17:36:24.4891147] [fortitcs] type: tcp-fwd
      [2025-11-30 17:36:24.4891242] [fortitcs] mode: transparent
      [2025-11-30 17:36:24.4891342] [fortitcs] destination: win-server.ad.labdomain.com:3389
      [2025-11-30 17:36:24.4891475] [fortitcs] gateway: ztnaproxy.ad.labdomain.com:443
      [2025-11-30 17:36:24.4891583] [fortitcs] enable_udp: 0
      [2025-11-30 17:36:24.4891690] [fortitcs] latency: 8760h0m0s
      [2025-11-30 17:36:24.4891791] [fortitcs] encryption: 0
      [2025-11-30 17:36:24.4891889] [fortitcs] FQDN: win-server.ad.labdomain.com
      [2025-11-30 17:36:24.4891987] [fortitcs] FQDN_flag: 0
      [2025-11-30 17:36:24.4892080] [fortitcs] IPStart: 10.235.0.2
      [2025-11-30 17:36:24.4892170] [fortitcs] IPEnd: 10.235.0.2
      [2025-11-30 17:36:24.4892262] [fortitcs] SubnetMask: 255.255.255.255
      [2025-11-30 17:36:24.4892355] [fortitcs] PortStart: 3389
      [2025-11-30 17:36:24.4892450] [fortitcs] PortEnd: 3389
      [2025-11-30 17:36:24.4892556] [fortitcs] Path:
      [2025-11-30 17:36:24.4892697] [fortitcs] ***
      [2025-11-30 17:36:24.4892891] [fortitcs] Found system dns clear tool: C:\WINDOWS\system32\ipconfig.exe
      [2025-11-30 17:36:24.4893146] [fortitcs] C:\WINDOWS\system32\ipconfig.exe /flushdns
      [2025-11-30 17:36:24.5234647] [fortitcs] [13 10 87 105 110 100 111 119 115 32 73 80 32 67 111 110 102 105 103 117 114 97 116 105 111 110 13 10 13 10 83 117 99 99 101 115 115 102 117 108 108 121 32 102 108 117 115 104 101 100 32 116 104 101 32 68 78 83 32 82 101 115 111 108 118 101 114 32 67 97 99 104 101 46 13 10]
      [2025-11-30 17:36:24.5414693] [fortitcs] UpdateDnsRedirectEntry: Ip=183173121 Port=3389 RealIp=0 Fqdn=win-ad.ad.labdomain.com Flag=0
      [2025-11-30 17:36:24.5415118] [fortitcs] UpdateDnsRedirectEntry: Ip=183173122 Port=69 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-11-30 17:36:24.5415282] [fortitcs] UpdateDnsRedirectEntry: Ip=183173122 Port=3389 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-11-30 17:36:24.5416140] [fortitcs] Update DNS redirect Entry!
      [2025-11-30 17:36:24.5416268] [fortitcs] Initialize UDP-DNS Success!
      [2025-11-30 17:36:24.5416337] [fortitcs] Initialize UDP-Proxy Success!
      [2025-11-30 17:36:24.5416437] [fortitcs] TCP Forwarding: IpStart: 10.235.0.1 IpEnd: 10.235.0.1 PortStart: 3389 PortEnd: 3389
      [2025-11-30 17:36:24.5416519] [fortitcs] TCP Forwarding: IpStart: 192.168.1.241 IpEnd: 192.168.1.241 PortStart: 3389 PortEnd: 3389
      [2025-11-30 17:36:24.5416589] [fortitcs] TCP Forwarding: IpStart: 10.235.0.2 IpEnd: 10.235.0.2 PortStart: 69 PortEnd: 69
      [2025-11-30 17:36:24.5416669] [fortitcs] TCP Forwarding: IpStart: 10.235.0.2 IpEnd: 10.235.0.2 PortStart: 3389 PortEnd: 3389
      [2025-11-30 17:36:24.5416741] [fortitcs] DOH Forwarding: IpStart: 16777217 IpEnd: 16777217 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5416807] [fortitcs] DOH Forwarding: IpStart: 16843009 IpEnd: 16843009 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5416874] [fortitcs] DOH Forwarding: IpStart: 134743044 IpEnd: 134743044 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5416937] [fortitcs] DOH Forwarding: IpStart: 134744072 IpEnd: 134744072 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5416998] [fortitcs] DOH Forwarding: IpStart: 151587081 IpEnd: 151587081 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417086] [fortitcs] DOH Forwarding: IpStart: 3741648133 IpEnd: 3741648133 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417153] [fortitcs] DOH Forwarding: IpStart: 1746021035 IpEnd: 1746021035 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417225] [fortitcs] DOH Forwarding: IpStart: 2889912661 IpEnd: 2889912661 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417290] [fortitcs] DOH Forwarding: IpStart: 1745942777 IpEnd: 1745942777 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417357] [fortitcs] DOH Forwarding: IpStart: 1745943033 IpEnd: 1745943033 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417419] [fortitcs] DOH Forwarding: IpStart: 2456824066 IpEnd: 2456824066 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417482] [fortitcs] DOH Forwarding: IpStart: 3118770344 IpEnd: 3118770344 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417541] [fortitcs] DOH Forwarding: IpStart: 3118770186 IpEnd: 3118770186 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417603] [fortitcs] DOH Forwarding: IpStart: 3165443477 IpEnd: 3165443477 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417665] [fortitcs] DOH Forwarding: IpStart: 1747227829 IpEnd: 1747227829 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417731] [fortitcs] DOH Forwarding: IpStart: 2507176202 IpEnd: 2507176202 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417795] [fortitcs] DOH Forwarding: IpStart: 2507176458 IpEnd: 2507176458 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417860] [fortitcs] DOH Forwarding: IpStart: 2507176212 IpEnd: 2507176212 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417937] [fortitcs] DOH Forwarding: IpStart: 2507176468 IpEnd: 2507176468 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5417999] [fortitcs] DOH Forwarding: IpStart: 2507176222 IpEnd: 2507176222 PortStart: 443 PortEnd: 443
      [2025-11-30 17:36:24.5418063] [fortitcs] DOH Forwarding: IpStart: 2507176478 IpEnd: 2507176478 PortStart: 443 PortEnd: 443

      This log was taken right after an update, signalized by the “Configuration Changed!” message right at the top.

      We can see each ZTNA destination being listed, and for the destinations that are FQDNs, we have the “IPStart” and “IPEnd” fields being filled with what are effectively bogus IP addresses from the 10.235.0.0/16 subnet (I assume it’s /16, but I’m not going to test this). This subnet is, to my knowledge, hardcoded, and you might ask, “What happens if there is an overlap in my network with this?” Well, you’ll have problems when doing FQDN ZTNA destinations, so pray that you don’t have this subnet in use.

      And if you’re curious, entries like “Ip=183173121” and “IpStart: 16843009” are IPs converted to decimal. If you math this out, you get 10.235.0.1 and 1.1.1.1 respectively. The “DOH Forwarding” entries all seem to be DNS over HTTPS (DoH) servers, but I don’t know why they are in there.

      Circling back, if the client tries to access an FQDN ZTNA destination, FortiClient intercepts the DNS request and returns a bogus IP. This happens regardless of whether the connection is relevant to ZTNA or not. Even a ping, and ICMP isn’t supported by Fortinet’s ZTNA implementation, gives you the 10.235.0.0/16 IP.

      Ping FQDN that is a ZTNA destination

      If the ZTNA destination is an IP, nothing special happens; it simply gets forwarded, and this is where FortiTCS comes in again.

      FortiTCS is responsible for establishing a connection to the FortiGate, which acts as the ZTNA access proxy, aka ZTNA server, aka ZTNA Forwarding Access Proxy (TFAP), aka ZTNA gateway (I think that are all the names you see in the documentation), and processing and forwarding the relevant traffic via a secure connection.

      Looking at fortitcs_X.log again, we see the following log entry if an FQDN ZTNA destination is accessed:

      FortiClient FQDN ZTNA access log

      [2025-11-30 15:12:02.0717327] [fortitcs] Accept success
      [2025-11-30 15:12:02.0718489] [fortitcs] family: 2 port: 3389 ip: 10.235.0.1 pid: 6904
      [2025-11-30 15:12:02.0718881] [fortitcs] forward ip: 10.235.0.1 port: 3389
      [2025-11-30 15:12:02.0720149] [fortitcs] FindFQDNFromDoh: IP=10.235.0.1 Port=3389 FQDN=win-server.ad.labdomain.com
      [2025-11-30 15:12:02.0720306] [fortitcs] UpdateDnsRedirectEntry: Ip=183173121 Port=3389 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-11-30 15:12:02.0720584] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 10.235.0.1fqdn: win-server.ad.labdomain.com
      [2025-11-30 15:12:02.0721258] [fortitcs] Request is not for PAM Agent. Ip: 10.235.0.1, Port: 3389, FQDN: win-server.ad.labdomain.com
      [2025-11-30 15:12:02.0721942] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-11-30 15:12:02.0782044] [fortitcs] gateway ip: ztnaproxy.ad.labdomain.com port: 443encryption: 0 cur_thread: 4568
      [2025-11-30 15:12:02.1321076] [fortitcs] handshake success!
      [2025-11-30 15:12:02.1321540] [fortitcs] SAML address: https://ztnaproxy.ad.labdomain.com:443/tcp?address=win-server.ad.labdomain.com&port=3389&tls=0
      [2025-11-30 15:12:02.1321638] [fortitcs] Cache address: https://ztnaproxy.ad.labdomain.com:443
      [2025-11-30 15:12:02.1323757] [fortitcs] Request: GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1
      Host: ztnaproxy.ad.labdomain.com
      Accept: /
      Authorization:
      Cookie:
      User-Agent: Forticlient
      X-Fos-Auth:
      
      [2025-11-30 15:12:02.1324585] [fortitcs] Send tunnel cmd Success
      [2025-11-30 15:12:02.1343570] [fortitcs] response code: 101 html Reply:
      [2025-11-30 15:12:02.1344669] [fortitcs] Tunnel upgrade success
      [2025-11-30 15:12:02.6492340] [fortitcs] Forward win-server.ad.labdomain.com:: local: 127.0.0.1:49689 -> remote: 127.0.0.1:50242proxy: 172.31.1.2:50243 -> remote: 172.31.1.1:443

      For the sake of brevity, I have removed some lines that are unimportant, and I will do this with all logs and debugs in this article. You’re welcome.

      As we can see in the log, it’s a connection for the IP 10.235.0.1, on port 3389, and the FQDN is win-server.ad.labdomain.com. We also see the decimal IP 183173121 here again. The gateway IP, or actually FQDN, is ztnaproxy.ad.labdomain.com, but the actually interesting thing is the following line:

      Request: GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1


      FortiClient’s ZTNA connection is nothing more than an HTTP GET request towards the FortiGate, and this happens over an encrypted tunnel.

      The URL being accessed says what resource we’re trying to access over what port, and the tls parameter determines if this is getting extra encryption, but more on that later. Some headers are being sent with that, including the Host header, which is not important for this type of ZTNA connection, because we’re doing TCP forwarding.

      Note how the URL uses the FQDN as the address parameter. The FortiGate does not get the IP in this case, and considering it would be a bogus IP, that is for the better. The FortiGate needs to be able to resolve this FQDN itself so it can establish a connection with the correct real server.

      The same type of connection with an IP instead of an FQDN is much the same, except that there is no DNS stuff going on.

      FortiClient ZTNA IP access log

      [2025-11-30 15:31:46.2775951] [fortitcs] Accept success
      [2025-11-30 15:31:46.2777679] [fortitcs] family: 2 port: 3389 ip: 192.168.1.241 pid: 6904
      [2025-11-30 15:31:46.2778108] [fortitcs] forward ip: 192.168.1.241 port: 3389
      [2025-11-30 15:31:46.2778936] [fortitcs] Request is not for PAM Agent. Ip: 192.168.1.241, Port: 3389, FQDN: 
      [2025-11-30 15:31:46.2779464] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-11-30 15:31:46.2918114] [fortitcs] gateway ip: ztnaproxy.ad.labdomain.com port: 443encryption: 0 cur_thread: 9132
      [2025-11-30 15:31:46.3503269] [fortitcs] handshake success!
      [2025-11-30 15:31:46.3503960] [fortitcs] SAML address: https://ztnaproxy.ad.labdomain.com:443/tcp?address=192.168.1.241&port=3389&tls=0
      [2025-11-30 15:31:46.3506344] [fortitcs] Cache address: https://ztnaproxy.ad.labdomain.com:443
      [2025-11-30 15:31:46.3509011] [fortitcs] Request: GET /tcp?address=192.168.1.241&port=3389&tls=0 HTTP/1.1
      Host: ztnaproxy.ad.labdomain.com
      Accept: */*
      Authorization: ******
      Cookie: ******
      User-Agent: Forticlient
      X-Fos-Auth: 
      
      
      [2025-11-30 15:31:46.3510320] [fortitcs] Send tunnel cmd Success
      [2025-11-30 15:31:46.3523777] [fortitcs] response code: 101 html Reply: 
      [2025-11-30 15:31:46.3524196] [fortitcs] Tunnel upgrade success
      [2025-11-30 15:31:46.8645496] [fortitcs] Forward :: local: 127.0.0.1:49689 -> remote: 127.0.0.1:50646proxy: 172.31.1.2:50647 -> remote: 172.31.1.1:443

      For the time being, that’s everything on the FortiClient’s side, so let’s check what the FortiGate does with all the information FortiTCS provides.

      How FortiGate does ZTNA

      In Fortinet’s ZTNA construct, a FortiGate acts as a proxy server, meaning it receives connections from FortiClient and connects to the real server on behalf of the client. How does it do that?

      FortiOS handles the ZTNA feature in the Web Application Daemon (WAD) process, so if we want to know what happens, we have to look at the debugs of WAD. To see the relevant WAD debug categories for ZTNA and enable debugging, you can use the following commands:

      diagnose wad debug enable category session
      diagnose wad debug enable category policy
      diagnose wad debug enable category http
      diagnose wad debug enable category vs
      diagnose wad debug enable category ssl
      diagnose wad debug enable category quic
      diagnose debug enable

      If you don’t want to miss anything, you can also turn on everything WAD has to offer:

      diagnose wad debug enable category all
      diagnose debug enable

      Let’s see what a ZTNA FQDN connection looks like in the debugs:

      FortiGate ZTNA FQDN debug

      [I][p:3335][s:41045] wad_vs_ssl_port_caps_c2p_on_client_hello:11217 675:ZTNA-LAB: wsp(0x7fa1a4b440) handshake recv ClientHello record 3.1 client 3.3 supported 3.3
      [V][p:3335][s:41045] wad_vs_proxy_match_vhost :4662 675:ZTNA-LAB: matching vhost by: ztnaproxy.ad.labdomain.com
      [V][p:3335][s:41045] wad_vs_matcher_map_find :798 Empty matcher!
      [V][p:3335][s:41045] wad_vs_proxy_match_vhost :4665 675:ZTNA-LAB: no host matched.
      [I][p:3335][s:41045] wad_vs_ssl_port_caps_cert_req :14077 675:ZTNA-LAB: requested certs from client. ca_store is ready.
      [I][p:3335][s:41045] wad_vs_ssl_c2p_cert_req_add_trust_issuers:14038 675:ZTNA-LAB: adding subject: C = CA, ST = California, L = Sunnyvale, O = Fortinet, OU = 00000000000000000000000000000000, CN = FCTEMS8824011503 to CCR.
      [I][p:3335][s:41045] wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 675:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.
      [I][p:3335][s:41045] wad_vs_ssl_access_proxy_on_clt_certs:13476 675:ZTNA-LAB: received certs from the client.
      [I][p:3335][s:41045] __wad_ssl_cert_open_cert :641 https server uses key_len 2048
      [I][p:3335][s:41045] wad_ssl_cert_check_auth_status_with_ca_store:464 authi(0x7fa1ec2570) status(0)
      [I][p:3335][s:41045] wad_ui_ssl_cert_verify :1562 Certificate verified!
      [I][p:3335][s:41045] wad_ssl_cert_parse_cert :1448 EMS SN=FCTEMS8824011503
      [I][p:3335][s:41045] wad_ssl_cert_parse_cert :1453 Tenant ID=00000000000000000000000000000000
      [I][p:3335][s:41045] wad_ssl_cert_get_sn :1364 ci(0x7fa1b10eb8): sn=3AA3BF4801BF5771BF3079CCDFB7F66DF0841E6F
      [I][p:3335][s:41045] __wad_ssl_validate_ztna_cert_by_ca_store:3465 Successfully verified cert by CA.
      [I][p:3335][s:41045] wad_vs_ssl_access_proxy_on_clt_certs:13530 675:ZTNA-LAB: Cert auth success. issued_by: ems
      [I][p:3335][s:41045] wad_vs_ssl_port_caps_c2p_on_handshake_done:12075 wsp 0x7fa1a4b440 type 10 handshake done
      [I][p:3335][s:41045] wad_vs_http_make :5500 make vs http session
      [V][p:3335][s:41045][r:33554433] wad_http_req_alloc :1911 req=0x7fa2aa1048(ses_ctx:ct|Pv|M|H|C|A1|O) dst(orig=172.31.1.1:443 dst=172.31.1.1:443 srv=172.31.1.1:443) hs=0x7fa33e9228 clt_port=0x7fa1a4b6a0 svr_port=(nil)
      [I][p:3335][s:41045][r:33554433] wad_dump_http_request :3066 hreq=0x7fa2aa1048 Received request from client: 172.31.1.2:51280
      
      GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1
      Host: ztnaproxy.ad.labdomain.com
      User-Agent: Forticlient
      Accept: /
      Connection: Upgrade
      Cookie:
      Upgrade: tcp-forwarding/1.0
      X-Fos-Auth:
      
      [V][p:3335][s:41045][r:33554433] wad_http_marker_uri :1526 path=/tcp len=4
      [V][p:3335][s:41045][r:33554433] wad_http_parse_host :1904 host_len=26
      [I][p:3335][s:41045][r:33554433] wad_http_parse_host :1941 host=[26]ztnaproxy.ad.labdomain.com
      [I][p:3335][s:41045][r:33554433] wad_http_str_canonicalize :2466 enc=0 path=/tcp len=4 changes=0
      [I][p:3335][s:41045][r:33554433] wad_http_str_canonicalize :2468 end=4 path=address=win-server.ad.labdomain.com&port=3389&tls=0 len=51 changes=0
      [V][p:3335][s:41045][r:33554433] wad_http_normalize_uri :2705 host_len=26 path_len=4 query_len=51
      [I][p:3335][s:41045][r:33554433] wad_http_req_detect_special :14378 captive_portal detected: false, preflight=(null)
      [I][p:3335][s:41045][r:33554433] wad_vs_proxy_match_gwy :4543 675:ZTNA-LAB: matching gwy with vhost(def_virtual_host)
      [V][p:3335][s:41045][r:33554433] wad_vs_proxy_match_vhost :4662 675:ZTNA-LAB: matching vhost by: ztnaproxy.ad.labdomain.com
      [V][p:3335][s:41045][r:33554433] wad_vs_matcher_map_find :798 Empty matcher!
      [V][p:3335][s:41045][r:33554433] wad_vs_proxy_match_vhost :4665 675:ZTNA-LAB: no host matched.
      [I][p:3335][s:41045][r:33554433] wad_vs_proxy_match_gwy :4611 675:ZTNA-LAB: matching gwy by (/tcp) with vhost(def_virtual_host).
      [I][p:3335][s:41045][r:33554433] wad_vs_proxy_match_gwy :4630 675:ZTNA-LAB: Matched gwy(1) type(tcp-fwd).
      [I][p:3335][s:41045][r:33554433] wad_http_srv_selector_static_make :1942 make static server selector.
      [I][p:3335][s:41045][r:33554433] wad_vs_gwy_tcp_dst_ovrd :3382 675:ZTNA-LAB:1: req(0x7fa2aa1048) query(address=win-server.ad.labdomain.com&port=3389&tls=0)
      [I][p:3335][s:41045][r:33554433] wad_vs_gwy_tcp_get_parameters :3136 675:ZTNA-LAB:1: got fqdn=win-server.ad.labdomain.com.
      [V][p:3335][s:41045][r:33554433] wad_vs_matcher_map_find :798 Empty matcher!
      [W][p:3335][s:41045][r:33554433] wad_vs_proxy_dns_resolve :3290 req(0x7fa2aa1048) vs DNS request name=win-server.ad.labdomain.com len=27 type/pref=0/0
      [V][p:3335][s:41045][r:33554433] wad_http_mstrm_ops_read_block :1428 strm paused, flag=0x0 is_clt=1
      [V][p:3335][s:41045][r:33554433] wad_http_clt_read_sync :2955 hs=0x7fa33e9228 pause=(1/0x0) ret=1 execute=wad_http_clt_read_req_line
      [I][p:3335][s:41045][r:33554433] wad_vs_proxy_dns_request_done :3237 req(0x7fa2aa1048) vs DNS resolved: 192.168.1.241
      [V][p:3335][s:41045][r:33554433] wad_http_mstrm_ops_read_block :1436 strm resumed, execute=wad_http_clt_read_req_line is_clt=1
      [I][p:3335][s:41045][r:33554433] wad_http_req_exec_on_vs_dns_ready :12441 req(0x7fa2aa1048) vs DNS ready: dns_resolved(1), domain_matched(0), addr_matched(1)
      [V][p:3335][s:41045][r:33554433] wad_http_req_get_dst_intf :12196 vd=0 dst=192.168.1.241 ifidx=5
      [V][p:3335][s:41045][r:33554433] wad_http_req_vs_check_policy :12369 HTTP req=0x7fa2aa1048 out_intf=5, vwl=0
      [V][p:3335][s:41045][r:33554433] wad_http_req_check_policy_with_flags:12021 start match policy vd=0(ses_ctx:ct|Pv|M|H|C|A1|O) (172.31.1.2:51280@8->192.168.1.241:3389@5) absUrl=0
      [V][p:3335][s:41045][r:33554433] wad_fw_addr_match_ap :1292 matching ap:ZTNA-LAB(8) with vip addr:ZTNA-LAB(8)
      [I][p:3335][s:41045][r:33554433] wad_http_policy_match_one :501 fw_pol_id=123(pol_ctx:th|Ad|7|=p) pflag:H|W|U|A asyn_info=1
      [I][p:3335][s:41045][r:33554433] wad_fw_policy_async_match :5929 pol_ctx:th|Ad|7|=d
      [I][p:3335][s:41045][r:33554433] wad_http_req_policy_set :10156 match policy-id=123(pol_ctx:th|Ad|7|=d) vd=0(ses_ctx:ct|Pv|Me|H|C|A1|O) (172.31.1.2:51280@8 -> 192.168.1.241:3389@5)
      [V][p:3335][s:41045][r:33554433] wad_https_ap_pol_info_get :9957 policy info created, req=0x7fa2aa1048, ses_ctx=0x7fa1d4f0c8, info=0x7fa1eaf958
      [I][p:3335][s:41045][r:33554433] wad_http_req_proc_policy :9635 ses_ctx:ct|Pvx|Me|H|C|A1|O conn_srv=0 fwd_srv=
      [I][p:3335][s:41045][r:33554433] wad_http_req_proc_policy :9770 policy result:vf_id=0:0 sec_profile=0x7fa1d7c9c8 set_cookie=0
      [I][p:3335][s:41045][r:33554433] wad_http_req_domain_front_chk :9563 chk uri host res=pass (is_abs=0 http_encap=0 connect=0): ztnaproxy.ad.labdomain.com
      [V][p:3335][s:41045][r:33554433] wad_http_session_disconn_srv :1659 hcs=0x7fa33e9228 http_svr=(nil)
      [I][p:3335][s:41045][r:33554433] wad_http_connect_srv :852
      http ses=0x7fa33e9228 req=0x7fa2aa1048 ses_ctx=0x7fa1d4f0c8
      nontp(0) dst_type(3)
      req: dst:172.31.1.1:3389, proto:10)
      connect svr orig 172.31.1.2:51280->172.31.1.1:443 out 172.31.1.2:0->172.31.1.1:443
      [I][p:3335][s:41045][r:33554433] wad_http_upd_ses_ctx_by_req :948 wad http session 0x7fa33e9228 forward (nil) fwd_srv_ip=
      [V][p:3335][s:41045][r:33554433] wad_http_connect_srv :875 [0x7fa2aa1048] Connect to server: 192.168.1.241:3389/192.168.1.241:3389
      [I][p:3335][s:41045][r:33554433] wad_http_ses_ctx_find_conn_port :653 ssi: 0x7fa34d0c08 is looking for conn_port by vf:0 from:any to:192.168.1.241:3389 outif:-1
      [I][p:3335][s:41045][r:33554433] wad_http_ses_ctx_find_conn_port :653 ssi: 0x7fa34d0c08 is looking for conn_port by vf:0 from:any to:192.168.1.241:3389 outif:-1
      [V][p:3335][s:41045][r:33554433] wad_http_mstrm_ops_read_block :1428 strm paused, flag=0x0 is_clt=1
      [V][p:3335][s:41045][r:33554433] wad_http_req_get_svr :7985 http session 0x7fa33e9228 req=0x7fa2aa1048 connecting
      [V][p:3335][s:41045] wad_vs_http_server_connected :5484 port=0x7fa1abe2d8
      [V][p:3335] wad_http_msg_start_setup_proc :2131 msg(0x7fa2aa1048) proc-setup started from: req_vs.
      [V][p:3335] wad_http_def_proc_msg_plan :2093 msg(0x7fa2aa1048) setting up processor(req_vs)
      [I][p:3335] wad_http_req_check_vs_tunnel_type :3319 try to match HTTPS/HTTP/SSH/CIFS/DOT with nport=3389
      [I][p:3335] wad_http_req_check_vs_tunnel_type :3349 port=3389, proto=-1, protocol not matched.
      [I][p:3335] wad_http_req_check_vs_tunnel_type :3359 Check redir PROXY port=3389((null))
      [I][p:3335] wad_http_req_check_vs_tunnel_type :3367 TCP tunnel detected without type.
      [V][p:3335] wad_http_req_convert_tfap_tunnel :3211 req(0x7fa2aa1048) is converting to VS forwarding tunnel.
      [V][p:3335] wad_http_build_tfap_resp :653 ua=Forticlient
      [I][p:3335] wad_http_req_set_tfap_param :3193 req:0x7fa2aa1048 got tunnel param type:unknown proto:tcp off_ssl:1
      [I][p:3335][s:41045][r:33554433] wad_dump_fwd_http_resp :3087 hreq=0x7fa2aa1048 Forward response from Internal:
      
      HTTP/1.1 101 Switching Protocols
      Upgrade: tcp-forwarding/1.0
      Connection: Upgrade
      
      [I][p:3335][s:41045][r:33554433] __wad_http_req_close              :2053  ret = -1!
      [I][p:3335][s:41045][r:33554433] wad_http_clt_turn_off_ssl         :3697  replace SSL transport.
      [I][p:3335][s:41045][r:33554433] wad_http_strm_turn_ssl_off :3324 waiting for SSL port(0x7fa1a4b6a0) to close.
      [I][p:3335][s:41045][r:33554433] wad_http_clt_turn_off_ssl :3726 wait for SSL port.
      [I][p:3335][s:41045][r:33554433] wad_http_sstrm_off_ssl_for_tunnel :3647 Server is ready to drain SSL data.
      [I][p:3335][s:41045][r:33554433] wad_http_req_convert_tfap_tunnel :3256 Wait for tunnel convertion.
      [I][p:3335][s:41045] wad_vs_http_server_connected :5487 vs http server connected

      The very first thing that happens after the ClientHello is that the FortiGate tries to find a virtual host, but since we’re doing TCP forwarding, not HTTP(S) access proxy, we don’t care about this part, and “no host matched” tells us there is nothing to be found.

      When FortiTCS connects to the FortiGate, it sends not only the HTTP request to the FortiGate, but also, upon request, the certificate that the logged-in user gets from FortiClient EMS when FortiClient first connects as that user. By default, the access proxy on the FortiGate has “client-cert” enabled, so it must verify the certificate, and for that the EMS ZTNA root CA certificate is used:

      [I][p:3335][s:41045]      wad_vs_ssl_port_caps_cert_req     :14077 675:ZTNA-LAB: requested certs from client. ca_store is ready.
      [I][p:3335][s:41045]      wad_vs_ssl_c2p_cert_req_add_trust_issuers:14038 675:ZTNA-LAB: adding subject: C = CA, ST = California, L = Sunnyvale, O = Fortinet, OU = 00000000000000000000000000000000, CN = FCTEMS8824011503 to CCR.
      [I][p:3335][s:41045]      wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 675:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.

      Once the certificate has been sent, the serial number on the certificates gets verified, and the serial number matches what we see in EMS (ZTNA Serial Number):

      [I][p:3335][s:41045]      wad_ui_ssl_cert_verify            :1562  Certificate verified!
      [I][p:3335][s:41045]      wad_ssl_cert_parse_cert           :1448  EMS SN=FCTEMS8824011503
      [I][p:3335][s:41045]      wad_ssl_cert_parse_cert           :1453  Tenant ID=00000000000000000000000000000000
      [I][p:3335][s:41045]      wad_ssl_cert_get_sn               :1364  ci(0x7fa1b10eb8): sn=3AA3BF4801BF5771BF3079CCDFB7F66DF0841E6F
      [I][p:3335][s:41045]      __wad_ssl_validate_ztna_cert_by_ca_store:3465  Successfully verified cert by CA.
      [I][p:3335][s:41045]      wad_vs_ssl_access_proxy_on_clt_certs:13530 675:ZTNA-LAB: Cert auth success. issued_by: ems
      Serial of endpoint

      After that, we see the incoming HTTP request that FortiTCS has forwarded, including the headers, which is basically the same thing we saw in the FortiClient section.

      GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1
      Host: ztnaproxy.ad.labdomain.com
      User-Agent: Forticlient
      Accept: */*
      Connection: Upgrade
      Cookie:
      Upgrade: tcp-forwarding/1.0
      X-Fos-Auth:

      We get some not-so-important information about the path, host length, virtual host matching, and then we see the FortiGate performing the DNS resolution for the FQDN that was supplied in the HTTP request:

      [I][p:3335][s:41045][r:33554433] wad_vs_gwy_tcp_get_parameters     :3136  675:ZTNA-LAB:1: got fqdn=win-server.ad.labdomain.com.
      [V][p:3335][s:41045][r:33554433] wad_vs_matcher_map_find           :798   Empty matcher!
      [W][p:3335][s:41045][r:33554433] wad_vs_proxy_dns_resolve          :3290  req(0x7fa2aa1048) vs DNS request name=win-server.ad.labdomain.com len=27 type/pref=0/0
      [V][p:3335][s:41045][r:33554433] wad_http_mstrm_ops_read_block     :1428  strm paused, flag=0x0 is_clt=1
      [V][p:3335][s:41045][r:33554433] wad_http_clt_read_sync            :2955  hs=0x7fa33e9228 pause=(1/0x0) ret=1 execute=wad_http_clt_read_req_line
      [I][p:3335][s:41045][r:33554433] wad_vs_proxy_dns_request_done     :3237  req(0x7fa2aa1048) vs DNS resolved: 192.168.1.241

      After that, the FortiGate tries to match a policy, since it now has the 5-tuple:

      [V][p:3335][s:41045][r:33554433] wad_http_req_check_policy_with_flags:12021 start match policy vd=0(ses_ctx:ct|Pv|M|H|C|A1|O) (172.31.1.2:51280@8->192.168.1.241:3389@5) absUrl=0
      [V][p:3335][s:41045][r:33554433] wad_fw_addr_match_ap              :1292  matching ap:ZTNA-LAB(8) with vip addr:ZTNA-LAB(8)
      [I][p:3335][s:41045][r:33554433] wad_http_policy_match_one         :501   fw_pol_id=123(pol_ctx:th|Ad|7|=p) pflag:H|W|U|A asyn_info=1
      [I][p:3335][s:41045][r:33554433] wad_fw_policy_async_match         :5929  pol_ctx:th|Ad|7|=d
      [I][p:3335][s:41045][r:33554433] wad_http_req_policy_set           :10156 match policy-id=123(pol_ctx:th|Ad|7|=d) vd=0(ses_ctx:ct|Pv|Me|H|C|A1|O) (172.31.1.2:51280@8 -> 192.168.1.241:3389@5)

      With the supplied information, it matched the policy with ID 123 (I specifically set this ID to make it more visible).

      Note that I am using proxy policies for ZTNA connections, since that is the recommended way, and from experience, it works better.

      We found a policy, and as long as the policy action is accept the FortiGate connects to the real server:

      [V][p:3335][s:41045][r:33554433] wad_http_connect_srv              :875   [0x7fa2aa1048] Connect to server: 192.168.1.241:3389/192.168.1.241:3389

      With default settings, the FortiGate will perform SNAT using the interface IP of the “closest” interface to the real server. “Closest” in this case means according to the routing table. If the real server is on a directly connected interface, then that interface’s IP will be used. If the server is reachable via another route, then the interface on which that traffic would egress will be used.

      A packet capture on the real server shows this:

      Capture to show no NAT

      You can overwrite this behaviour on the CLI by using an IP pool:

      config firewall ippool
          edit "ZTNA-POOL"
              set startip 192.168.1.251
              set endip 192.168.1.251
          next
      end
      config firewall proxy-policy
          edit 123
              set poolname "ZTNA-POOL"
          next
      end

      After that, we can see that the real server sees the IP from the pool:

      Capture to show active NAT

      Finishing up the debug, we can see the FortiGate trying to match some protocols, then a conversion to a Virtual Server (VS) forwarding tunnel, and it ends with a “vs http server” connected message.

      [I][p:3335]               wad_http_req_check_vs_tunnel_type :3319  try to match HTTPS/HTTP/SSH/CIFS/DOT with nport=3389
      [I][p:3335]               wad_http_req_check_vs_tunnel_type :3349  port=3389, proto=-1, protocol not matched.
      [I][p:3335]               wad_http_req_check_vs_tunnel_type :3359  Check redir PROXY port=3389((null))
      [I][p:3335]               wad_http_req_check_vs_tunnel_type :3367  TCP tunnel detected without type.
      [V][p:3335]               wad_http_req_convert_tfap_tunnel  :3211  req(0x7fa2aa1048) is converting to VS forwarding tunnel.
      [V][p:3335]               wad_http_build_tfap_resp          :653   ua=Forticlient
      [I][p:3335]               wad_http_req_set_tfap_param       :3193  req:0x7fa2aa1048 got tunnel param type:unknown proto:tcp off_ssl:1
      [I][p:3335][s:41045][r:33554433] wad_dump_fwd_http_resp            :3087  hreq=0x7fa2aa1048 Forward response from Internal:
      
      HTTP/1.1 101 Switching Protocols
      Upgrade: tcp-forwarding/1.0
      Connection: Upgrade
      [I][p:3335][s:41045][r:33554433] wad_http_strm_turn_ssl_off        :3324  waiting for SSL port(0x7fa1a4b6a0) to close.
      [I][p:3335][s:41045][r:33554433] wad_http_clt_turn_off_ssl         :3726  wait for SSL port.
      [I][p:3335][s:41045][r:33554433] wad_http_sstrm_off_ssl_for_tunnel :3647  Server is ready to drain SSL data.
      [I][p:3335][s:41045][r:33554433] wad_http_req_convert_tfap_tunnel  :3256  Wait for tunnel convertion.
      [I][p:3335][s:41045]      wad_vs_http_server_connected      :5487  vs http server connected

      That is more or less everything relevant that the FortiGate does in a basic ZTNA TCP forwarding connection.

      At a high level, it’s:

      1. Certificate validation
      2. An optional step for DNS resolution
      3. Policy matching
      4. Establishing a proxy connection

      Now, let’s turn to some options we can enable to cover some other use cases.

      UDP? UDP.

      Since FortiOS 7.6.0 and FortiClient 7.4.1, UDP connections can also be handled by ZTNA; before that, only TCP was supported.

      The way this works is that when FortiClient recognizes a relevant UDP connection attempt, the UDP traffic is encapsulated in TCP, a QUIC connection is formed, and traffic gets forwarded like a regular ZTNA connection by FortiTCS.

      Here is a debug log from a ZTNA UDP connection from fortitcs_X.log

      FortiClient ZTNA UDP

      [2025-11-30 18:50:16.7098277] [fortitcs] FindFQDNFromDoh: IP=10.235.0.2 Port=53 FQDN=win-server.ad.labdomain.com
      [2025-11-30 18:50:16.7099651] [fortitcs] UpdateDnsRedirectEntry: Ip=183173122 Port=53 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-11-30 18:50:16.7100409] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 10.235.0.2fqdn: win-server.ad.labdomain.com
      [2025-11-30 18:50:16.7100897] [fortitcs] Establish: ztnaproxy.ad.labdomain.com:443
      [2025-11-30 18:50:16.7102869] [fortitcs] strPort: 53
      [2025-11-30 18:50:16.7105815] [fortitcs] Request: GET /tcp?address=win-server.ad.labdomain.com&port=53&proto=udp HTTP/1.1
      Host: ztnaproxy.ad.labdomain.com:443
      Accept: /
      User-Agent: Forticlient
      X-Fos-Auth:
      
      [2025-11-30 18:50:16.7105917] [fortitcs] Establish: ph2
      [2025-11-30 18:50:16.7106238] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-11-30 18:50:16.7106342] [fortitcs] Establish: ph3
      [2025-11-30 18:50:16.7106407] [fortitcs] tls_config.InsecureSkipVerify: true
      [2025-11-30 18:50:16.7106714] [fortitcs] Establishing new connection to ztnaproxy.ad.labdomain.com:443
      [2025-11-30 18:50:16.7776034] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-11-30 18:50:16.7900419] [fortitcs] Not found virtual IP for ztnaproxy.ad.labdomain.com.
      [2025-11-30 18:50:16.8184412] [fortitcs] Handshake complete for 172.31.1.1:443
      [2025-11-30 18:50:16.8319126] [fortitcs] Establish: RoundTripOpt end!
      [2025-11-30 18:50:16.8320560] [fortitcs] Establish: ph4
      [2025-11-30 18:50:16.8320814] [fortitcs] Establish: ph5
      [2025-11-30 18:50:16.8320925] [fortitcs] Establish: ph6
      [2025-11-30 18:50:16.8321824] [fortitcs] Establish: ph7
      [2025-11-30 18:50:16.8322048] [fortitcs] Establish: ph8
      [2025-11-30 18:50:16.8322226] [fortitcs] Establish: ph9
      [2025-11-30 18:50:16.8322504] [fortitcs] Establish: ph10

      The log doesn’t look that different from a TCP connection, except that we can see that “proto=udp” is now in the HTTP request, but HTTP 1.1 is still in use here, so no QUIC now.

      The debug log on the FortiGate’s side is a bit more interesting:

      FortiGate ZTNA UDP

      [V][p:3291] wad_quic_sock_listener_on_event :1070 start processing QUIC event=0x1 events=0x1 fd=36
      [I][p:3291] wad_quic_sock_rx :514 |sock 36|recv packet|ecn 0|bytes 1252|
      [I][p:3291] wad_quic_sock_rx :521 |src:172.31.1.2:65309->dst:172.31.1.1:443|ifindex 8|
      [V][p:3291][s:40194] wad_vs_proxy_match_vhost :4662 675:ZTNA-LAB: matching vhost by: ztnaproxy.ad.labdomain.com
      [V][p:3291][s:40194] wad_vs_matcher_map_find :798 Empty matcher!
      [V][p:3291][s:40194] wad_vs_proxy_match_vhost :4665 675:ZTNA-LAB: no host matched.
      [I][p:3291][s:40194] wad_quic_tls_c2vs_cert_req_add_trust_issuers:5681 adding subject: C = CA, ST = California, L = Sunnyvale, O = Fortinet, OU = 00000000000000000000000000000000, CN = FCTEMS8824011503 to CCR.
      [V][p:3291][s:40194] wad_key_share_bucket_get_entry :172 key_share 0x7f84d23c48 g=29 use_cnt=1 holding=1 reuseable 1
      [I][p:3291] wad_quic_tls_c2vs_ap_on_certs :5468 conn:0x7f84fd4048|c2vs| received certs from the client.
      [I][p:3291] __wad_ssl_cert_open_quic_cert :710 https server uses key_len 2048
      [I][p:3291] wad_quic_tls_c2vs_ap_on_certs :5478 conn:0x7f84fd4048|c2vs| cert cache cert(0x7f83725ed8) authi(0x7f8401e638)
      [I][p:3291] wad_ssl_cert_check_auth_status_with_ca_store:464 authi(0x7f8401e638) status(0)
      [I][p:3291] wad_ssl_cert_parse_cert :1448 EMS SN=FCTEMS8824011503
      [I][p:3291] wad_ssl_cert_parse_cert :1453 Tenant ID=00000000000000000000000000000000
      [I][p:3291] wad_ssl_cert_get_sn :1364 ci(0x7f83452f40): sn=3AA3BF4801BF5771BF3079CCDFB7F66DF0841E6F
      [I][p:3291] wad_ssl_validate_cert_by_cas :3365 Certificate verified!
      [I][p:3291] wad_ssl_validate_cert_by_ca_store :3421 Single cert verified Done!
      [I][p:3291] wad_quic_tls_c2vs_ap_on_certs :5517 conn:0x7f84fd4048|c2vs| Cert auth success. issued_by: ems
      [V][p:3291] wad_quic_sock_listener_on_event :1070 start processing QUIC event=0x1 events=0x1 fd=36
      [V][p:3291] wad_quic_conn_rx_hs :8615 conn:0x7f84fd4048|c2vs| conn: 0x7f84fd4048 dcid: path: src:172.31.1.2:65309 dst:172.31.1.1:443 activated
      [V][p:3291][s:40194][r:33554433] wad_http_req_alloc :1911 req=0x7f84fa3048(ses_ctx:t|Pv|M|H|C|A1|O) dst(orig=172.31.1.1:443 dst=172.31.1.1:443 srv=172.31.1.1:443) hs=0x7f84d4a228 clt_port=0x555cde52d8 svr_port=0x555cde52d8
      [I][p:3291][s:40194][r:33554433] wad_dump_http_request :3066 hreq=0x7f84fa3048 Received request from client: 172.31.1.2:65309
      
      GET /tcp?address=win-server.ad.labdomain.com&port=53&proto=udp HTTP/3.0
      Host: ztnaproxy.ad.labdomain.com:443
      x-fos-auth:
      user-agent: Forticlient
      accept: /
      accept-encoding: gzip
      
      [V][p:3291][s:40194][r:33554433] wad_http_marker_uri :1526 path=/tcp len=4
      [V][p:3291][s:40194][r:33554433] wad_http_parse_host :1904 host_len=30
      [V][p:3291][s:40194][r:33554433] wad_http_parse_host :1946 len=26
      [V][p:3291][s:40194][r:33554433] wad_http_parse_host :1955 len=3
      [I][p:3291][s:40194][r:33554433] wad_http_str_canonicalize :2466 enc=0 path=/tcp len=4 changes=0
      [I][p:3291][s:40194][r:33554433] wad_http_str_canonicalize :2468 end=4 path=address=win-server.ad.labdomain.com&port=53&proto=udp len=53 changes=0
      [V][p:3291][s:40194][r:33554433] wad_http_normalize_uri :2705 host_len=26 path_len=4 query_len=53
      [I][p:3291][s:40194][r:33554433] wad_http_req_detect_special :14378 captive_portal detected: false, preflight=(null)
      [I][p:3291][s:40194][r:33554433] wad_vs_proxy_match_gwy :4543 675:ZTNA-LAB: matching gwy with vhost(def_virtual_host)
      [V][p:3291][s:40194][r:33554433] wad_vs_proxy_match_vhost :4662 675:ZTNA-LAB: matching vhost by: ztnaproxy.ad.labdomain.com
      [V][p:3291][s:40194][r:33554433] wad_vs_matcher_map_find :798 Empty matcher!
      [V][p:3291][s:40194][r:33554433] wad_vs_proxy_match_vhost :4665 675:ZTNA-LAB: no host matched.
      [I][p:3291][s:40194][r:33554433] wad_vs_proxy_match_gwy :4611 675:ZTNA-LAB: matching gwy by (/tcp) with vhost(def_virtual_host).
      [I][p:3291][s:40194][r:33554433] wad_vs_proxy_match_gwy :4630 675:ZTNA-LAB: Matched gwy(1) type(tcp-fwd).
      [I][p:3291][s:40194][r:33554433] wad_vs_gwy_tcp_dst_ovrd :3382 675:ZTNA-LAB:1: req(0x7f84fa3048) query(address=win-server.ad.labdomain.com&port=53&proto=udp)
      [I][p:3291][s:40194][r:33554433] wad_vs_gwy_tcp_get_parameters :3136 675:ZTNA-LAB:1: got fqdn=win-server.ad.labdomain.com.
      [I][p:3291][s:40194][r:33554433] wad_vs_gwy_tcp_get_parameters :3166 675:ZTNA-LAB:1: failed to get TLS, set to 0.
      [V][p:3291][s:40194][r:33554433] wad_vs_matcher_map_find :798 Empty matcher!
      [W][p:3291][s:40194][r:33554433] wad_vs_proxy_dns_resolve :3290 req(0x7f84fa3048) vs DNS request name=win-server.ad.labdomain.com len=27 type/pref=0/0
      [I][p:3291][s:40194][r:33554433] wad_vs_proxy_dns_request_done :3237 req(0x7f84fa3048) vs DNS resolved: 192.168.1.241
      [I][p:3291][s:40194][r:33554433] wad_http_req_exec_on_vs_dns_ready :12441 req(0x7f84fa3048) vs DNS ready: dns_resolved(1), domain_matched(0), addr_matched(1)
      [V][p:3291][s:40194][r:33554433] wad_http_req_get_dst_intf :12196 vd=0 dst=192.168.1.241 ifidx=5
      [V][p:3291][s:40194][r:33554433] wad_http_req_vs_check_policy :12369 HTTP req=0x7f84fa3048 out_intf=5, vwl=0
      [V][p:3291][s:40194][r:33554433] wad_http_req_check_policy_with_flags:12021 start match policy vd=0(ses_ctx:t|Pv|M|H|C|A1|O) (172.31.1.2:65309@8->192.168.1.241:53@5) absUrl=0
      [V][p:3291][s:40194][r:33554433] wad_fw_addr_match_ap :1292 matching ap:ZTNA-LAB(8) with vip addr:ZTNA-LAB(8)
      [I][p:3291][s:40194][r:33554433] wad_http_policy_match_one :501 fw_pol_id=123(pol_ctx:th|Ad|7|=p) pflag:H|W|U|A asyn_info=1
      [I][p:3291][s:40194][r:33554433] wad_fw_policy_async_match :5929 pol_ctx:th|Ad|7|=d
      [I][p:3291][s:40194][r:33554433] wad_http_req_policy_set :10156 match policy-id=123(pol_ctx:th|Ad|7|=d) vd=0(ses_ctx:t|Pv|Me|H|C|A1|O) (172.31.1.2:65309@8 -> 192.168.1.241:53@5)
      [V][p:3291][s:40194][r:33554433] wad_https_ap_pol_info_get :9957 policy info created, req=0x7f84fa3048, ses_ctx=0x7f836b30c8, info=0x7f84009710
      [W][p:3291][s:40194][r:33554433] wad_http_srv_slct_no_connect_get_connectable:2179 trace
      [I][p:3291][s:40194][r:33554433] wad_http_req_proc_policy :9635 ses_ctx:t|Pvx|Me|H|C|A1|O conn_srv=0 fwd_srv=
      [I][p:3291][s:40194][r:33554433] wad_http_req_proc_policy :9770 policy result:vf_id=0:0 sec_profile=0x7f836e09c8 set_cookie=0
      [V][p:3291][s:40194][r:33554433] wad_http_srv_slct_no_connect_get_srv:2145 trace
      [I][p:3291][s:40194][r:33554433] wad_http_upd_ses_ctx_by_req :948 wad http session 0x7f84d4a228 forward (nil) fwd_srv_ip=
      [V][p:3291][s:40194][r:33554433] wad_http_req_get_svr :7981 http session 0x7f84d4a228 req=0x7f84fa3048 connected
      [V][p:3291][s:40194][r:33554433] wad_http_msg_start_setup_proc :2131 msg(0x7f84fa3048) proc-setup started from: req_vs.
      [V][p:3291][s:40194][r:33554433] wad_http_def_proc_msg_plan :2093 msg(0x7f84fa3048) setting up processor(req_vs)
      [I][p:3291][s:40194][r:33554433] wad_http_req_check_vs_tunnel_type :3319 try to match HTTPS/HTTP/SSH/CIFS/DOT with nport=53
      [I][p:3291][s:40194][r:33554433] wad_http_req_check_vs_tunnel_type :3349 port=53, proto=-1, protocol not matched.
      [I][p:3291][s:40194][r:33554433] wad_http_req_check_vs_tunnel_type :3359 Check redir PROXY port=53((null))
      [I][p:3291][s:40194][r:33554433] wad_http_req_check_vs_tunnel_type :3367 TCP tunnel detected without type.
      [V][p:3291][s:40194][r:33554433] wad_http_req_convert_tfap_tunnel :3211 req(0x7f84fa3048) is converting to VS forwarding tunnel.
      [V][p:3291][s:40194][r:33554433] wad_http_build_tfap_resp :653 ua=Forticlient
      [I][p:3291][s:40194][r:33554433] wad_http_req_set_tfap_param :3193 req:0x7f84fa3048 got tunnel param type:unknown proto:tcp/udp off_ssl:1
      [I][p:3291][s:40194][r:33554433] wad_dump_fwd_http_resp :3087 hreq=0x7f84fa3048 Forward response from Internal:
      
      HTTP/1.1 101 Switching Protocols
      Upgrade: tcp-forwarding/1.0
      Connection: Upgrade
      
      [I][p:3291][s:40194][r:33554433] wad_http_req_resp_fwd_done :3443 req(0x7f84fa3048) resp(0x7f837f0b00/(nil)) resp-fwd done!
      [V][p:3291][s:40194][r:33554433] wad_http_resp_msg_fwd_done :1828 resp(0x7f837f0b00) forwarding done! shmstrm((nil))
      [I][p:3291][s:40194][r:33554433] wad_http_req_finished :1574 req=0x7f84fa3048 cstrm=0x7f84d4a2d0 req_done=1 done_close=0
      [I][p:3291][s:40194][r:33554433] __wad_http_req_close :2053 ret = -1!
      [I][p:3291][s:33554433] wad_http_req_convert_tfap_tunnel :3258 Converted to tfap tunnel.

      The first line already tells us about the QUIC connection that is being started. We then get the regular virtual host matching and certificate verification.

      The HTTP GET now shows “HTTP/3.0”, so we know this is QUIC, and it also has an “accept-encoding” header with gzip as its value, but I can’t tell you for what gzip is used.

      The rest of the debug is much the same as before, except with UDP traffic.

      About that Encryption setting

      We can also enable the encryption setting for ZTNA destinations, and the documentation states the following:

      When Encryption is enabled, traffic between FortiClient and the FortiGate is always encrypted, even if the original traffic has already been encrypted. When Encryption is disabled, traffic between FortiClient and the FortiGate is not encrypted.

      So this setting makes sense for traffic that isn’t encrypted at a protocol level, like FTP, HTTP, telnet, etc.

      How does this look in practice? Here is a comparison picture of two Wireshark captures of HTTP requests, performed on the client, to win-server.ad.labdomain.com to get the default Microsoft IIS start page. The encrypted connection is on the left, and the unencrypted connection is on the right.

      Wireshark to show encryption vs. no encryption

      We can see that only destination port 443 is ever in use, because traffic is destined to the FortiGate, which has its access proxy listening on 443, but the traffic being sent is the actual HTTP traffic, which is why we see “iisstart.png” and “favicon.ico” in the unencrypted version, but we don’t in the encrypted version.

      It’s a bit pointless to show, but you can believe me that the end result for the client was the same page.

      So if you turn on the encryption setting, even the client’s NIC doesn’t know what is being transported, regardless of the protocol.

      A quick look at the fortitcs_X.log doesn’t display many differences, except that the “tls” parameter is now at 1.

      FortiClient ZTNA encrypted

      [2025-12-04 18:09:26.0012819] [fortitcs] Accept success
      [2025-12-04 18:09:26.0013202] [fortitcs] raw socket: 1644
      [2025-12-04 18:09:26.0016489] [fortitcs] status: recordsSize: 988
      [2025-12-04 18:09:26.0017080] [fortitcs] redirectContextSize: 260
      [2025-12-04 18:09:26.0017550] [fortitcs] contextSize: 260
      [2025-12-04 18:09:26.0017765] [fortitcs] family: 2 port: 80 ip: 10.235.0.2 pid: 12112
      [2025-12-04 18:09:26.0019011] [fortitcs] forward ip: 10.235.0.2 port: 80
      [2025-12-04 18:09:26.0019647] [fortitcs] FindFQDNFromDoh: IP=10.235.0.2 Port=80 FQDN=win-server.ad.labdomain.com
      [2025-12-04 18:09:26.0019802] [fortitcs] UpdateDnsRedirectEntry: Ip=183173122 Port=80 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-12-04 18:09:26.0020077] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 10.235.0.2fqdn: win-server.ad.labdomain.com
      [2025-12-04 18:09:26.0020579] [fortitcs] Request is not for PAM Agent. Ip: 10.235.0.2, Port: 80, FQDN: win-server.ad.labdomain.com
      [2025-12-04 18:09:26.0020805] [fortitcs] According to PID 12112 Get session ID: 5
      [2025-12-04 18:09:26.0021202] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-12-04 18:09:26.0032192] [fortitcs] gateway ip: 172.31.1.1 port: 443encryption: 1 cur_thread: 6288
      [2025-12-04 18:09:26.0102319] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 192.168.1.240 fqdn:
      [2025-12-04 18:09:26.0103274] [fortitcs] enable_udp == 0
      [2025-12-04 18:09:26.0104330] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 192.168.1.240 fqdn:
      [2025-12-04 18:09:26.0105029] [fortitcs] enable_udp == 0
      [2025-12-04 18:09:26.0407382] [fortitcs] nSession(5)
      [2025-12-04 18:09:26.0413592] [fortitcs] ImpersonateLoggedOnUser success! cur_thread = 6288
      [2025-12-04 18:09:26.0445245] [fortitcs] trace reinit -> pid: 10132
      [2025-12-04 18:09:26.0746709] [fortitcs] handshake success!
      [2025-12-04 18:09:26.0748232] [fortitcs] SAML address: https://172.31.1.1:443/tcp?address=win-server.ad.labdomain.com&port=80&tls=1
      [2025-12-04 18:09:26.0748731] [fortitcs] Cache address: https://172.31.1.1:443
      [2025-12-04 18:09:26.0751770] [fortitcs debug] GetSAMLAuth: no token found for gateway: 172.31.1.1:443
      [2025-12-04 18:09:26.0753055] [fortitcs debug] GetFormAuth: no token found for gateway: 172.31.1.1:443
      [2025-12-04 18:09:26.0757257] [fortitcs] Request: GET /tcp?address=win-server.ad.labdomain.com&port=80&tls=1 HTTP/1.1
      Host: 172.31.1.1
      Accept: /
      Authorization:
      Cookie:
      User-Agent: Forticlient
      X-Fos-Auth:
      
      [2025-12-04 18:09:26.0760973] [fortitcs] Send tunnel cmd Success
      [2025-12-04 18:09:26.0784150] [fortitcs] response code: 101 html Reply:
      [2025-12-04 18:09:26.0786411] [fortitcs] Tunnel upgrade success
      [2025-12-04 18:09:26.0786899] [fortitcs] CallUpdateStatus call! mod=4 status=0 msgType=0
      [2025-12-04 18:09:26.0825090] [fortitcs] procUpdateStatus: mod=4 status=0 msgType=0 msg.Extra= ret = 1
      [2025-12-04 18:09:26.0827333] [fortitcs] Forward win-server.ad.labdomain.com:: local: 127.0.0.1:61022 -> remote: 127.0.0.1:57137proxy: 172.31.1.2:57138 -> remote: 172.31.1.1:443

      The only notable difference on the FortiGate between an encrypted and an unencrypted connections that I could find, is this on the unencrypted connection:

      [I][p:693]               wad_http_req_set_tfap_param       :3193  req:0x7f9492c048 got tunnel param type:http proto:tcp off_ssl:1
      [I][p:693][s:5086][r:33554433] wad_dump_fwd_http_resp            :3087  hreq=0x7f9492c048 Forward response from Internal:
      
      HTTP/1.1 101 Switching Protocols
      Upgrade: tcp-forwarding/1.0
      Connection: Upgrade
      
      [I][p:693][s:5086][r:33554433] __wad_http_req_close              :2053  ret = -1!
      [I][p:693][s:5086][r:33554433] wad_http_clt_turn_off_ssl         :3697  replace SSL transport.
      [I][p:693][s:5086][r:33554433] wad_http_strm_turn_ssl_off        :3324  waiting for SSL port(0x7f939096a0) to close.
      [I][p:693][s:5086][r:33554433] wad_http_clt_turn_off_ssl         :3726  wait for SSL port.
      [I][p:693][s:5086][r:33554433] wad_http_sstrm_off_ssl_for_tunnel :3647  Server is ready to drain SSL data.
      [I][p:693][s:5086][r:33554433] wad_http_req_convert_tfap_tunnel  :3256  Wait for tunnel convertion.
      [I][p:693][s:5086]       wad_vs_http_server_connected      :5487  vs http server connected
      [I][p:693]               wad_http_strm_on_clt_ssl_sock_port:3572  client port replaced.
      [I][p:693]               wad_http_cstrm_off_ssl_for_tunnel :3542  Client is ready to drain SSL data.
      [I][p:693]               wad_http_strm_on_clt_ssl_sock_port:3575  Client off-ssl done.

      The encrypted connection has “off_ssl” set to 0 instead of 1, and the SSL transport replacement, closing, and draining aren’t present on the encrypted connection either. SSL here is, of course, TLS, but we all know how good the industry is with displacing the term SSL.

      Here is the encrypted connection as a comparison:

      [I][p:725]               wad_http_req_set_tfap_param       :3193  req:0x7f96101048 got tunnel param type:http proto:tcp off_ssl:0
      [I][p:725][s:5673][r:33554433] wad_dump_fwd_http_resp            :3087  hreq=0x7f96101048 Forward response from Internal:
      
      HTTP/1.1 101 Switching Protocols
      Upgrade: tcp-forwarding/1.0
      Connection: Upgrade
      
      [I][p:725][s:5673][r:33554433] __wad_http_req_close              :2053  ret = -1!
      [I][p:725][s:5673][r:33554433] wad_http_strm_convert_tunnel      :4391  cstrm(0x7f948deb00) sstrm(0x7f948dec88) type:http proto:tcp
      [I][p:725][s:5673][r:33554433] wad_http_srv_slct_static_set_connectable:1113  static server selector connectable set to 0.
      ap posture change observer allocated, 0x7f945e7ce8
      [I][p:725][s:5673][r:33554433] wad_http_req_convert_tfap_tunnel  :3256  Wait for tunnel convertion.
      [I][p:725][s:5673]       wad_vs_http_server_connected      :5487  vs http server connected

      Do we need EMS for this?

      Yes. Moving on.

      Okay, let’s expand on this. We absolutely need EMS for a ZTNA TCP forwarding connection, because only with licensed FortiClients, which require EMS, can we even configure ZTNA destinations.

      EMS gives every user a client certificate, and this certificate is, by default, verified by the FortiGate. In debugs further up, we already saw this, but here it is again:

      [I][p:3335][s:41045]      wad_vs_ssl_port_caps_cert_req     :14077 675:ZTNA-LAB: requested certs from client. ca_store is ready.
      [I][p:3335][s:41045]      wad_vs_ssl_c2p_cert_req_add_trust_issuers:14038 675:ZTNA-LAB: adding subject: C = CA, ST = California, L = Sunnyvale, O = Fortinet, OU = 00000000000000000000000000000000, CN = FCTEMS8824011503 to CCR.
      [I][p:3335][s:41045]      wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 675:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.
      [I][p:3335][s:41045]      wad_vs_ssl_access_proxy_on_clt_certs:13476 675:ZTNA-LAB: received certs from the client.
      [I][p:3335][s:41045]      __wad_ssl_cert_open_cert          :641   https server uses key_len 2048
      [I][p:3335][s:41045]      wad_ssl_cert_check_auth_status_with_ca_store:464   authi(0x7fa1ec2570) status(0)
      [I][p:3335][s:41045]      wad_ui_ssl_cert_verify            :1562  Certificate verified!
      [I][p:3335][s:41045]      wad_ssl_cert_parse_cert           :1448  EMS SN=FCTEMS8824011503
      [I][p:3335][s:41045]      wad_ssl_cert_parse_cert           :1453  Tenant ID=00000000000000000000000000000000
      [I][p:3335][s:41045]      wad_ssl_cert_get_sn               :1364  ci(0x7fa1b10eb8): sn=3AA3BF4801BF5771BF3079CCDFB7F66DF0841E6F
      [I][p:3335][s:41045]      __wad_ssl_validate_ztna_cert_by_ca_store:3465  Successfully verified cert by CA.
      [I][p:3335][s:41045]      wad_vs_ssl_access_proxy_on_clt_certs:13530 675:ZTNA-LAB: Cert auth success. issued_by: ems

      When a FortiGate connects to EMS, it will load the FortiClient ID, the EMS serial that hands out user certificates, endpoint information, associated hardware information, and the EMS ZTNA root CA certificate into memory. Note that nothing about the user certificate is mentioned in these messages, because this is endpoint information, not user information.

      FortiGate endpoint update

      wad_ui_global_ec_fctems_on_event :5299 receive global endpoint-control fctems event.
      [I][p:604] wad_conf_update_gen_array :6704 vd: NA update: [global]endpoint-control.fctems now_gen: 1834171729 last_gen:1834171728
      accepted connection from pid 192
      accept us=0x7f8c90fa90 fd=32 make_ret=1
      [I][p:597] wad_info_store_update_ec :144 Update EC(uid F3AB0220E981419CAA9A13CE4811E3F9 sn FCTEMS8824011503 tenant id 00000000000000000000000000000000).
      Received multi-src attr src=EMS, type=VENDOR, val=Dell Inc., weight=255
      Received multi-src attr src=EMS, type=HW_TYPE, val=Latitude 5300, weight=255
      Hardware/Software src infos before updating
      Merged node result:
      Last updated time:2025-12-01 19:28:16 src=CID, type=VENDOR, val=Dell, weight=128
      Last updated time:2025-12-01 19:28:16 src=CID, type=OS_NAME, val=Windows, weight=128
      Source node 1, src=CID
      Last updated time:2025-12-01 19:28:16 src=CID, type=VENDOR, val=Dell, weight=128
      Last updated time:2025-12-01 19:28:16 src=CID, type=OS_NAME, val=Windows, weight=128
      Hardware/Software src infos after updating
      Merged node result:
      Last updated time:2025-12-01 19:30:03 src=EMS, type=VENDOR, val=Dell Inc., weight=255
      Last updated time:2025-12-01 19:30:03 src=EMS, type=HW_TYPE, val=Latitude 5300, weight=255
      Last updated time:2025-12-01 19:28:16 src=CID, type=OS_NAME, val=Windows, weight=128
      Source node 1, src=EMS
      Last updated time:2025-12-01 19:30:03 src=EMS, type=VENDOR, val=Dell Inc., weight=255
      Last updated time:2025-12-01 19:30:03 src=EMS, type=HW_TYPE, val=Latitude 5300, weight=255
      Source node 2, src=CID
      Last updated time:2025-12-01 19:28:16 src=CID, type=VENDOR, val=Dell, weight=128
      Last updated time:2025-12-01 19:28:16 src=CID, type=OS_NAME, val=Windows, weight=128
      Updating attr to interface src=EMS, type=VENDOR, val=Dell Inc., weight=255
      Updating attr to interface src=EMS, type=VERSION, val=, weight=0
      Updating attr to interface src=EMS, type=HW_TYPE, val=Latitude 5300, weight=255
      Updating attr to interface src=EMS, type=HW_FAMILY, val=, weight=0
      Updating attr to interface src=CID, type=OS_NAME, val=Windows, weight=128
      Updating attr to interface src=EMS, type=OS_VERSION, val=, weight=0
      [I][p:604] wad_ui_ems_ca_cert_checker :5787 checker 'ems-ca-cert': now_gen: 3 last_gen:2
      [I][p:604] wad_update_conf_checker :6737 checker ems-ca-cert update
      [I][p:523] wad_ui_ssl_ems_ca_changed :858 cur_gen=0, new_gen=1
      [I][p:523] wad_ui_ssl_ca_store_flush :3208 Flush ca_store!
      [I][p:523] __wad_ui_ssl_ca_store_load :3546 Flush the ca store in VDOM root!
      [I][p:523] __wad_ui_ssl_ca_store_load :3568 Load CA file /var/log/ems_ztna_certs/global/EMS1-ZTNARootCA.pem!
      [I][p:523] __wad_ui_ssl_ca_store_load :3576 Load CA files!
      [I][p:523] wad_ui_ssl_ca_store_load :3629 Load CA path /var/log/ems_ztna_certs/global, vdom = root, ret = 1.
      [I][p:523] wad_vs_servers_activate_standby :2316 no standby server to activate.
      [I][p:523] wad_vs_gwy_get_servers_nop :3527 684:ZTNA-LAB:4294967295: trace

      The verification has a caching mechanism, so EMS doesn’t have to be online 100% of the time.

      [I][p:273][s:1490]       wad_vs_ssl_port_caps_cert_req     :14077 684:ZTNA-LAB: requested certs from client. ca_store is ready.
      [I][p:273][s:1490]       wad_vs_ssl_c2p_cert_req_add_trust_issuers:14038 684:ZTNA-LAB: adding subject: C = CA, ST = California, L = Sunnyvale, O = Fortinet, OU = 00000000000000000000000000000000, CN = FCTEMS8824011503 to CCR.
      [I][p:273][s:1490]       wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 684:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.
      [I][p:273][s:1490]       wad_vs_ssl_access_proxy_on_clt_certs:13476 684:ZTNA-LAB: received certs from the client.
      [I][p:273][s:1490]       wad_ssl_cert_auth_find            :176   Found auth_info (stable)
      [I][p:273][s:1490]       wad_vs_ssl_access_proxy_on_clt_certs:13530 684:ZTNA-LAB: Cert auth success. issued_by: ems

      “Found auth info (stable)” is the short version of the full verification using cached information, from what I can gather.

      If the FortiGate reboots, the cached information gets lost, and if EMS isn’t available, the EMS ZTNA root CA certificate won’t be loaded, and thus the certificate can’t be verified.

      FortiGate certificate validation failure

      [I][p:273] wad_unix_stream_sendmsg :297 WAD unix stream stream 0x7f75aa7048 msg=0x7f75a8f048 n_fd=1.
      [I][p:276] wad_tcp_port_alloc :1482 alloc tcp_port=0x7fa5058568
      [I][p:276] __wad_tcp_port_rebuild :2517 rebuild client TCP 172.31.1.2:61586 -> 172.31.1.1:443 on 62
      [I][p:276][s:488] wad_session_context_learn_session_config:464 vf_id=0 ses_ctx=0x7fa531b0e8 policy-id=0, sec_profile=(nil) app_type=http
      wan_opt_mode=0 av_idx=0 dd_method=0 wan_opt_tcp=0
      tp-mode=0 web_cache=0 webcache_ssl=0
      check_policy: http=0 ssh=0 ssh_tun=0 fw_ztna=0 ap=1
      ipsapp_redirect=0
      ssl_enabled=0 ssl_full=0 wanopt_ssl=0 ssl_proc=
      ses_ctx:t|Pv|M|H|C|A7|O fwdsvr=''
      [I][p:276][s:488] wad_ssl_port__open :23548 port=0x7fa5058568 type=10 making SSL port
      [I][p:276][s:488] wad_ssl_port_inline_ips_init :2665 wsp=0x7fa5051028/10 inline-ips disabled
      [I][p:276][s:488] wad_ssl_port__open :23864 wsp=0x7fa5051028/10 SSL-port open succ type=10 port=0x7fa5058568 vd=0 svr=172.31.1.1:443: succ
      [I][p:276][s:488] wad_ssl_port_run :23919 sp=0x7fa5051028/10 state=1
      [I][p:276] wad_unix_stream_flush_data :650 WAD unix stream stream 0x7fa5c1f048 write (1,4)
      [I][p:276][s:488] wad_tcp_port_on_event :1903 start processing tcp event=0x1 events=0x1 fd=62 n_out_block=0 state=2 close/shut=0/0 n_out_block=0
      [I][p:276][s:488] wad_tcp_port_on_read :1779 sock 62 read (0,4072)
      [I][p:276][s:488] wad_ssl_sock_port_exec_up_forward_txn:18919 sp=0x7fa5051028/10 forwarded len=193/193
      [I][p:276][s:488] wad_vs_ssl_port_caps_c2p_on_client_hello:11217 684:ZTNA-LAB: wsp(0x7fa5051028) handshake recv ClientHello record 3.1 client 3.3 supported 3.3
      [I][p:276][s:488] wad_elliptic_curve :3354 found elliptic curve 25
      [I][p:276][s:488] wad_vs_ssl_port_client_hello_full :10618 wsp 0x7fa5051028 ssl full handshake
      [I][p:276][s:488] wad_vs_ssl_port_caps_c2p_on_server_hello:12629 wsp 0x7fa5051028 handshake sent ServerHello 3.3
      [I][p:276][s:488] wad_vs_ssl_port_caps_cert_req :14077 684:ZTNA-LAB: requested certs from client. ca_store is empty.
      [I][p:276][s:488] wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 684:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.
      [I][p:276][s:488] wad_vs_ssl_c2p_cert_req_add_trust_issuers:14032 684:ZTNA-LAB: cas->x509 is (nil) or ca list is empty.
      [I][p:276][s:488] wad_vs_ssl_access_proxy_on_clt_certs:13476 684:ZTNA-LAB: received certs from the client.
      [I][p:276][s:488] __wad_ssl_cert_open_cert :641 https server uses key_len 2048
      [I][p:276][s:488] wad_vs_ssl_access_proxy_on_clt_certs:13508 684:ZTNA-LAB: Empty EMS CAs!
      [W][p:276][s:488] wad_ssl_clt_cert_chk_user_ca :13390 vd=root not configured user ca
      [W][p:276][s:488] wad_vs_ssl_access_proxy_on_clt_certs:13542 684:ZTNA-LAB: Cert auth failed. status=9
      [I][p:276][s:488] wad_ssl_cert_parse_cert :1448 EMS SN=FCTEMS8824011503
      [I][p:276][s:488] wad_ssl_cert_parse_cert :1453 Tenant ID=00000000000000000000000000000000
      [I][p:276][s:488] wad_ssl_cert_get_sn :1364 ci(0x7fa50f5f40): sn=03F6813097089958ECC2496BBBDD1917E0A09131
      [I][p:276][s:488] wad_vs_log_clt_cert_failure :98 684:ZTNA-LAB: Traffic denied because cert auth failed, cert-cn:F3AB0220E981419CAA9A13CE4811E3F9, cert-issuer:FCTEMS8824011503, cert-status:untrusted fail-reason:(null)
      [I][p:276] wad_vs_ssl_port_caps_c2p_on_handshake_done:12075 wsp 0x7fa5051028 type 10 handshake done
      [I][p:276] wad_vs_http_make :5500 make vs http session
      [I][p:276] wad_http_srv_selector_multiplex_make:2026 make multiplex server selector.
      [I][p:276] wad_http_full_ses_make :15795 make ok session=0x7fa8219c80 server=(nil).
      [I][p:276] wad_vs_ssl_port_c2p_issue_ticket :11983 684:ZTNA-LAB: wsp(0x7fa5051028) can't offer ticket to client.
      [I][p:276][s:488][r:33554433] wad_dump_http_request :3066 hreq=0x7fa6c40048 Received request from client: 172.31.1.2:61586
      
      GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1
      Host: 172.31.1.1
      User-Agent: Forticlient
      Accept: /
      Connection: Upgrade
      Cookie:
      Upgrade: tcp-forwarding/1.0
      X-Fos-Auth:
      
      [I][p:276][s:488][r:33554433] wad_http_parse_host :1941 host=[10]172.31.1.1
      [I][p:276][s:488][r:33554433] wad_http_str_canonicalize :2466 enc=0 path=/tcp len=4 changes=0
      [I][p:276][s:488][r:33554433] wad_http_str_canonicalize :2468 end=4 path=address=win-server.ad.labdomain.com&port=3389&tls=0 len=51 changes=0
      [I][p:276][s:488][r:33554433] wad_http_req_detect_special :14378 captive_portal detected: false, preflight=(null)
      [I][p:276][s:488][r:33554433] __wad_http_build_replmsg_resp :951 Generating replacement message. repmsg_id 88
      [I][p:276][s:488][r:33554433] wad_dump_fwd_http_resp :3087 hreq=0x7fa6c40048 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: 35174

      We can see a few failure messages here:

      [I][p:276][s:488]        wad_vs_ssl_access_proxy_on_clt_certs:13508 684:ZTNA-LAB: Empty EMS CAs!
      [W][p:276][s:488]        wad_ssl_clt_cert_chk_user_ca      :13390 vd=root not configured user ca
      [W][p:276][s:488]        wad_vs_ssl_access_proxy_on_clt_certs:13542 684:ZTNA-LAB: Cert auth failed. status=9
      [I][p:276][s:488]        wad_ssl_cert_parse_cert           :1448  EMS SN=FCTEMS8824011503
      [I][p:276][s:488]        wad_ssl_cert_parse_cert           :1453  Tenant ID=00000000000000000000000000000000
      [I][p:276][s:488]        wad_ssl_cert_get_sn               :1364  ci(0x7fa50f5f40): sn=03F6813097089958ECC2496BBBDD1917E0A09131
      [I][p:276][s:488]        wad_vs_log_clt_cert_failure       :98    684:ZTNA-LAB: Traffic denied because cert auth failed, cert-cn:F3AB0220E981419CAA9A13CE4811E3F9, cert-issuer:FCTEMS8824011503, cert-status:untrusted  fail-reason:(null)

      And a 403 Forbidden error gets returned:

      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: 35174

      In response to the 403, FortiClient displays a message on its internal browser that says the certificate is invalid, which isn’t strictly true, since in this case, it can’t be verified.

      Error: No EMS connection

      This HTML is built by FortiTCS, along with some other error messages.

      FortiClient certificate validation failure

      [2025-12-04 18:33:57.9286264] [fortitcs] Accept success
      [2025-12-04 18:33:57.9286556] [fortitcs] raw socket: 1716
      [2025-12-04 18:33:57.9286778] [fortitcs] status: <nil> recordsSize: 926
      [2025-12-04 18:33:57.9286847] [fortitcs] redirectContextSize: 260
      [2025-12-04 18:33:57.9286928] [fortitcs] contextSize: 260
      [2025-12-04 18:33:57.9287007] [fortitcs] family: 2 port: 3389 ip: 10.235.0.2 pid: 11896
      [2025-12-04 18:33:57.9287344] [fortitcs] forward ip: 10.235.0.2 port: 3389
      [2025-12-04 18:33:57.9287713] [fortitcs] FindFQDNFromDoh: IP=10.235.0.2 Port=3389 FQDN=win-server.ad.labdomain.com
      [2025-12-04 18:33:57.9287848] [fortitcs] UpdateDnsRedirectEntry: Ip=183173122 Port=3389 RealIp=0 Fqdn=win-server.ad.labdomain.com Flag=0
      [2025-12-04 18:33:57.9288140] [fortitcs] UpdateFQDNIpAndPort: get real_ip: 10.235.0.2fqdn: win-server.ad.labdomain.com
      [2025-12-04 18:33:57.9289428] [fortitcs] Request is not for PAM Agent. Ip: 10.235.0.2, Port: 3389, FQDN: win-server.ad.labdomain.com
      [2025-12-04 18:33:57.9289660] [fortitcs] According to PID 11896 Get session ID: 5
      [2025-12-04 18:33:57.9289971] [fortitcs] disallow_invalid_server_certificate: 0
      [2025-12-04 18:33:57.9296716] [fortitcs] gateway ip: 172.31.1.1 port: 443encryption: 0 cur_thread: 11364
      [2025-12-04 18:33:57.9666418] [fortitcs] nSession(5)
      [2025-12-04 18:33:57.9669035] [fortitcs] ImpersonateLoggedOnUser success!  cur_thread = 11364
      [2025-12-04 18:33:57.9684843] [fortitcs] trace reinit -> pid: 10132
      [2025-12-04 18:33:57.9924492] [fortitcs] handshake success!
      [2025-12-04 18:33:57.9925337] [fortitcs] SAML address: https://172.31.1.1:443/tcp?address=win-server.ad.labdomain.com&port=3389&tls=0
      [2025-12-04 18:33:57.9925562] [fortitcs] Cache address: https://172.31.1.1:443
      [2025-12-04 18:33:57.9926872] [fortitcs debug] GetSAMLAuth: no token found for gateway: 172.31.1.1:443
      [2025-12-04 18:33:57.9927247] [fortitcs debug] GetFormAuth: no token found for gateway: 172.31.1.1:443
      [2025-12-04 18:33:57.9929090] [fortitcs] Request: GET /tcp?address=win-server.ad.labdomain.com&port=3389&tls=0 HTTP/1.1
      Host: 172.31.1.1
      Accept: */*
      Authorization: ******
      Cookie: ******
      User-Agent: Forticlient
      X-Fos-Auth: 
      
      
      [2025-12-04 18:33:57.9930282] [fortitcs] Send tunnel cmd Success
      [2025-12-04 18:33:58.0005314] [fortitcs] response code: 403 html Reply: <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <meta http-equiv="X-UA-Compatible" content="IE=8; IE=EDGE">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <style type="text/css">
                  body {
                      height: 100%;
                      font-family: Helvetica, Arial, sans-serif;
                      color: #6a6a6a;
                      margin: 0;
                      display: flex;
                      align-items: center;
                      justify-content: center;
                  }
                  input[type=date], input[type=email], input[type=number], input[type=password], input[type=search], input[type=tel], input[type=text], input[type=time], input[type=url], select, textarea {
                      color: #262626;
                      vertical-align: baseline;
                      margin: .2em;
                      border-style: solid;
                      border-width: 1px;
                      border-color: #a9a9a9;
                      background-color: #fff;
                      box-sizing: border-box;
                      padding: 2px .5em;
                      appearance: none;
                      border-radius: 0;
                  }
                  input:focus {
                      border-color: #646464;
                      box-shadow: 0 0 1px 0 #a2a2a2;
                      outline: 0;
                  }
                  button {
                      padding: .5em 1em;
                      border: 1px solid;
                      border-radius: 3px;
                      min-width: 6em;
                      font-weight: 400;
                      font-size: .8em;
                      cursor: pointer;
                  }
                  button.primary {
                      color: #fff;
                      background-color: rgb(47, 113, 178);
                      border-color: rgb(34, 103, 173);
                  }
                  .message-container {
                      height: 500px;
                      width: 500px;
                      padding: 0;
                      margin: 10px;
                  }
                  .logo {
                      background: url('data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAA...') no-repeat left center;
                      height: 267px;
                      object-fit: contain;
                  }
                  table {
                      background-color: #fff;
                      border-spacing: 0;
                      margin: 1em;
                  }
                  table > tbody > tr > td:first-of-type:not([colspan]) {
                      color: rgba(0,0,0,.5);
                  }
                  table > tbody > tr > td:first-of-type {
                      vertical-align: top;
                  }
                  table > tbody > tr > td {
                      padding: .3em .3em;
                  }
                  .field {
                      display: table-row;
                  }
                  .field > :first-child {
                      display: table-cell;
                      width: 20%;
                  }
                  .field.single > :first-child {
                      display: inline;
                  }
                  .field > :not(:first-child) {
                      width: auto;
                      max-width: 100%;
                      display: inline-flex;
                      align-items: baseline;
                      virtical-align: top;
                      box-sizing: border-box;
                      margin: .3em;
                  }
                  .field > :not(:first-child) > input {
                      width: 230px;
                  }
                  .form-footer {
                      display: inline-flex;
                      justify-content: flex-start;
                  }
                  .form-footer > * {
                      margin: 1em;
                  }
                  .text-scrollable {
                      overflow: auto;
                      height: 150px;
                      border: 1px solid rgb(200, 200, 200);
                      padding: 5px;
                      font-size: 1em;
                  }
                  .text-centered {
                      text-align: center;
                  }
                  .text-container {
                      margin: 1em 1.5em;
                  }
                  .flex-container {
                      display: flex;
                  }
                  .flex-container.column {
                      flex-direction: column;
                  }
                  .newline-linebreak {
                      white-space: pre-line;
                  }
              </style>
              <title>ZTNA Access Denied</title>
          </head>
          <body><div class="message-container">
          <div class="logo"></div>
          <h1>Invalid ZTNA Certificate</h1>
          <table><tbody>
              <tr>
                  <td><b>Error Code:</b></td>
                  <td>001</td>
              </tr>
              <tr>
                  <td><b>Error Message:</b></td>
                  <td>The page you requested has been blocked because the ZTNA certificate is invalid.</td>
              </tr>
              <tr>
                  <td><b>Certificate Information:</b></td>
                  <td>Serial number: 03F6813097089958ECC2496BBBDD1917E0A09131.</td>
              </tr>
              <tr>
                  <td><b>Request Time:</b></td>
                  <td>1764869574; 2025-12-04 18:32:54 CET</td>
              </tr>
          </tbody></table>
      </div></body>
      </html>
      
      
      [2025-12-04 18:33:58.0015387] [fortitcs] WriteFile ztna_error.html success!
      [2025-12-04 18:33:58.0201953] [fortitcs] resp: &{200 OK 200 HTTP/1.1 1 1 map[Content-Type:[text/html; charset=utf-8] Date:[Thu, 04 Dec 2025 17:33:58 GMT]] 0xc000086740 -1 [chunked] false false map[] 0xc0000b66c0 <nil>}
      [2025-12-04 18:33:58.0203948] [fortitcs] PostAlertGui: Gui opened!
      [2025-12-04 18:33:58.0204292] [fortitcs] PostAlertGui: {"Id":"ZTNA_ALERT","Url":"http://127.0.0.1:49476"}
      [2025-12-04 18:33:58.0204795] [fortitcs] Sent: 50
      [2025-12-04 18:33:58.0204922] [fortitcs] OpenUrl: <nil>
      [2025-12-04 18:33:58.0205919] [fortitcs debug] 16 bytes data length=16
      00000000   53 52 6E 64 36 32 42 C3 B6 54 63 51 45 30 72 4E   SRnd62B..TcQE0rN
      [2025-12-04 18:33:58.0206035] [fortitcs] Sent: 16 bytes to confirm tunnel created
      [2025-12-04 18:33:58.0206129] [fortitcs] CallUpdateStatus call! mod=4 status=0 msgType=0
      [2025-12-04 18:33:58.0219855] [fortitcs] procUpdateStatus: mod=4 status=0 msgType=0 msg.Extra= ret = 1
      [2025-12-04 18:33:58.0221182] [fortitcs] Forward win-server.ad.labdomain.com:: local: 127.0.0.1:61022 -> remote: 127.0.0.1:61585proxy: 172.31.1.2:61586 -> remote: 172.31.1.1:443
      [2025-12-04 18:33:58.0228141] [fortitcs error] CopyNetBuffer error: read tcp 127.0.0.1:61022->127.0.0.1:61585: use of closed network connection

      The impact of a revoked certificate

      Revoke client certificate

      If a certificate gets revoked via EMS, three things happen:

      1. After a sync, FortiClient deletes the certificate from the user store on the endpoint.
      2. The FortiGate gets an update of the endpoint, but the certificate isn’t mentioned in it (this is the same update that was shown further above).
      3. The certificate gets re-enrolled on the endpoint after a few syncs, so this isn’t the end of everything.

      But what if the endpoint doesn’t have a connection to EMS, so it never gets the message to delete the certificate? This leads to a situation where a technically revoked certificate gets used to access a service that requires a valid certificate, so what does the FortiGate say here?

      Well, EMS does not provide a Certificate Revocation List (CRL), and without that, the FortiGate verifies the certificate against the EMS ZTNA root CA certificate, and since there can’t be a problem there, the ZTNA connection will work. This means that a revoked certificate does not mean that a client can’t access ZTNA resources.

      This behaviour sounds like a New Feature Request (NFR) to me. Maybe we will get a CRL in the future.

      Wrapping up

      After all this, there is still a lot I left on the floor, because this is just TCP forwarding. I did not go into ZTNA tags, HTTP(S) access proxies or SaaS forwarding, for example. I will leave these things for another day, because this article is already long enough. I wanted to start with TCP forwarding, because it’s the most important part in Fortinet’s ZTNA strategy, and I think it’s the best way to start with understanding the other parts.

      I hope that this entire thing was interesting and hopefully helpful to you, dear reader.