1
0
Files

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

docs/output/06-mtls.txt:

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