Tag: ztna

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