91 lines
4.0 KiB
Markdown
91 lines
4.0 KiB
Markdown
# 6. mTLS: in the mesh or in the application
|
|
|
|
*Prev: [5. The gateway](05-the-gateway.md) · Next: [7. Choosing](07-choosing.md)*
|
|
|
|
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
|
|
|
|
```yaml
|
|
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" }
|
|
```
|
|
|
|
```java
|
|
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`](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](05-the-gateway.md) · Next: [7. Choosing](07-choosing.md)*
|