IIS client certificate authentication — mutual TLS, or mTLS — lets you authenticate users and services with certificates instead of, or in front of, Windows credentials. It is the strongest access control IIS offers without a VPN, and it fails in three confusing ways: 403.16 when the server rejects the client certificate chain, 403.7 when no certificate is presented at all, and a browser that silently sends nothing because the certificate store holding the key was the wrong one. This walkthrough covers the full path from opening the IIS configuration section to reading the HTTP.sys logs that explain a failure.
How IIS client certificate authentication actually works
During the TLS handshake the server sends a CertificateRequest asking the client for a certificate. IIS then applies one of four values of system.webServer/security/access sslFlags: None, Ssl (Accept — request but do not require), SslRequireCert, and SslMap / SslRequireCert combined with a mapping. With Accept, IIS negotiates a certificate if one is offered and simply proceeds anonymously if not; with Require, the handshake ends before any HTTP request is logged.
Two details drive almost every support ticket. First, certificate selection is client-side: Chrome and Edge use the Windows CurrentUser\My and LocalMachine\My stores, while Firefox keeps its own NSS database. Second, the server must be able to build the client certificate’s chain, but that chain is not the same as the site’s server certificate chain unless both came from the same CA. If your site uses a public certificate and clients present certificates from an internal AD CS enterprise CA, the internal root must be in LocalMachine\Root on the IIS server.
mTLS needs no special Windows licence — only careful PKI hygiene — so it runs fine on any plan; see Windows VPS plan tiers and what each supports if you are sizing the box at the same time.
Require client certificates at the site level
Everything can be done from an elevated PowerShell session with the WebAdministration module loaded; the same settings live in applicationHost.config if you prefer to edit it directly. Run this against the site you intend to lock down, not the server default, so that a misconfiguration does not take every site offline at once.
Import-Module WebAdministration
# Turn on client certificate negotiation with a required certificate
Set-WebConfigurationProperty -PSPath 'IIS:\' `
-Filter "system.webServer/security/access" `
-Name sslFlags -Value "SslNegotiateCert,SslRequireCert" `
-Location "Default Web Site"
# Read the effective value back
Get-WebConfigurationProperty -PSPath 'IIS:\' `
-Filter "system.webServer/security/access" -Name sslFlags `
-Location "Default Web Site"
The flag names map to the numeric values in the table below. Note that Ssl128 is a legacy flag that is ignored on current TLS stacks; do not add it expecting a cipher restriction, because cipher suites are controlled by HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols and the OS policy, not by this access section.
| sslFlags | Numeric | Handshake behaviour | Logged as |
|---|---|---|---|
| None | 0 | Client certificates ignored entirely | — |
| Ssl | 8 | Certificate requested, optional | Anonymous access proceeds |
| SslNegotiateCert | 32 | Certificate requested, optional (explicit) | Anonymous access proceeds |
| SslRequireCert | 1 | Certificate required, chain validated | 403.7 when missing, 403.16 when untrusted |
| SslMap | 64 | Certificate required and mapped to an account | 401.1 when mapping fails |
Make the issuing CA trusted on the IIS server
A client certificate that validates on the client machine can still fail on the server if the root is missing. Import the root and any intermediate into the machine stores — a per-user store import is the single most common reason a working test certificate suddenly produces 403.16 under the IIS service account.
# Root CA into LocalMachine\Root, intermediate into LocalMachine\CA
Import-Certificate -FilePath "C:\PKI\root-ca.cer" `
-CertStoreLocation Cert:\LocalMachine\Root
Import-Certificate -FilePath "C:\PKI\issuing-ca.cer" `
-CertStoreLocation Cert:\LocalMachine\CA
# Sanity check: can the server build the chain?
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\PKI\client.cer")
$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$chain.ChainPolicy.RevocationMode = "NoCheck" # AD CS CRL is often unreachable
$chain.Build($cert)
$chain.ChainStatus | Select-Object Status, StatusInformation
Revocation checking is the second trap. IIS uses the Windows certificate chaining engine, which will attempt CRL or OCSP lookups against the CDP extension in the certificate. On an isolated VPS with no route to an internal CRL distribution point, chain building times out and the handshake is refused even though the certificate is otherwise valid. Either publish the CRL where the server can reach it, or run a local responder; disabling revocation entirely is a deliberate risk trade-off, not a fix.
Map a certificate to a Windows identity
Requiring a certificate proves possession of a key; it does not tell IIS who the user is. For a one-to-one mapping, bind an exact thumbprint to a local or domain account. For a many-to-one mapping, bind by subject or issuer and let Active Directory do the group lookup. Both are stored under system.webServer/security/authentication/iisClientCertificateMappingAuthentication, and the one-to-one section must be enabled before the entries are honoured.
$site = "IIS:\Sites\Default Web Site"
$hash = (Get-PfxCertificate "C:\certs\client.pfx").Thumbprint
# One-to-one: exact thumbprint -> Windows account
New-WebConfigurationProperty -Filter "system.webServer/security/authentication/iisClientCertificateMappingAuthentication" `
-Name "." -Value @{enabled="true"} -PSPath 'IIS:\' -Location $site
Add-WebConfigurationProperty -Filter "system.webServer/security/authentication/iisClientCertificateMappingAuthentication/oneToOneMappings" `
-Name "." -Value @{certificate=$hash; userName="CONTOSO\svc-report"; password="<pwd>"} `
-PSPath 'IIS:\' -Location $site
# Many-to-one: any certificate from the reporting CA maps to a group
Add-WebConfigurationProperty -Filter "system.webServer/security/authentication/iisClientCertificateMappingAuthentication/manyToOneMappings" `
-Name "." -Value @{name="ReportReaders"; certificateField="Subject"; certificateSubField="${subject}"; `
matchCriteria="CN=Report"; userName=""; password=""; enabled="true"} -PSPath 'IIS:\' -Location $site
Diagnosing the classic errors
Start with the sub-status code in the IIS log at %SystemDrive%\inetpub\logs\LogFiles, then look in System and Microsoft-Windows-HttpService for the matching handshake failure. The mapping between symptom and cause is narrow enough to check in a single pass:
| Symptom | Typical cause | Where to confirm |
|---|---|---|
| 403.7 Forbidden | No certificate presented; client store empty or Firefox NSS | IIS log sc-status=403, sc-substatus=7 |
| 403.16 Forbidden | Chain incomplete, CRL unreachable, expired client cert | Event 36888 / Schannel, HttpService 15021 |
| 401.1 Unauthorized | Mapping found but account disabled or wrong password | Security log 4625 on the server |
| Handshake reset | Client cert not for client auth (missing EKU) | certutil -dump client.cer | find “Enhanced” |
| Works in Edge, fails in Firefox | NSS store not populated | about:config security.remember_cert_checkbox_default_setting |
A command-line test removes the browser from the equation. curl.exe on Server 2019 and later uses Schannel, so it exercises the Windows store directly:
curl.exe -v --cert "C:\certs\client.pfx:password" https://mtls.example.com/health
Testing and hardening before you switch the site
Run mTLS on a second site binding or a dedicated host header first. Once SslRequireCert is set on a production binding, every client — including your monitoring probe, your load balancer health check, and the backup agent — must present a certificate, or it starts generating 403.7 noise in the logs at the same rate as your health-check interval.
- Give the monitoring and load-balancer probes their own long-lived certificate and rotate it on a documented schedule.
- Set an explicit validity window shorter than your CA’s maximum; a ten-year client certificate defeats the point of key rotation.
- Enable
Failed Request Tracingwith theAUTHENTICATION_COMPLETEprovider during rollout, then turn it off — the trace files are large. - Keep at least one binding without
SslRequireCertreachable from a management network until you are confident in the mapping rules.
Finally, size the box for the extra handshake cost. Mutual TLS roughly doubles the asymmetric work per connection: expect 5–15 ms of added CPU per new TLS session on a single vCPU, which is invisible for a few hundred users and very visible for a public API doing 2,000 new connections per second. Session resumption helps; SslRequireCert with client certificates does not resume as cheaply as anonymous TLS, so budget for it rather than discovering it under load.
If you are choosing an environment capable of running IIS with a custom SCHANNEL policy and a local CA hierarchy, compare the dedicated options on the Windows VPS hosting platform overview before migrating.


