mTLS / client-certificate auth
As of 0.37.0 DaloyJS ships clientCertAuth() — a middleware that authenticates a request by its TLS client certificate, the standard answer to “prove this internal call came from a trusted peer” in zero-trust / service-to-service deployments. It is dependency-free and runtime-portable, with two certificate sources:
- Native TLS — when the runtime terminates TLS itself, the Node adapter reads the peer certificate off the socket and attaches it to the request (lazily — plain requests pay nothing).
- Forwarded by a trusted proxy — when TLS is terminated upstream (Envoy, nginx, HAProxy, Traefik, a cloud load balancer), the middleware parses the verified identity the proxy forwards in request headers (Envoy
X-Forwarded-Client-Certor operator-named structured headers).
Quick start
On success the accepted ClientCertificate is stamped on ctx.state.clientCertificate (configurable via stateKey) for downstream handlers and audit logging.
Rejection semantics
- No certificate presented →
401application/problem+jsonwithCache-Control: no-store. - Unverified chain, failed allow-list, expired, or custom-rejected →
403(the response never echoes certificate details, to avoid leaking which check failed).
Native TLS (Node)
When the peer socket is a TLS socket presenting a client certificate, the Node adapter normalizes it (subject, issuer, fingerprint, SANs, validity window, and whether the chain was verified) and attaches it to the request. The read is deferred behind a lazy thunk, so only routes actually guarded by clientCertAuth() pay for it — and plain HTTP requests pay nothing. Run your Node server with requestCert: true and a configured CA so the runtime verifies the chain.
Behind a TLS-terminating proxy
When a proxy terminates TLS, it forwards the verified client identity in request headers. Because those headers are spoofable by anything that can reach the app directly, the header path is opt-in — only enable it when the app is exclusively reachable through the terminating proxy.
Envoy (X-Forwarded-Client-Cert)
nginx / HAProxy / Traefik (structured headers)
For proxies that forward parsed fields in separate headers, name each header. The verifyheader lets the middleware require the proxy's own verification result (nginx $ssl_client_verify === "SUCCESS"):
Allow-lists & checks
requireVerified(defaulttrue) — refuse any certificate the TLS terminator did not verify.allowSubjectCNs/allowIssuerCNs— exact CN match.allowFingerprints— SHA-256 fingerprint match in constant time (colons/spaces and case are ignored, so a value copied fromopensslworks as-is).allowSANs— at least one Subject Alternative Name must match (asTYPE:valuelikeURI:spiffe://acme/svc-a, or as a bare value).checkValidity(defaulttrue) — reject certificates outside their[notBefore, notAfter]window when known (belt-and-braces for header-forwarded certs).verify(cert, ctx)— a custom async hook run last; returningfalserejects with403.
The ClientCertificate
Whatever the source, handlers receive one normalized shape:
The building blocks are exported from @daloyjs/core/mtls too: parseForwardedClientCert() (Envoy XFCC), normalizePeerCertificate() (Node getPeerCertificate() shape), and setClientCertificate() / getClientCertificate() for custom adapters.