PIV/CAC Client-Certificate Login (X.509)
MOD supports PIV/CAC smart-card login: a browser sign-in that presents a
client certificate over an mTLS edge can be authenticated by Keycloak's
built-in X509/Validate Username Form authenticator
(provider id auth-x509-client-username-form, shipped in Keycloak 26.x)
instead of by password. The certificate chain, revocation and trust
validation all live in Keycloak — MOD does not implement its own
certificate auth — and the login produces the same tokens the password
flow produces, so no new identity channel exists on the API side.
The X.509 step is installed in the realm's browser flow as
ALTERNATIVE while enabled: password + 2FA stays the primary path, and a
login without a client certificate is unaffected. Service and machine
accounts (client-credentials, direct grant, e2e_service_principals)
never traverse the browser flow, so they are exempt by construction —
the step can never lock a machine principal in or out.
Scope note: this page describes a capability MOD enables. It does not by itself satisfy any control in any framework, and enabling PIV/CAC login does not remove the operator's remaining responsibilities: the DoD CA / PIV trust store, card issuance and lifecycle, and the operator's own policy remain the operator's responsibility.
The four platform settings
All four live in the auth group and are applied through the audited
platform-settings path. A change to any of them triggers
converge_x509 (CORE/API/services/x509_policy.py) after the settings
commit, which updates the live execution in Keycloak — no Keycloak
restart is needed. keycloak-init.sh installs the execution on boot
and re-converges it.
| Setting | Default | What it does |
|---|---|---|
auth.x509_enabled | false | Master switch. true sets the X.509 execution requirement to ALTERNATIVE (and applies the config below); false sets it to DISABLED. Default off = the feature is dormant even if the edge forwards certificates. |
auth.x509_user_mapping | subject_email | Which field of the certificate is matched against the MOD user's username/email in the realm. subject_email → SUBJECTALTNAME_EMAIL (RFC 822 SAN), subject_cn → SUBJECTDN_CN (Subject Common Name), upn_san → SUBJECTALTNAME_OTHERNAME (X.509 UPN SAN). |
auth.x509_revocation | ocsp | Revocation checking for client certificates: ocsp (OCSP only), crl (CRL via the certificate's CRL distribution point), both (both), or none (no revocation check — not recommended). |
auth.x509_ocsp_responder_uri | (empty) | OCSP responder URI used for revocation checks. Empty = the certificates' own AIA OCSP URL. Applied only while OCSP checking is on (ocsp or both). |
The user mapper is fixed to USERNAME_EMAIL — the certificate field
chosen by auth.x509_user_mapping is matched against the realm user's
username or email.
Operator mTLS edge (required)
Keycloak cannot see the TLS peer of a browser connection; the operator's
edge proxy must terminate mutual TLS, verify the client certificate
against the trusted CA bundle, and forward the verified certificate to
Keycloak. Keycloak's nginx cert-lookup provider
(x509-certificate-lookup provider of type nginx) reads the
certificate from the X-SSL-Cert header and only accepts it when
X-SSL-Verify is SUCCESS, so a forged header from a non-mTLS client
is ignored.
Documented reference shape for the Keycloak-facing server block —
apply it on your own edge, not in this repository:
server {
listen 443 ssl;
server_name keycloak.example.com;
ssl_certificate /etc/nginx/tls/keycloak.crt;
ssl_certificate_key /etc/nginx/tls/keycloak.key;
# Optional client certs: present-and-valid for PIV/CAC logins,
# absent for ordinary password logins.
ssl_verify_client optional;
ssl_client_certificate /etc/nginx/tls/piv-cac-ca-bundle.crt;
location / {
proxy_pass http://keycloak_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Forward the verified client certificate (escaped PEM) and the
# verification verdict. Keycloak's nginx cert-lookup provider
# consumes these.
proxy_set_header X-SSL-Cert $ssl_client_escaped_cert;
proxy_set_header X-SSL-Verify $ssl_client_verify;
}
}
Notes:
ssl_verify_client optional;is what lets ordinary password logins (no card) continue to work on the same vhost. With a valid but untrusted or expired client certificate the handshake still succeeds, but$ssl_client_verifyis notSUCCESSand Keycloak ignores the header./etc/nginx/tls/piv-cac-ca-bundle.crtmust contain the CA chain that issued the PIV/CAC cards (for DoD cards, the appropriate CAC CA certificates).- If the edge is Caddy, the equivalent is the mTLS
tlsblock with the same CA bundle plus anheader_uprule that forwards the verified certificate and verify status to Keycloak; the Keycloak-side provider stays the nginx cert-lookup (or the header-based equivalent) that readsX-SSL-Cert/X-SSL-Verify. - This repository does not ship or edit any edge configuration — the block above is documentation for the operator's own proxy.
Verify it
- With the edge configured, present a test client certificate (chain
trusted by
ssl_client_certificate) through the mTLS edge while signing in and confirm the sign-in is accepted via the X.509 alternative and the mapped user session is issued. - Sign in with no client certificate and confirm the normal password (+ 2FA) flow still works.
- Present a revoked certificate (per the active
auth.x509_revocationmode) and confirm the login is refused. - Set
auth.x509_enabledtofalseand confirm the execution returns toDISABLEDin the Keycloak browser flow (presenting a valid certificate no longer authenticates).