4.0 KiB
6. mTLS: in the mesh or in the application
Prev: 5. The gateway · Next: 7. Choosing
Run ./scripts/certs.sh, then start MtlsApplication on 8443. The certificates it generates
are the point: edge.crt and rogue.crt have identical subjects and different issuers.
subject=CN = edge-service, OU = payments issuer=CN = Internal Mesh CA
subject=CN = edge-service, OU = payments issuer=CN = Some Other CA
Identity under mTLS is not the subject. It is the subject plus the fact that a CA in the trust store vouched for it.
In-application mTLS
server:
ssl:
bundle: server
client-auth: need
spring:
ssl:
bundle:
pem:
server:
keystore: { certificate: "file:...server.crt", private-key: "file:...server.key" }
truststore: { certificate: "file:...internal-ca.crt" }
http.x509((x509) -> x509
.subjectPrincipalRegex("CN=([^,]*)(?:,|$)")
.userDetailsService(certificateUsers()));
A good certificate produces a PreAuthenticatedAuthenticationToken with the CN as the
principal, and — in Spring Security 7 — a FACTOR_X509 authority alongside the roles, the
sibling of the FACTOR_BEARER you get from a JWT and the FACTOR_PASSWORD you get from Basic.
Any assertion using containsExactly on authorities will fail on it.
Three things the transcript shows that a diagram does not
The rogue certificate produces no HTTP status at all. curl exits 56; there is no response
line, no 401, no 403, and nothing in the application log at INFO. The handshake failed. Your
application-level metrics will show nothing, because from the application's point of view
nothing happened. Debugging this means -Djavax.net.debug=ssl:handshake or the load balancer's
own logs.
client-auth: need is a property of the connector, not of a path. /mtls/trusted-header is
permitAll() and it fails exactly the same way without a certificate. You cannot expose a
public health endpoint on an mTLS-only connector; it needs a second connector, or want
instead of need plus an explicit authorization rule that treats an absent certificate as
anonymous.
Header-based identity is verified by nothing. The last call in the transcript presents a
valid certificate for edge-service and a header claiming to be payments-service, and the
endpoint reports payments-service.
Mesh mTLS
A service mesh terminates TLS in a sidecar. The application receives plain HTTP on localhost and
the peer identity arrives as a header — X-Forwarded-Client-Cert in Envoy, carrying the SPIFFE
URI SAN. The trade:
| Mesh | In-application | |
|---|---|---|
| Certificate lifecycle | Handled, rotated automatically | Yours: issuance, rotation, expiry alerts |
| Application code | None | An SSL bundle plus x509(..) |
| Identity in the app | A header | A verified X509Certificate |
| Works with cert-bound tokens (RFC 8705) | No | Yes |
| Failure mode | Anything that bypasses the sidecar can spoof the header | Handshake failure, no HTTP status |
The row that decides it for a security-sensitive service is the RFC 8705 one. Certificate-bound
access tokens — the cnf / x5t#S256 claim, which X509CertificateThumbprintValidator
already checks by default (chapter 4) — bind a token to the TLS connection it was issued for, so
a stolen token is useless without the private key. That binding requires the application to see
the client certificate. Terminate mTLS in a sidecar and the strongest defence available against
token theft is off the table.
If you take mesh identity from a header, the header must be stripped at ingress on every
path into the pod, and the pod must not be reachable except through the proxy. Both are
infrastructure guarantees that no amount of application code can verify — which is exactly why
/mtls/trusted-header answers "verifiedBy": "nothing. This endpoint believes a header.".
Prev: 5. The gateway · Next: 7. Choosing