Dear reader, I have another installment of the quick tips series. This time the topics are FQDN-based split tunneling for IPsec with FortiClient and how to use the URL access feature of a FortiGate Web Application Firewall (WAF) profile.
FQDNs are not supported in split-tunnel destinations. If FQDNs have been configured in the split-tunnel address group, it cannot be applied in the (set ipv4-split-include) config of the dialup IPsec.
That won’t stop us, however.
In order to make this happen, we can use EMS’ application-based split tunneling feature while editing a tunnel. In there, you can include or exclude Cloud and Video Streaming Applications, but also Domains, and that last point is the one that is interesting, because you can use FQDNs here.
In this case, I am adding the FQDN of this blog, blog.guenay.at, to the configuration.
The VPN configuration of a FortiGate doesn’t require any special configuration for this to work, and I have configured a regular PSK VPN with split tunneling, that routes 192.168.0.0/16 into the tunnel.
FortiGate VPN configuration
config vpn ipsec phase1-interface
edit "PSK-VPN"
set type dynamic
set interface "wan1"
set ike-version 2
set keylife 28800
set peertype any
set net-device disable
set mode-cfg enable
set ipv4-dns-server1 192.168.1.169
set proposal aes256gcm-prfsha512
set dhgrp 31
set eap enable
set eap-identity send-request
set authusrgrp "VPN_USERS"
set client-resume enable
set client-resume-interval 600
set transport auto
set ipv4-start-ip 172.16.101.1
set ipv4-end-ip 172.16.101.20
set ipv4-split-include "BASE-RFC1918-192"
set psksecret ENC TaAvnptXQNmMUc/zjoy5F1CweIG8wOZUwliDjZCfvt8lr36rtJl1i95oOzgQjN22lPhP3tvZ0HOLy5XCQNtGvKO+piZ6I2wDKHVXe4Q787D5NNdrtPwonaDXZxrdyAZ5hl3r3am7zJMhWC5F5Pdz/ms5t5oew98CiSjzPUv/FHHYrxObTvi+IrxH+MW5VmUIa85ZEVlmMjY3dkVA
set dpd-retryinterval 5
next
end
config vpn ipsec phase2-interface
edit "PSK-VPN"
set phase1name "PSK-VPN"
set proposal aes256gcm
set dhgrp 31
set keepalive enable
set keylifeseconds 3600
next
end
A policy to allow the WAN-bound traffic from the VPN is, of course, also needed for our FQDN split tunneling to work.
Once the connection on FortiClient is up, we can see, in the log file of the VPN (in my case it is C:\Program Files\Fortinet\FortiClient\logs\trace\iked_ikev2_PSK-VPN7EB027B2_1.log), that split tunneling is active and per-application policies have been applied in include mode.
Lots of routes are being added, and this is reflected in the route table.
If you use the exclude mode in the EMS VPN configuration, you see this in the log, and the routes are added to the routing table using the regular interface with your normal default gateway. Funnily enough, you don’t see these routes being added in the log.
This method of FQDN-based split tunneling is a nice compromise when transitioning from SSL-VPN to IPsec. Don’t forget the Cloud Applications option in this, because it offers a few often-used services and acts like the other options.
FortiGate Web Application Firewall URL access
Due to a blog post about FortiClient EMS Let’s Encrypt Security that I wrote for my employer, I was told about the URL access feature that lives inside the WAF profile of a FortiGate, which I wasn’t aware of before. Outside of the CLI reference, I cannot find any documentation of this feature for a FortiGate, only for FortiWeb, but it is relatively self-explanatory. Still, let’s see how to configure it.
Note: The URL access feature is CLI-only, but you should still enable Web Application Firewall in Feature Visibility, because otherwise the option to assign the profile in a policy does not show up in the GUI. Also, you can do much the same with a web filter profile, which I do in the post above, so the WAF option is not the only method to achieve this; it just saves on another profile.
On the CLI, you can configure a basic URL access block as follows:
config waf profile
edit "WAF_URL-ACCESS"
config url-access
edit 1
set address "ubuntu-ws-1.ad.labdomain.com"
set action block
set log enable
config access-pattern
edit 1
set srcaddr "all"
set pattern ".*/login.html$"
set regex enable
next
end
next
end
next
end
Let’s go through it.
The address option determines the backend host that is being protected, and it is an actual address object. I have tested it with the types ipmask and fqdn, and both worked here, assuming your DNS resolution is correct for the FQDN option.
action has the block option, which does exactly that, bypass, which allows the connection and skips all other WAF scanning, and permit, which allows the connection and continues with WAF scanning.
log is for logging; who would have thought.
In the access-pattern entry itself, we can set the srcaddr option, which determines to which source address this should apply, so we can target only specific addresses. Again, this is an actual address object.
Enabling regex does that, and because I prefer regex-matching over everything else, I have it enabled. Disabling it probably does a simple match on the URL, but I didn’t test this because if regex is available, I will use only that.
The actual regex pattern is used in the pattern option. In my case, I am checking for every URL path that ends in /login.html.
That’s it on how to configure the feature.
Note that there is no implicit deny rule here, so if you don’t have an entry for a pattern, it is allowed. Entries are matched top down, so keep that in mind.
Assign the WAF profile in a policy, and if you perform deep inspection, either with an SSL/SSH profile or a virtual server, you can also control access in HTTPS traffic.
If I test the URL https://ubuntu-ws-1.ad.labdomain.com/login.html on my client, I get the block, as expected, and I see this in my FortiGate Web Application Firewall log.
Browsing to any other URL, like https://ubuntu-ws-1.ad.labdomain.com/index.html or just https://ubuntu-ws-1.ad.labdomain.com, does not lead to a block, because I never explicitly mentioned this in my WAF profile and I also don’t have a catch-all entry.
If you ever need to debug this feature, then you need to know that the WAF feature uses the wad process, and the category http is the important one.
If you want a configuration that allows a specific URL path and blocks all others, you can configure it like this:
config waf profile
edit "WAF_URL-ACCESS"
config url-access
edit 1
set address "ubuntu-ws-1.ad.labdomain.com"
set log enable
config access-pattern
edit 1
set srcaddr "all"
set pattern ".*/login.html$"
set regex enable
next
end
next
edit 2
set address "ubuntu-ws-1.ad.labdomain.com"
set action block
set log enable
config access-pattern
edit 1
set srcaddr "all"
set pattern ".*"
set regex enable
next
end
next
end
next
end
And that’s that feature explained.
Short and sweet and done
The configuration for both things in this quick tips installment isn’t anything to write home about, as it should be for this series, but I haven’t seen much on either of these things, so hopefully this helps at least you, dear reader.
Certificates are ubiquitous in our daily life, whether we realize it or not, and most people fall into one of two camps when it comes to them:
They love them because they make authentication and trust easy
They hate them because they can seem like black magic to some and create unnecessary work due to having to provision and replace them.
Both sides have valid points, but we can all agree that we need them, be it for securing web traffic, enabling strong authentication or to get rid of those pesky certificate warnings on our GUIs.
FortiClient EMS has a few options to interact with certificates, so, dear reader, let’s have a look to see how EMS can help us with managing certificates.
FortiClient EMS does two things out of the box when it comes to certificates:
It acts as a certificate authority (CA), using a self-signed ZTNA CA to…
Provision managed FortiClients with a user certificate for ZTNA purposes
These two things open up quite a few possibilities because the user certificates aren’t anything special. They are regular certificates for client authentication, and since EMS is already distributing the certificates, we might as well put them to good use.
We could download the default ZTNA public CA certificate and use that in our infrastructure to authenticate users, but this has some caveats, like:
It’s a self-signed CA that has no connection to anything, which can be seen as a problem by some people
We cannot issue certificates for other services, like webservers, using this CA, because the private key lies with EMS, and EMS only gives certificates to managed FortiClients
Corollary, this cannot scale because I only have this one CA, and I can’t create sub/intermediate CAs
To solve this, I can create a real public key infrastructure (PKI) with a root CA, several sub CAs, including an EMS CA, hand out certificates for all purposes, and I got a nice chain of trust, and that is what I want to show in this post.
Note: None of the use cases I cover in this post requires running your own PKI. You can do everything in some form using the default certificates and CAs you get with FortiClient EMS and a FortiGate, and without a root CA. I am doing this more to show that you can take it further.
With the preamble out of the way, let’s begin with the foundation.
The certificate structure
To get started, I will be using FortiAuthenticator as a root CA to create two sub CAs:
The EMS_SUBCA, which replaces the default EMS ZTNA CA and signs the certificates for managed FortiClients
The FGT_SUBCA, which will be used by the FortiGate for SSL/TLS inspection
Very important: I will use EMS_SUBCA in this post to refer to the sub CA that EMS uses, but the CN for this CA must be the serial number of your EMS server. This is a requirement for everything to function correctly.
Additionally, I create one certificate with the server authentication extended keyusage (EKU), so the FortiGate can later authenticate VPN users.
Note: I want to mention that the PKI topology I use for this post is not best practice. In a production environment, you should always start with a root CA that gets taken offline after issuing a sub CA, and that sub CA then issues all other certificates. To make it a bit easier on myself, I am only using an online root CA.
How do we get to our certificates?
FortiAuthenticator certificate management
With FortiAuthenticator, we can do basically everything, so if we just head to Certificate Management -> Certificate Authorities -> Local CAs, we can create the root CA using the provided Root CA type, and after providing an ID and a CN, we got the first part done. You can, of course, change the settings for the CA as you wish.
With the root CA in hand, we can create the EMS and FortiGate sub CAs from the same menu using the Intermediate CA type. Again, it’s very important that the Common Name is the serial number of your EMS server. I only show the EMS sub CA here.
After creating, make sure to export the key and cert for the two CAs and keep them safe for now. Also, download the public keys using the Export Certificate button from all three CAs.
We need a certificate for the FortiGate later, and I will do that using the Simple Certificate Enrollment Protocol (SCEP) on FortiAuthenticator. Don’t forget to enable this service on the interface on which the FortiGate contacts FortiAuthenticator. If you use HTTPS, make sure that FortiAuthenticator’s certificate is trusted by the FortiGate.
Then head over to the SCEP Enrollment Requests menu and create a regular request for the FortiGate, signed by the root CA, with a fitting CN, and make sure that the Extended Key Usage of Server Authentication is present. All settings can, of course, be changed as you wish; I am just presenting a basic configuration.
Note the password after creating the request, because we need it right now, because we get to…
FortiGate certificate management
Be sure to enable Certificates in Feature Visibility first.
On the FortiGate, first import the FortiAuthenticator root CA and EMS sub CA certificates. You can do this either using SCEP or via a file upload.
Then create a certificate signing request (CSR), making sure that the name and domain name match the CN you presented in the SCEP enrollment request (fgt-vpncert for me) and that Online SCEP is used as the Enrollment Method. The CA Server URL is in the format of http://FORTIAUTHENTICATOR-IP-FQDN/app/cert/scep, with HTTPS if you want, and your own IP or FQDN, of course.
The Challenge Password is the one you got earlier.
After a few seconds, the status of the CSR should get signed by FortiAuthenticator and return a Valid status.
While you’re here, also import the sub CA certificate for the FortiGate. Remember that this is considered a Certificate, not a CA certificate, when you click on the Create/Import button.
That’s it for now, so let’s turn to EMS.
EMS certificate management
On EMS, we have to do two things:
Change the ZTNA CA certificate to our created one
Make sure clients get the FortiGate’s sub CA certificate
Point one is done by going to System Settings -> EMS Settings and clicking on the cog icon next to EMS CA Certificate (ZTNA) and uploading the new CA certificate. EMS will automatically hand out certificates to managed FortiClients using this certificate.
For point two, we first need to upload the CA certificate under Endpoint Policy & Components -> CA Certificates.
With the uploaded certificate, go to your System Settings profile of choice, make sure that under Other, the Install CA Certificate on Client setting is enabled and pick the CA certificates you want to push to managed FortiClients. Note that the certificate used as the EMS ZTNA CA gets installed without having to do anything.
After these two steps and waiting for a sync, we can see that a connected client now has a user certificate from the new EMS sub CA and trusts the CA certificates that are in the System Settings profile.
Note: If you have an on-net endpoint, make sure that whatever you configure in the ZTNA profile does not interfere with normal operations. Imagine that you have a ZTNA destination that uses the FQDN of an internal server. Due to the way ZTNA works, FortiClient will intercept the DNS request, return a bogus IP, and try to ZTNA this connection, which probably means the connection will fail. You probably want to have a disabled ZTNA profile attached to the endpoint’s policy when it is on-net.
With all our certificates in order, we can actually use them, and why not begin with VPN?
For VPN
Note: Since EMS is provisioning user certificates, the certificate can only be used for regular VPN authentication with a logged-in user and pre-logon/start before logon (SBL) is not possible, since only machine certificates can be used in a pre-logon state.
Starting with the FortiGate, we first need a PKI user that represents the EMS sub CA, so create this PKI user.
config user peer
edit "EMS-SUBCA"
set ca "EMS_SUBCA"
next
end
The VPN configuration is relatively simple; just make sure that the previously created server authentication certificate is used and only accept client certificates from the EMS sub CA. All other settings, like IP assignment, encryption, DH, etc., are up to you.
Don’t forget to create a policy using the VPN interface; otherwise, clients can’t connect.
FortiGate certificate authentication VPN
config vpn ipsec phase1-interface
edit "CERT-VPN"
set type dynamic
set interface "port1"
set ike-version 2
set keylife 28800
set authmethod signature
set net-device disable
set mode-cfg enable
set ipv4-dns-server1 192.168.1.240
set proposal aes256gcm-prfsha512
set dhgrp 21
set client-resume enable
set client-resume-interval 300
set transport auto
set certificate "fgt-vpncert"
set peer "EMS-SUBCA"
set ipv4-start-ip 172.16.100.1
set ipv4-end-ip 172.16.100.20
set ipv4-split-include "BASE-RFC1918-192"
next
end
config vpn ipsec phase2-interface
edit "CERT-VPN"
set phase1name "CERT-VPN"
set proposal aes256-sha256
set dhgrp 21
set keepalive enable
set keylifeseconds 3600
next
end
config firewall policy
edit 0
set name "CERT-VPN"
set srcintf "CERT-VPN"
set dstintf "LAN"
set action accept
set srcaddr "all"
set dstaddr "all"
set schedule "always"
set service "ALL"
set logtraffic all
set nat enable
next
end
On EMS, we need a matching VPN configuration in the Remote Access profile. There are a few things to note:
The basic idea is that the certificate for the VPN connection is filtered, so only a specific one appears, which makes the user experience better because you don’t have to worry about which certificate gets used by a user. In my configuration, I am doing a regex match on the common name and a simple match on the issuer, which is enough to get what I need.
The FortiClient XML reference has some more information on how to match and what options are available.
I am configuring the session resumption feature for this tunnel, by the way.
Both the FortiGate VPN configuration and the Remote Access XML profile EMS are in the GitHub repository linked above.
A bit of advice when you first set this up: Create a personal VPN on a FortiClient, recreate the necessary VPN settings, and make sure that works first, just so you know that certificate authentication works. Once that is done, test the VPN configuration you get from EMS. Especially with the certificate filtering, it’s easier to do this bit by bit and not start with the EMS-provided VPN tunnel.
With the FortiGate and EMS having the correct VPN configuration, we can test the connection, and it should work. We can see that the CN matches the FortiClient ID we see on EMS.
If we look at the debugs of ike and fnbamd, we can see the entire process (some parts of the debugs are removed):
FortiGate VPN debugs
diagnose debug application ike -1
diagnose debug application fnbamd -1
diagnose debug enable
ike V=root:0: comes 192.168.100.2:51056->192.168.100.1:4500,ifindex=5,vrf=0,len=486....
ike V=root:0: IKEv2 exchange=SA_INIT id=5a88db4c560f15da/0000000000000000 len=482
ike 0: in 5A88DB4C560F15DA00000000000000002120220800000000000001E22200006C02000024010100030300000C01000014800E0100030000080400001500000008020000070000004402010007030000080300000C0300000C0100000C800E0100030000080400001503000008020000050300000802000006030000080200000700000008020000022800008C0015000000C99A8B88C2DDEE9963B6D3B52C40B57927668DC1C786D1981C35B1AD5638F56CC380D5280C6905FCF86C1FCBA01C8677249BA6C19290C86DAD24694C3B63F61D750102AB27393AB8D9D29E48D099E36A6BEF6BB06895B64DA96A2E67AF9D8481DCED49F36FA2C2B1FA09C117751D029699749DD359090740383B7EFFE6BCCFA47BE19A2B00002441A822057CFB67B96D2DBA82EE1C490EEA106F34B30490242EE4229FBABB92122B00001C4C53427B6D465D1B337BB755A37A7FEF706B9EBAF67F00002B00001CB4F01CA951E9DA8D0BAFBBD34AD3044E906B9EBAF67F00002900001CC1DC4350476B98A429B91781914CA43E202A8801FD010000290000080000F118290000080000402E2900001C0000400468F9B3E80FB82683054FF61FC4BFF1639CBAD7672900001C0000400540DAB0491BAC2CEB4C65D6506300387954912FAE0000000E0000402F000200030004
ike V=root:0:5a88db4c560f15da/0000000000000000:23: responder received SA_INIT msg
ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID forticlient connect license 4C53427B6D465D1B337BB755A37A7FEF706B9EBAF67F0000
ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID Fortinet Endpoint Control B4F01CA951E9DA8D0BAFBBD34AD3044E906B9EBAF67F0000
ike V=root:0:5a88db4c560f15da/0000000000000000:23: VID Forticlient EAP Extension C1DC4350476B98A429B91781914CA43E202A8801FD010000
ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type CLIENT_RESUME
ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type FRAGMENTATION_SUPPORTED
ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type NAT_DETECTION_SOURCE_IP
ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type NAT_DETECTION_DESTINATION_IP
ike V=root:0:5a88db4c560f15da/0000000000000000:23: received notify type SIGNATURE_HASH_ALGORITHMS
ike V=root:0:5a88db4c560f15da/0000000000000000:23: incoming proposal:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 1:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: protocol = IKEv2:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: encapsulation = IKEv2/none
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=ENCR, val=AES_GCM_16 (key_len = 256)
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA2_512
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=DH_GROUP, val=ECP521.
ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 2:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: protocol = IKEv2:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: encapsulation = IKEv2/none
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=ENCR, val=AES_CBC (key_len = 256)
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=INTEGR, val=AUTH_HMAC_SHA2_256_128
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA2_512
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA2_384
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA2_256
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=DH_GROUP, val=ECP521.ike V=root:0: cache rebuild start
ike V=root:0:CERT-VPN: cached as wildcard, user peer 'EMS-SUBCA'
ike V=root:0:CERT-VPN: cached as dynamic, user peer 'EMS-SUBCA' subj='' cn=''
ike V=root:0: cache rebuild done
ike V=root:0:5a88db4c560f15da/0000000000000000:23: matched proposal id 1
ike V=root:0:5a88db4c560f15da/0000000000000000:23: proposal id = 1:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: protocol = IKEv2:
ike V=root:0:5a88db4c560f15da/0000000000000000:23: encapsulation = IKEv2/none
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=ENCR, val=AES_GCM_16 (key_len = 256)
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=INTEGR, val=NONE
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=PRF, val=PRF_HMAC_SHA2_512
ike V=root:0:5a88db4c560f15da/0000000000000000:23: type=DH_GROUP, val=ECP521.
ike V=root:0:5a88db4c560f15da/0000000000000000:23: lifetime=28800
ike V=root:0:5a88db4c560f15da/0000000000000000:23: SA proposal chosen, matched gateway CERT-VPN
ike V=root:0:CERT-VPN:23: sending CERTREQ payload (len=21)
ike V=root:0:CERT-VPN:23: certreq[0]: '85936D8D9D330144B1FBE59B30BE1EF36474F48F'
ike V=root:0:CERT-VPN:23: received peer identifier DER_ASN1_DN 'C = CA, ST = BC, L = Burnaby, O = Fortinet, CN = F3AB0220E981419CAA9A13CE4811E3F9'ike V=root:0:CERT-VPN:23: match gw peer ID by FNBAMike V=root:0:CERT-VPN:23: Validating X.509 certificate
ike V=root:0:CERT-VPN:23: peer cert, subject='F3AB0220E981419CAA9A13CE4811E3F9', issuer='EMS_SUBCA'
ike V=root:0:CERT-VPN:23: peer ID verified
ike V=root:0:CERT-VPN:23: building fnbam peer candidate list
ike V=root:0:CERT-VPN:23: FNBAM_GROUP_NAME candidate 'EMS-SUBCA'
ike V=root:0:CERT-VPN:23: certificate validation pending
[323] fnbamd_chain_build-Chain discovery, opt 0x13, cur total 1
[341] fnbamd_chain_build-Following depth 0
[376] fnbamd_chain_build-Extend chain by system trust store. (good: 'EMS-SUBCA')
[341] fnbamd_chain_build-Following depth 1
[376] fnbamd_chain_build-Extend chain by system trust store. (good: 'FAC_ROOT')
[341] fnbamd_chain_build-Following depth 2
[355] fnbamd_chain_build-Self-sign detected.
[109] __cert_chg_st- 'Init' -> 'Validation'
[1025] __cert_verify-req_id=40737833275404
[1026] __cert_verify-Chain is complete.
[540] fnbamd_cert_verify-Chain number:3
[554] fnbamd_cert_verify-Following cert chain depth 0
[627] fnbamd_cert_verify-Issuer found: EMS-SUBCA (SSL_DPI opt 1)
[554] fnbamd_cert_verify-Following cert chain depth 1
[627] fnbamd_cert_verify-Issuer found: FAC_ROOT (SSL_DPI opt 1)
[554] fnbamd_cert_verify-Following cert chain depth 2
[1057] __cert_verify-peer_info.no_ocsp_query:0 cert->status:640.
[733] fnbamd_cert_check_group_list-checking group with name 'EMS-SUBCA'
[546] __check_add_peer-check 'EMS-SUBCA'
[422] peer_subject_cn_check-Cert subject 'C = CA, ST = BC, L = Burnaby, O = Fortinet, CN = F3AB0220E981419CAA9A13CE4811E3F9'
[553] __check_add_peer-'EMS-SUBCA' check ret:good
[668] __peer_user_clear_unmatched-Clear all user(s) other than 'EMS-SUBCA'
[689] __peer_user_clear_unmatched-
[202] __get_default_ocsp_ctx-def_ocsp_ctx=(nil), no_ocsp_query=0, ocsp_enabled=0
[806] fnbamd_cert_check_group_list-Peer users
[809] fnbamd_cert_check_group_list- 'EMS-SUBCA' ('N/A','N/A','N/A')
[1069] __cert_verify_do_next-req_id=40737833275404
[109] __cert_chg_st- 'Validation' -> 'Done'
[1163] __cert_done-req_id=40737833275404
[1567] fnbamd_auth_session_done-Session done, id=40737833275404
[1209] __fnbamd_cert_auth_run-Exit, req_id=40737833275404
[1610] create_auth_cert_session-fnbamd_cert_auth_init returns 0, id=40737833275404
[1523] auth_cert_success-id=40737833275404
[1321] fnbamd_cert_auth_copy_cert_status-req_id=40737833275404
[1329] fnbamd_cert_auth_copy_cert_status-Matched peer user 'EMS-SUBCA'
[914] fnbamd_cert_check_matched_groups-checking group with name 'EMS-SUBCA', peer_ctx->peer_user->setting.name:EMS-SUBCA
[975] fnbamd_cert_check_matched_groups-matched
[1361] fnbamd_cert_auth_copy_cert_status-Leaf cert status is unchecked.
[1452] fnbamd_cert_auth_copy_cert_status-Cert st 2c0, req_id=40737833275404
[279] fnbamd_comm_send_result-Sending result 0 (nid 672) for req 40737833275404, len=2611
[1398] destroy_auth_cert_session-id=40737833275404
[1293] fnbamd_cert_auth_uninit-req_id=40737833275404
ike V=root:0:CERT-VPN:23: fnbam reply 'EMS-SUBCA'
ike V=root:0:CERT-VPN:23: fnbam matched peer 'EMS-SUBCA'
[1985] fnbamd_ldaps_destroy-
ike V=root:0:CERT-VPN:23: certificate validation succeeded
[1667] fnbamd_rads_destroy-
[140] fnbamd_peer_ctx_free-Freeing peer ctx 'EMS-SUBCA'
ike V=root:0:CERT-VPN:23: signature verification succeeded
ike V=root:0:CERT-VPN:23: auth verify done
ike V=root:0:CERT-VPN:23: responder AUTH continuation
ike V=root:0:CERT-VPN:23: authentication succeeded
ike V=root:0:CERT-VPN:23: processing notify type FORTICLIENT_CONNECT
ike V=root:0:CERT-VPN:23: received FCT data len = 326, data = 'VER=1
FCTVER=7.4.6.2001
UID=F3AB0220E981419CAA9A13CE4811E3F9
IP=192.168.100.2
MAC=a4-bb-6d-13-07-2b;24-41-8c-fb-b1-b6;24-41-8c-fb-b1-b7;26-41-8c-fb-b1-b6;
HOST=FCXLAB
USER=labuser
OSVER=Microsoft Windows 8.0 Professional Edition, 64-bit (build 9200)
REG_STATUS=0
EMSSN=FCTEMSSERIAL
EMSID=00000000000000000000000000000000
'
ike V=root:0:CERT-VPN:23: received FCT-UID : F3AB0220E981419CAA9A13CE4811E3F9
ike V=root:0:CERT-VPN:23: received EMS SN : FCTEMSSERIAL
ike V=root:0:CERT-VPN:23: received EMS tenant ID : 00000000000000000000000000000000
ike V=root:0:CERT-VPN:23: received FCT-HOST : FCXLAB
ike V=root:0:CERT-VPN:23: responder creating new child
ike V=root:0:CERT-VPN:23: mode-cfg type 1 request 0:''
ike V=root:0:CERT-VPN: mode-cfg allocate 172.16.100.1/0.0.0.0
ike V=root:0:CERT-VPN:23: mode-cfg using allocated IPv4 172.16.100.1
ike V=root:0:CERT-VPN:23:8: peer proposal:
ike V=root:0:CERT-VPN:23:8: TSi_0 0:0.0.0.0-255.255.255.255:0
ike V=root:0:CERT-VPN:23:8: TSr_0 0:0.0.0.0-255.255.255.255:0
ike V=root:0:CERT-VPN:23:CERT-VPN:8: comparing selectors
ike V=root:0:CERT-VPN:23:CERT-VPN:8: matched by rfc-rule-2
ike V=root:0:CERT-VPN:23:CERT-VPN:8: phase2 matched by subset
ike V=root:0:CERT-VPN:23:CERT-VPN:8: using mode-cfg override 0:172.16.100.1-172.16.100.1:0
ike V=root:0:CERT-VPN:23:CERT-VPN:8: accepted proposal:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: TSi_0 0:172.16.100.1-172.16.100.1:0
ike V=root:0:CERT-VPN:23:CERT-VPN:8: TSr_0 0:0.0.0.0-255.255.255.255:0
ike V=root:0:CERT-VPN:23:CERT-VPN:8: dialup
ike V=root:0:CERT-VPN:23:CERT-VPN:8: incoming child SA proposal:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 1:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: protocol = ESP:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: encapsulation = TUNNEL
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ENCR, val=AES_GCM_16 (key_len = 128)
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ESN, val=NO
ike V=root:0:CERT-VPN:23:CERT-VPN:8: PFS is disabled
ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 2:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: protocol = ESP:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: encapsulation = TUNNEL
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ENCR, val=AES_CBC (key_len = 256)
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=INTEGR, val=SHA256
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ESN, val=NO
ike V=root:0:CERT-VPN:23:CERT-VPN:8: PFS is disabled
ike V=root:0:CERT-VPN:23:CERT-VPN:8: matched proposal id 2
ike V=root:0:CERT-VPN:23:CERT-VPN:8: proposal id = 2:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: protocol = ESP:
ike V=root:0:CERT-VPN:23:CERT-VPN:8: encapsulation = TUNNEL
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ENCR, val=AES_CBC (key_len = 256)
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=INTEGR, val=SHA256
ike V=root:0:CERT-VPN:23:CERT-VPN:8: type=ESN, val=NO
ike V=root:0:CERT-VPN:23:CERT-VPN:8: PFS is disabled
ike V=root:0:CERT-VPN:23:CERT-VPN:8: lifetime=3600
ike V=root:0:CERT-VPN:23: responder preparing AUTH msg
ike V=root:0:CERT-VPN: adding new dynamic tunnel for 192.168.100.2:51056
ike V=root:0:CERT-VPN_0: tunnel created tun_id 172.16.100.1/::10.0.0.10 remote_location 0.0.0.0
ike V=root:0:CERT-VPN_0: added new dynamic tunnel for 192.168.100.2:51056
ike V=root:0:CERT-VPN_0:23: local cert, subject='fgt-vpncert', issuer='FAC-ROOT'
ike V=root:0:CERT-VPN_0:23: mode-cfg assigned (1) IPv4 address 172.16.100.1
ike V=root:0:CERT-VPN_0:23: mode-cfg assigned (2) IPv4 netmask 255.255.255.255
ike V=root:0:CERT-VPN_0:23: mode-cfg send (13) 0:192.168.0.0/255.255.0.0:0
ike V=root:0:CERT-VPN_0:23: mode-cfg send (3) IPv4 DNS(1) 192.168.1.240
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: IPsec SA selectors #src=1 #dst=1
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: src 0 7 0:0.0.0.0-255.255.255.255:0
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: dst 0 7 0:172.16.100.1-172.16.100.1:0
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: add dynamic IPsec SA selectors 683
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: added dynamic IPsec SA proxyids new 1 683
ike V=root:0:CERT-VPN:8: add route 172.16.100.1/255.255.255.255 gw 172.16.100.1 oif CERT-VPN(36) metric 15 priority 1
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: tunnel 1 of VDOM limit 0/0
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: add IPsec SA: SPIs=78e2bc59/8dc65e44
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: added IPsec SA: SPIs=78e2bc59/8dc65e44
ike V=root:0:CERT-VPN_0:23:CERT-VPN:8: sending SNMP tunnel UP trap
ike V=root:0:CERT-VPN_0: tunnel up event assigned address 172.16.100.1
ike V=root:0:CERT-VPN_0: sent tunnel-up message to EMS: (fct-uid=F3AB0220E981419CAA9A13CE4811E3F9, intf=CERT-VPN_0, addr=172.16.100.1, vdom=root)
We get the incoming connection with some FortiClient information as well as the IKE phase 1 proposal, so a gateway can be matched
FortiClient sends the user certificate during the connection attempt (received peer identifier) and the peer cert, the EMS sub CA, gets taken from the configuration and passed to the fnbamd process for certificate validation.
The certificate chain gets walked to find the possible issuers (Following cert chain)
The EMS sub CA certificate provides a match (__check_add_peer-'EMS-SUBCA' check ret:good)
The resulting CA gets returned to the ike process (fnbam reply 'EMS-SUBCA) and certificate validation is successful (certificate validation succeeded)
Regular IPsec phase 2 processes are done and we get a sending SNMP tunnel UP trap message, which we love to see
VPN is looking good, so next up is SSL/TLS inspection.
For SSL/TLS inspection
I will be referring to SSL/TLS inspection as Deep Packet Inspection (DPI) going forward.
When doing DPI, the client needs to trust the CA that is issuing the replaced certificate, and since we already did that in the EMS certificate management section, we only need to create an SSL/SSH inspection profile and configure firewall policies to use it.
Remember that you need at least one other security profile in a policy for the SSL/SSH inspection profile to do anything, which is why I am using an application control and IPS profile. Without an additional security profile, it’s like no security profile is being used.
If a client then, for example, browses to a website secured by HTTPS, we see that DPI does its thing, meaning the FortiGate replaces the certificate, and the connection is also trusted.
DPI done, so on to 802.1X.
A detour to configure FortiAuthenticator for EAP-TLS authentication with a FortiGate and FortiSwitch
Initially, I didn’t want to explain how to configure FortiAuthenticator as a RADIUS server for EAP-TLS authentication and point to the documentation, but I couldn’t find an up-to-date example of it, so let’s quickly go through it.
Make sure that at least RADIUS Auth is enabled on the FortiAuthenticator’s interface
If it doesn’t already exist, create a Local Services certificate for EAP authentication in Certificate Management -> End Entities -> Local Services
This certificate may not have multiple SANs, but one is fine
Assign the certificate as the EAP Server Certificate under Authentication -> RADIUS Service -> General
Create a RADIUS client for the device that will send RADIUS requests
For this post, it is only the FortiGate’s IP since I NAT the FortiSwitch communication towards FortiAuthenticator
You don’t need it for 802.1X authentication, but you might as well enable the requirement for the Message-Authenticator attribute and also do it on the Network Access Server (NAS). For MAC Authentication Bypass (MAB), you need it anyway, and the attribute doesn’t hurt
Create an Auth Profile with the Authentication type of Certificates and the EMS_SUBCA as a Trusted CA
Create a RADIUS policy with your RADIUS client, the Client Credentials as EAP-TLS, and pick your Authentication Profile
FortiAuthenticator RADIUS interface
FortiAuthenticator Local Services certificate
FortiAuthenticator EAP Server Certificate
FortiAuthenticator RADIUS client
FortiAuthenticator Authentication profile first step
FortiAuthenticator Authentication profile second step
FortiAuthenticator RADIUS policy first step
FortiAuthenticator RADIUS policy second step
The configuration on the FortiGate is the same as always:
Create a RADIUS server
Create a group with that RADIUS server as a remote member
Create a FortiSwitch Security Policy for 802.1X
You probably want MAC-based authentication so each client gets authenticated, not just the first one, like with port-based authentication
Assign the security policy to a port
Don’t forget to enable the Security Policy column
Create a firewall policy fromyour FortiLink interface to the RADIUS server that allows the RADIUS traffic
The FortiLink interface can only be assigned via the CLI
Optionally enable NAT on this policy if you want that, which I do
FortiAuthenticator RADIUS server
FortiAuthenticator user group
FortiSwitch security policy
FortiSwitch security policy assignment
FortiSwitch RADIUS policy
The 802.1X configuration the FortiGate uses is in the GitHub repository linked above.
With that completed, we can go back to the topic at hand.
Actually…
[Update 2026-06-07]: I have asked around a bit and got the feedback that the user certificate should not be removed if the client goes offline, which is also the experience I’ve had in the past. I have checked the release notes for EMS and FortiClient and couldn’t find anything that would hint at a change in behaviour or a bug, and I also updated FortiClient from 7.4.6 to 7.4.7, but this didn’t change anything. The user certificate continues to get removed shortly after I take the client offline. I will follow this topic, and hopefully I get to the bottom of it, because making 802.1X authentication possible in an easy manner would be great. In the meantime, you can test this behaviour yourself, and maybe you have a different experience. I can’t rule any configuration issues out on my side.
[Update 2026-06-08]: I have found the root cause of the behaviour where FortiClient deletes the user certificate. The reason is that when writing this post, I used the CN EMS_SUBCA for the EMS sub CA. FortiClient’s FortiESNAC process, however, has a certificate check that runs in some situations, like when the client goes offline, and this check inspects the issuing CA for the user certificate and if the CN does not match the serial number of EMS, the user certificate gets deleted. Practically, this means that if you create your custom CA, the CN needs to match the EMS serial number. I have put this information further up, so people start with the correct information.
We can see this behaviour in the FortiESNAC_1.log file (lots of lines in between are removed). I am presenting a case here where the serial number is not present in the CN.
FortiESNAC tries to find a certificate that has been issued by a CA with the EMS serial number as the CN, and if it doesn’t match, we see that the GetClientCertificate function fails:
Could not find the desired certificate FCTEMSSERIAL
FortiESNAC removes the unmatched certificate, which is the user certificate that was already deployed
A new ZTNA client certificate gets requested (Requesting new ZTNA client certificate: client certificate cannot be found on the system)
EMS cannot be contacted for this, because the client is offline (Could not contact the current server - backing up server address and trying other available servers)
With this information in mind, you can ignore the rest of this section, which I have marked for you.
IGNORE BLOCK STARTS HERE
I am so sorry for the detours, but it would be remiss of me not to mention the next part.
One thing rears its ugly head when it comes to 802.1X and using the EMS user certificates, which is the fact that FortiClient will delete the user certificate if it loses the EMS connection. I was able to narrow it down to the Online/Offline status displayed on FortiClient, and in my testing, this status change took about 10 seconds.
This behaviour leads to the problem that user authentication for 802.1X cannot really work because, by definition, you don’t have network access before authentication and you can’t get the Online status.
As a workaround, I (ab)used the guest VLAN function in the security policy. The idea is that a client fails, or doesn’t even attempt, the 802.1X authentication, falls back to MAB, which also fails, assuming you haven’t configured MAB, and arrives in the guest VLAN after some time. This VLAN allows a connection to EMS to get a certificate, and the client can complete the 802.1X authentication with this certificate.
You can, of course, configure MAB as a fallback.
You have to configure the following things for this:
Create a guest VLAN with all the necessary components, i.e. DHCP, DNS, etc.
Enable the Guest VLAN option in the FortiSwitch Security Policy
Set the Guest authentication delay to something reasonable
Enable MAC authentication bypass
Create a firewall policy allowing the necessary traffic
At a minimum, guests need to reach EMS over TCP/8013
Keep DNS in mind
Once the client gets the certificate, he can, at least on Windows, easily sign in using the provided pop-up.
FortiSwitch guest security policy
FortiGate guest policy
Missing certificate on Windows
Windows sign-in
This works for wired 802.1X. Wireless is another beast, and I got nothing here.
Sorry, not everything is a winner. Maybe someone can help me in this regard.
IGNORE BLOCK ENDS HERE
For 802.1X with EAP-TLS
Note: Much like with the VPN part, you cannot use the user certificate in a pre-logon scenario. You need a machine/computer certificate for that.
802.1X with EAP-TLS really is the thing you want to secure your network access in the physical space, and thanks to the work we already did, all the pieces exist to make it a reality.
FortiAuthenticator acts as the RADIUS/AAA server, the client has a user certificate and the CA certificates installed, and so we just need a layer 2 device to act as the NAS, and for me, this role is fulfilled by a FortiSwitch and a FortiAP.
How to configure 802.1X with EAP-TLS was already handled, so here are just some additional points:
In the RADIUS policy on FortiAuthenticator, only certificates issued by the EMS_SUBCA are accepted, because those are the certificates the client presents
If your client is validating the RADIUS server certificate, you have to make sure that it uses the correct CA certificate for this. In my case, the server certificate comes from the FAC_ROOT CA
The two points already mentioned are important if you are using Windows group policies for this, because you want/need to restrict both the server validation CA, as well as the issuing CA, for simple certificate selection
With a correctly configured client, we can check the 802.1X authentication state. First, wired, using the diagnose switch-controller switch-info 802.1X S424ENTFSERIAL portX command:
For Wi-Fi connections, we can also see, on the CLI, that there is an authenticated user available using the diagnose firewall auth list command.
diagnose firewall auth list
10.100.0.4, F3AB0220E981419CAA9A13CE4811E3F9
type: other, id: 0, duration: 204, idled: 123
flag(10): radius
server: FAC
packets: in 83 out 83, bytes: in 13099 out 7500
Again, here are the boring FortiAuthenticator RADIUS debugs:
Something on the side that cost me more time than I want to admit: I was using an out-of-band connection for the Windows 11 client and was connected via RDP to it so I could test the wired 802.1X configuration from a different room, and I constantly got authentication failures displayed on the NIC. I checked the Windows Event Viewer and saw the following error message on every authentication attempt:
Event ID 15514
Wired 802.1X Authentication failed.
Network Adapter: Intel(R) Ethernet Connection (6) I219-V
Interface GUID: {c8f64a9d-a891-4332-9e1e-1430095beadf}
Peer Address: 000000000000
Local Address: A4BB6D13072B
Connection ID: 0xf
Identity: -
User: -
Domain: -
Reason: 0x50001
Reason Text: Unable to identify a user for 802.1X authentication
Error Code: 0x525
I looked for a long time to find out why a user couldn’t be found for 802.1X authentication, because, in theory, everything is in order. I then physically went to the client, and the authentication immediately worked. I went back to my office, connected via RDP and was met with the same failure message. Turns out that 802.1X EAP-TLS with user authentication doesn’t work when you are connected via RDP. Maybe there is a setting somewhere that allows this, but it’s such an edge case that I didn’t want to spend more time on it.
[Update 2026-06-19]: I recently found the reason, and 802.1X user authentication not working in an RDP session is expected and documented.
For mobile devices
If you have managed mobile devices, you might be able to use the EMS MDM Integration option to let your Android or iOS devices get a certificate from EMS if they are managed by one of the supported options.
In such a scenario, EMS acts as an SCEP server, reachable over TCP/4001 and TCP/4002, where mobile devices can request a certificate.
Once you have the certificate, you can do much the same as with regular clients, so VPN and 802.1X.
DPI is also possible, of course. If you can install the CA using your MDM, that saves you from having to do it manually using the certificates FortiClient gets from EMS.
Covering this topic could be a post in itself, so I will leave you with the documentation for now.
Using EMS user certificates outside of their intended use, that is, ZTNA connections, is a niche use case, and provisioning certificates using other methods can be a better option, but if it fits your use case, this is a good alternative. The idea for this post actually came from a talk I had with Manuel Lehner from Fortinet at an event. He brainstormed some stuff with me, and I thought the topic was interesting enough to see how everything behaves. I am glad that I did, because I definitely learned something from this, and I hope you, dear reader, were able to take something of value with you while reading this.
As I was looking at my notes to enhance my FortiGate baseline, before publishing it, I came across a Fortinet technical tip article I saved months ago that talks about making periodic hash checks of FortiGate files as a best practice.
This is definitely something you should do, but there really is only the command to list the hashes and as the article states:
“It is important to note that this command does not do any comparison on its own; it will simply provide the hashes. It is possible to take a known-good hash from the unit and save it in a secure place, then run this command periodically to compare against the known-good file.”
So the comparison is something you have to take care of, and since there are, on my 70G running 7.6.6, 3695 files, doing this manually is quite the task. To make this easier and more approachable, I wrote the FortiGate filesystem hash checker.
What is it?
The FortiGate filesystem hash checker is a Python script whose main purpose is to compare a known-good hash list of FortiGate system files with a new list of hashes. It highlights modifications, additions, and removals, or tells you that everything is in order.
To function, you first need to create a hash list, and you can either provide this as a raw file using the “diagnose sys filesystem hash” command or by having the script connect to the FortiGate via SSH, executing the command, grabbing the output and saving it to a file. Once you have both a known-good file and a new file, let the script do its comparison and optionally save the result.
The script can take in a CSV file so you can compare multiple FortiGates with a single script execution.
For a more in-depth explanation of the script and how to use it, please read the README.md on the GitHub repository.
If your FortiGate was ever attacked and compromised, it is possible that files were in some way changed, added or removed to gain persistence, deploy malicious code, exfiltrate data, etc. These file actions would be reflected in the hash changing, a new hash appearing or an existing one disappearing, and this can be an Indicator of Compromise (IoC). This has happened in the past, like with FG-IR-22-398.
Right now! There is no time like the present, and without a known-good baseline, you won’t ever know if something happened.
Once you have your known-good file, run the FortiGate filesystem hash checker:
Periodically, for some peace of mind
After you suspect a breach
Before an upgrade
After an upgrade, to build a new known-good file
Note that some file hash changes simply happen from time to time, and this is expected, because these are all files on the system, so certificates and related files, regularly updated files (signatures, ISDB, the geo IP database, etc.) and so on will also show up.
Wrapping up
I am aware that the FortiGate filesystem hash checker isn’t going to be for everyone, but if the script helps even one person, that’s time well spent as far as I’m concerned.
With that in mind, check out the GitHub repository, look at what’s there, and make your own decision about how useful it is to you. I hope you will only ever see a “Everything is OK. No changes detected between the old and new filesystem hash files.” message if you expect the worst.