IIS Client Certificate Authentication (mTLS): Require, Map, and Debug 403.7 / 403.16 Errors

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.

sslFlagsNumericHandshake behaviourLogged as
None0Client certificates ignored entirely—
Ssl8Certificate requested, optionalAnonymous access proceeds
SslNegotiateCert32Certificate requested, optional (explicit)Anonymous access proceeds
SslRequireCert1Certificate required, chain validated403.7 when missing, 403.16 when untrusted
SslMap64Certificate required and mapped to an account401.1 when mapping fails
Quotas and behaviour by SSL setting

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:

SymptomTypical causeWhere to confirm
403.7 ForbiddenNo certificate presented; client store empty or Firefox NSSIIS log sc-status=403, sc-substatus=7
403.16 ForbiddenChain incomplete, CRL unreachable, expired client certEvent 36888 / Schannel, HttpService 15021
401.1 UnauthorizedMapping found but account disabled or wrong passwordSecurity log 4625 on the server
Handshake resetClient cert not for client auth (missing EKU)certutil -dump client.cer | find “Enhanced”
Works in Edge, fails in FirefoxNSS store not populatedabout:config security.remember_cert_checkbox_default_setting
Quotas and behaviour by SSL 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 Tracing with the AUTHENTICATION_COMPLETE provider during rollout, then turn it off — the trace files are large.
  • Keep at least one binding without SslRequireCert reachable 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.

Leave a Comment