Mutual TLS only proves who is calling if every hop validates the client certificate properly, and most broken deployments fail at exactly that point. Ordinary TLS authenticates the server to the client; mTLS adds the reverse, so a service, pod, device or partner system proves its identity with an X.509 certificate and the private key behind it. That makes the certificate and key a non-human identity credential, with the same lifecycle, scoping and theft problems as any other. This guide covers the handshake, where to terminate mTLS, certificate identity and authorisation, mTLS in OAuth (RFC 8705), rollout, detection and the mistakes that quietly turn mTLS into plain TLS. It is written for platform, API and security engineers; for how mTLS compares with other machine authentication methods, see the NHI Authentication Guide.
Key takeaways
- mTLS authenticates a key, not a workload. Whoever holds the private key is the identity, so key generation, storage and short lifetimes matter more than the handshake itself.
- Authentication is not authorisation. A valid chain only proves the certificate came from a CA you trust; the server must still check the specific identity (SAN, SPIFFE ID or subject) against policy.
- Termination points are where mTLS breaks. If a load balancer or proxy terminates TLS, how it passes the verified identity on, and whether anything else can reach the backend, decides whether mTLS means anything.
- Permissive modes and default endpoints are bypasses. Istio PERMISSIVE, NGINX
optional, and the AWS API Gateway defaultexecute-apiendpoint all accept callers without a client certificate unless you close them. - mTLS also hardens OAuth. RFC 8705 uses the client certificate to authenticate to the token endpoint and to bind access tokens to that certificate, so a stolen token is useless without the key.
How mutual TLS works
In both TLS 1.2 and TLS 1.3 the server asks for a client certificate by sending a CertificateRequest message; it only does so when client authentication is wanted. The client answers with its Certificate message and a CertificateVerify message signed with its private key, which proves possession of the key rather than just possession of the certificate. The server then validates the chain to a configured trust anchor, checks the validity period and key usage, and optionally checks revocation.
- TLS 1.3 privacy: in TLS 1.3 (RFC 8446) every handshake message after ServerHello is encrypted, so the client certificate is not visible on the wire. Before TLS 1.3 the client certificate is sent in clear, which RFC 8705 notes allows tracking; prefer TLS 1.3 wherever both ends support it.
- Hinting which CA to use: the
certificate_authoritiesextension (RFC 8446 section 4.2.4) lets the server tell the client which CAs it accepts, which helps clients holding several certificates choose the right one. - Post-handshake authentication: TLS 1.3 clients can advertise
post_handshake_auth(sections 4.2.6 and 4.6.2), letting a server request a certificate later in the connection. HTTP/2 does not allow this, so design for authentication at handshake time. - Where the identity lives: modern deployments put the identity in the Subject Alternative Name (DNS name, URI or SPIFFE ID), not the subject common name. The certificate should carry the clientAuth extended key usage when it is used as a client credential.
mTLS compared with other service authentication methods
| Method | What proves identity | Replay of stolen credential | Best fit | Main weakness |
|---|---|---|---|---|
| mTLS (X.509 client certificate) | Private key used in the TLS handshake | Not possible without the private key | East-west service traffic, partner APIs, devices, OAuth clients | Breaks at TLS termination; certificate lifecycle and trust store management |
| Static API key or client secret | A shared string | Trivial: anyone holding the string is the client | Low-risk integrations where nothing better is supported | Leaks in code, logs and images; long-lived |
| OAuth bearer token | Possession of the token | Possible until expiry | APIs needing scopes and central policy | Token theft; needs sender constraint |
| OAuth with DPoP (RFC 9449) | Token plus an application-layer proof signed with a client key | Blocked without the key | Where TLS client certificates cannot be deployed end to end | Proof covers method and URI only; replay tracking across stateless servers is hard |
| Signed JWT from a workload identity provider | Short-lived signed assertion | Possible within the token lifetime and audience | Cloud federation, crossing proxies that terminate TLS | Audience and issuer validation errors |
mTLS and tokens are not alternatives so much as layers: many platforms use mTLS for the transport identity and an OAuth or JWT credential for application-level authorisation. See the Machine-to-Machine OAuth (Client Credentials) Guide and the Token and Session Security Guide.
Where to terminate mTLS: architecture choices
| Pattern | How it works | Choose it when | Watch for |
|---|---|---|---|
| Service mesh sidecar or node proxy (Istio, Linkerd) | Proxies hold workload certificates issued by the mesh CA and handle mTLS transparently | Many internal services in Kubernetes; you want mTLS without changing code | Permissive modes, non-meshed workloads, skipped ports, trust anchor expiry |
| API gateway (for example AWS API Gateway, Azure API Management) | Gateway validates client certificates against a trust store, then forwards to backends | External partners or devices calling a public API | Revocation support, default endpoints that bypass the gateway, header forwarding |
| Reverse proxy or load balancer (NGINX, Envoy, HAProxy) | Proxy validates the certificate and forwards identity in headers | Existing web tier; consistent policy for many apps | Spoofable identity headers; backend reachable directly |
| In the application | Application TLS library verifies the certificate itself | Point-to-point links, databases, message brokers, small estates | Every team re-implements validation; inconsistent trust stores |
Service mesh specifics
- Istio: the PeerAuthentication resource sets
mtls.modeto UNSET, DISABLE, PERMISSIVE or STRICT. With no policy at any level the effective behaviour is PERMISSIVE, which accepts plaintext as well as mTLS. A policy in the root namespace without a selector applies mesh-wide; namespace and workload-selector policies narrow it;portLevelMtlscan exempt individual ports but only on a workload-selector policy. PeerAuthentication controls authentication only: pair STRICT with AuthorizationPolicy rules that name the allowed caller identities. - Linkerd: mTLS is on by default for TCP traffic between meshed pods, with each proxy certificate bound to the pod's Kubernetes ServiceAccount and the private key generated inside the pod. Proxy certificates expire after 24 hours and rotate automatically, but the default trust anchor and issuer certificate last a year and are not rotated for you. Traffic from non-meshed pods (including Kubernetes health checks) and traffic on skip ports is not protected, and plaintext from non-meshed sources is accepted unless authorisation policy forbids it.
See the Service Mesh Security Guide for mesh hardening beyond mTLS, and the SPIFFE and SPIRE Guide for issuing workload identities outside a mesh.
Gateway and proxy specifics
- AWS API Gateway: mTLS is configured on a Regional custom domain with a trust store PEM file in S3 containing the full chain. It does not check revocation, so AWS recommends a Lambda authorizer for revocation and extra checks. The default
execute-apiendpoint bypasses mTLS until you disable it. Failed validation returns 403. - NGINX:
ssl_verify_clienttakeson,off(the default),optionaloroptional_no_ca.ssl_verify_depthdefaults to 1, which is too shallow for most private CA hierarchies with an intermediate.ssl_client_certificatelists trusted CAs and sends their names to clients;ssl_trusted_certificatetrusts without advertising. Revocation is viassl_crlorssl_ocsp(1.19.0 and later). Errors 495 and 496 mean verification failed or no certificate was sent. - Envoy: the
x-forwarded-client-cert(XFCC) header carries Hash, Cert, Chain, Subject, URI, DNS and Issuer fields. Envoy sanitises the header by default;SANITIZE_SETreplaces any incoming value andAPPEND_FORWARDappends to it. The Chain field is what the client sent, not a validated chain.
Certificate identity and authorisation
RFC 8705 warns that X.509 validation is error-prone and that implementers should use a well-tested library rather than writing their own. The bigger risk is usually not the chain check but what the server does after it.
- Limit trust anchors: a server that trusts a large public or shared CA set accepts any certificate those CAs issue. Use a dedicated private CA (or SPIFFE trust domain) for client authentication and trust only that.
- Match one identity field: authorise on an exact SAN or SPIFFE ID, not on "any certificate from our CA" and not on substring matches of the subject DN. The SPIFFE X.509-SVID standard requires exactly one URI SAN per certificate, and validators must reject certificates with more, because multiple identities complicate authorisation.
- Check leaf properties: leaf certificates should have CA set to false and must not carry keyCertSign. X.509-SVIDs require a critical digitalSignature key usage and, when extended key usage is present, both serverAuth and clientAuth.
- Constrain intermediates: name constraints can limit what a compromised intermediate can issue, but SPIFFE notes support for URI name constraints is limited, so test your libraries before relying on them.
- Default deny: after STRICT mTLS, write allow rules per caller identity and per path or method. mTLS with no authorisation layer means every workload holding a mesh certificate can call every service.
| Identity field | Example | Use for | Notes |
|---|---|---|---|
| URI SAN (SPIFFE ID) | spiffe://prod.example.org/ns/payments/sa/api | Workloads in Kubernetes and meshes | Encodes trust domain, namespace and service account |
| DNS SAN | orders.internal.example.org | Services addressed by hostname | Do not reuse the server certificate as a client credential across many hosts |
| Subject DN | CN=partner-a,O=Partner A Ltd | Legacy partners, RFC 8705 tls_client_auth_subject_dn | Compare the full normalised DN exactly |
| Certificate thumbprint | SHA-256 of the certificate | Pinned self-signed clients, RFC 8705 bound tokens | Changes on every renewal, so plan re-registration |
mTLS in OAuth: RFC 8705
RFC 8705 defines two things: using mTLS to authenticate an OAuth client to the authorisation server, and binding access tokens to the client certificate so they cannot be replayed by someone else. RFC 9700, the OAuth 2.0 Security Best Current Practice, recommends asymmetric client authentication such as mTLS or private_key_jwt over shared secrets (section 2.5) and recommends sender-constrained access tokens using mTLS or DPoP (section 2.2.1).
| Element | Value | What it does |
|---|---|---|
| PKI client authentication | tls_client_auth | Server validates the chain and matches exactly one registered value: tls_client_auth_subject_dn, _san_dns, _san_uri, _san_ip or _san_email |
| Self-signed client authentication | self_signed_tls_client_auth | Client registers its certificates via jwks or jwks_uri; no chain validation |
| Certificate-bound token | cnf claim with x5t#S256 | SHA-256 thumbprint of the client certificate in the JWT or introspection response |
| Resource server check | HTTP 401 invalid_token | Rejects the request if the TLS client certificate thumbprint does not match |
| Metadata | tls_client_certificate_bound_access_tokens | Server and client signal support for bound tokens (default false) |
| Endpoint aliases | mtls_endpoint_aliases | Separate mTLS token, revocation and introspection endpoints so other clients are not prompted for certificates |
Two operational consequences follow. A bound token stops working the moment the client renews its certificate, so clients must treat a thumbprint mismatch as an expired token and fetch a new one. And when TLS is terminated before the resource server, the proxy must pass the verified certificate on securely, which RFC 8705 explicitly leaves to implementers. Where certificates cannot be carried end to end, DPoP (RFC 9449) gives sender constraint at the HTTP layer instead. FAPI profiles build on both; see the OAuth 2.1, GNAP and FAPI Guide.
Certificate lifecycle for mTLS identities
- Generate keys where they are used: Linkerd generates proxy keys inside the pod and never moves them; follow the same rule elsewhere. Use hardware or platform key stores (TPM, HSM, cloud KMS, secure enclave) for devices and high-value clients.
- Short lifetimes and automation: mesh workload certificates of hours or a day remove most of the need for revocation. For partner and device certificates, automate renewal (ACME, SCEP, EST or the vendor's agent) and alert well before expiry.
- Track trust anchors and intermediates: leaf rotation is usually automated; root and issuer rotation often is not. An expired mesh trust anchor stops all service-to-service traffic at once.
- Rotate trust stores with overlap: add the new CA to every trust store before any client presents a certificate from it, then remove the old CA after the last old certificate has expired.
- Plan revocation honestly: some platforms do not check revocation (AWS API Gateway, for example), and many internal stacks never fetch CRLs. If you cannot revoke, keep lifetimes short and remove the identity from authorisation policy as the kill switch.
See the Machine Identity, PKI and Certificate Lifecycle Guide and the Certificate Expiry Outages and 47-Day TLS Certificates Guide.
Rolling out mTLS without outages
- Inventory callers: list every client of the service, including health checks, batch jobs, monitoring and partner systems. Non-meshed or legacy callers are what break when you go strict.
- Stand up the CA and identity scheme: choose a private CA or SPIFFE trust domain, define the SAN or SPIFFE ID naming pattern, and decide lifetimes before issuing anything.
- Run in permissive or optional mode with logging: Istio PERMISSIVE or NGINX
ssl_verify_client optional, logging$ssl_client_verifyand the subject for every request. This shows which callers already present valid certificates. - Fix the stragglers: onboard or retire each caller that still arrives without a certificate. Exempt health check ports explicitly and narrowly rather than leaving the whole service permissive.
- Enforce per service or namespace: move to STRICT or
ssl_verify_client onone ring at a time (test, internal low-risk, production), watching handshake failure rates. - Add authorisation: introduce allow rules per caller identity, first in audit or dry-run mode where the platform supports it, then enforced.
- Close bypasses: disable default endpoints, block direct access to backends behind the proxy with network policy or security groups, and strip incoming identity headers at the edge.
- Roll back safely: keep the previous policy as a versioned manifest; rolling back means reverting STRICT to PERMISSIVE for the affected scope only, never disabling mTLS mesh-wide. Record every rollback as an incident with an owner and a date to re-enforce.
Decision guidance
- Mesh versus gateway: use the mesh for east-west traffic inside clusters and a gateway or proxy for north-south traffic from partners and devices. Do not extend a mesh CA to external partners; give them certificates from a separate CA with its own trust store.
- mTLS versus DPoP for OAuth: choose RFC 8705 when clients are servers you control and certificates already flow end to end; choose DPoP when TLS is terminated by infrastructure you cannot change or clients cannot hold certificates.
- Small organisations: a managed gateway or a single proxy tier with a small private CA covers most needs; avoid running a mesh only to get mTLS for a handful of services.
- Large organisations: standardise one identity format (usually SPIFFE IDs), one or a few issuing CAs with automated rotation, and central authorisation policy. Treat CA operations as a privileged service.
Where identity fits
Every mTLS client certificate is a non-human identity: it needs an owner, an inventory record, a purpose, a lifetime and a removal date. Private keys copied into container images, shared across environments, or reused by several services are the mTLS equivalent of a shared service account password. ATT&CK covers theft of the key material as T1552.004 Private Keys and theft or forgery of certificates as T1649 Steal or Forge Authentication Certificates; a compromised issuing CA lets an attacker mint identities at will. Store client keys in a secrets manager or key store, never in repositories. See the Secrets Management Guide and the Cloud Workload Identity Guide.
Detection and measurement
- Log the verified identity: record client certificate subject or SPIFFE ID, issuer, serial, verification result and TLS version per connection (NGINX
$ssl_client_verify,$ssl_client_s_dn,$ssl_client_v_remain; mesh access logs with peer identity). - Alert on plaintext where STRICT is expected: mesh telemetry showing non-mTLS connections, or any request to a default endpoint that should be disabled.
- Alert on unknown issuers and identities: certificates from unexpected CAs, new SPIFFE IDs calling sensitive services, or one identity appearing from many source networks.
- Watch handshake failures: spikes in 495/496 or TLS alert rates often precede an expiry outage or reveal scanning with stolen or invalid certificates.
- Track certificate issuance: issuance volume per identity and per CA; a sudden rise can indicate key misuse or a compromised enrolment path.
| Metric | Target | Why it matters |
|---|---|---|
| Share of service-to-service traffic using mTLS in strict mode | Close to 100% for production namespaces | Permissive traffic is unauthenticated traffic |
| Services with identity-based authorisation rules, not just mTLS | All services handling sensitive data | mTLS without authorisation lets any workload call anything |
| Median client certificate lifetime | Hours to days for workloads | Shorter lifetimes reduce reliance on revocation |
| Trust anchors and intermediates with automated rotation and an expiry alert | 100% | Root or issuer expiry causes estate-wide outages |
| Exempted ports and permissive scopes, with owner and expiry date | Falling every quarter | Exceptions become permanent bypasses |
Common failure patterns
- Stuck in permissive: rollouts start in PERMISSIVE mode and never move on, so an attacker inside the network simply connects without a certificate. Set a date per namespace to go STRICT.
- Trust the header, skip the check: a backend reads an identity header such as X-Client-DN but is reachable directly or the edge does not strip incoming copies, so any caller can set it. Strip at the edge and lock backends to the proxy.
- "Any certificate from our CA": authorisation stops at chain validation, so every workload with a mesh certificate can reach the payment service. Authorise on exact identities.
- Default endpoints and side doors: the AWS API Gateway
execute-apiendpoint, a node port, or a second listener without client verification bypasses the mTLS listener completely. - Shallow or wrong verification:
ssl_verify_depth 1rejecting valid chains leads teams to switch tooptional_no_ca"temporarily"; certificate checks disabled in client libraries to get past a test failure ship to production. - Forgotten trust anchors: leaf certificates rotate daily but the mesh root expires after a year and takes down every service at once.
Framework mapping
| Framework | Reference | How mTLS applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-9 Service Identification and Authentication; IA-3 Device Identification and Authentication | Services and devices authenticate before connections are established |
| NIST SP 800-53 Rev. 5 | IA-5(2) Public Key-based Authentication; SC-17 Public Key Infrastructure Certificates | Certificate path validation, private key protection, CA management |
| NIST SP 800-53 Rev. 5 | SC-8 Transmission Confidentiality and Integrity; SC-23 Session Authenticity | Encrypted, authenticated channels between services |
| NIST CSF 2.0 | PR.AA-03; PR.DS-02 | Services and hardware are authenticated; data in transit is protected |
| ISO/IEC 27001:2022 Annex A | A.8.5 Secure authentication; A.8.24 Use of cryptography | Authentication technology and key management rules |
| MITRE ATT&CK v20 | T1557 Adversary-in-the-Middle; T1040 Network Sniffing; T1552.004; T1649 | mTLS counters interception and impersonation; key and certificate theft defeats it |
| MITRE D3FEND | D3-CBAN Certificate-based Authentication; D3-CERO Certificate Rotation; D3-CP Certificate Pinning | Defensive techniques that mTLS deployments implement |
Practitioner checklist
- Inventory every caller of each service, including health checks and batch jobs, before enforcing mTLS.
- Use a dedicated private CA or SPIFFE trust domain for client authentication and trust only that anchor.
- Prefer TLS 1.3 so client certificates are encrypted in the handshake.
- Move every production scope from permissive or optional to strict, with dated, owned exceptions only.
- Authorise on exact SAN or SPIFFE ID per service and method; default deny.
- Disable default and side-door endpoints and block direct access to backends behind proxies.
- Strip identity headers such as XFCC at the edge and set them only from verified connections.
- Automate rotation of leaf, issuer and root certificates and alert on expiry well in advance.
- Bind OAuth tokens to client certificates (RFC 8705) or DPoP keys for high-value APIs.
- Log verified identity, issuer, serial and verification result for every connection.
Standards and references
- IETF: RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- IETF: RFC 8446 The Transport Layer Security (TLS) Protocol Version 1.3
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- SPIFFE: X509-SVID Standard
- Istio: PeerAuthentication reference
- Linkerd: Automatic mTLS
- AWS: Configuring mutual TLS authentication for a REST API
- NGINX: Module ngx_http_ssl_module
Related NHI Mgmt Group resources: NHI Authentication Guide · Service Mesh Security Guide · SPIFFE and SPIRE Guide · Machine Identity, PKI and Certificate Lifecycle Guide · Token and Session Security Guide