Tag: quicktips

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

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