Extracts identity from a TLS peer certificate. Two ingestion paths:
in-process Request.PeerCerts (Mode B / native gRPC) and Envoy's
x-forwarded-client-cert header (Mode A).
Source: pkg/identity/mtls — registered as mtls.
- Service-to-service identity in a mesh (Istio, Linkerd, Consul).
- SPIFFE / SPIRE workloads (URI SAN like
spiffe://td/ns/foo/sa/bar). - You terminate mTLS at Envoy and want the SAN to flow into authz.
identifiers:
- name: mesh-mtls
type: mtls
config:
# XFCC ingestion is OPT-IN. Anything that can reach the auth
# surface can otherwise forge identity by setting the header
# itself. Set true only when a verified Envoy/Istio hop strips
# inbound XFCC and re-emits its own.
trustForwardedClientCert: true
header: x-forwarded-client-cert # default is "X-Forwarded-Client-Cert"; HTTP headers are case-insensitive so this is equivalent
# Strongly recommended when trustForwardedClientCert is true:
# pin the CA roots so a self-signed cert with a forged Issuer DN
# cannot bypass authentication. Lwauth runs cert.Verify() with
# this pool as Roots.
trustedCAFiles:
- /etc/lwauth/mesh-ca.pem
# …or inline:
# trustedCAs: |
# -----BEGIN CERTIFICATE-----
# ...
# -----END CERTIFICATE-----
# Optional secondary Subject-DN allow-list. NOT a trust check on
# its own — without trustedCAFiles/trustedCAs an attacker can
# mint a self-signed cert whose Issuer string matches.
trustedIssuers:
- "CN=workload-ca,O=acme"Default-deny posture (post-2026-04-28 security review). Without
trustForwardedClientCert: true, the module ignores the XFCC header entirely. Only certificates surfaced viaRequest.PeerCerts(i.e. lwauth itself terminated TLS and the Go stack verified the peer) are accepted. SupplyingtrustedCAFiles/trustedCAswithout the trust flag is rejected at config compile time. The symmetric mistake is also rejected:trustForwardedClientCert: truewithout any anchor — notrustedCAFiles, notrustedCAs, and notrustedIssuers— fails at compile time so an operator can't accidentally re-enable blind-trust XFCC by flipping a single flag.
When fronting lwauth with Envoy you must (a) sanitize any inbound XFCC from untrusted clients and (b) emit a fresh XFCC from the verified peer cert. The relevant knobs:
# envoy.config.core.v3.HttpConnectionManager
forward_client_cert_details: SANITIZE_SET # drop inbound, emit ours
set_current_client_cert_details:
cert: true
uri: true
subject: true
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service: { envoy_grpc: { cluster_name: lwauth } }
include_peer_certificate: trueThe Helm chart's NetworkPolicy should additionally restrict ingress to lwauth so the only callers are the trusted Envoy/sidecar peers.
Identity precedence: SPIFFE URI SAN > first DNS SAN > Subject CN. The
identity result is Identity{Subject: "spiffe://td/ns/foo/sa/bar", Source: "mesh-mtls", Claims: {issuer, dnsSANs, uriSANs, ...}}.
Envoy must be configured to forward XFCC. Operator-side snippet:
# Envoy upstream (DESIGN.md §1, Door A)
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service: { envoy_grpc: { cluster_name: lwauth } }
include_peer_certificate: true # ← required# values.yaml
config:
inline: |
identifiers:
- name: mesh-mtls
type: mtls
config:
trustedIssuers: ["CN=workload-ca,O=acme"]
authorizers:
- { name: by-spiffe, type: cel, config: { expression: "identity.subject.startsWith('spiffe://td/ns/orders/')" } }POST /grpc.foo.Bar/Method HTTP/2
x-forwarded-client-cert: By=spiffe://td/ns/api/sa/lwauth;Hash=...;URI=spiffe://td/ns/orders/sa/worker→ Identity{Subject: "spiffe://td/ns/orders/sa/worker", Source: "mesh-mtls"} → cel allows because the SPIFFE namespace is orders.
firstMatch: [jwt, mtls]: human users present a JWT, services present a cert.- Pair with
rbacrolesFrom: claim:spiffeNsafter stamping the SPIFFE namespace into a derived claim viacel.
- SPIFFE / SPIRE specs.
- Envoy
x-forwarded-client-certheader format. - Source: pkg/identity/mtls/mtls.go.