feat(identity-tls-cert-auth): optional trust-anchor chain verification for client certificates#7021
Conversation
…n for client certificates Add an opt-in per-identity `verifyClientCertificateChain` (default false). When enabled, the configured CA certificate is treated as a trust anchor and the client-presented chain (leaf + intermediates) is validated up to it via RFC 5280 path building, instead of requiring the CA to be the leaf's direct issuer. This supports PKIs whose issuing intermediate rotates beneath a long-lived root (e.g. SPIRE X.509-SVIDs): operators can pin the stable root and let the workload present its current intermediate alongside the leaf, with no rotation controller. Default behavior is unchanged (single-hop). The end-entity-leaf requirement and trusted-proxy header sourcing (Infisical#6913) and SAN matching (Infisical#6957) are preserved and enforced in both modes. Includes migration, path-validation unit tests, API/audit wiring, a UI toggle on the Advanced tab, and docs.
bd876ed to
5a9a460
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a9a460f4b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
| Filename | Overview |
|---|---|
| backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-fns.ts | Adds verifyClientCertificateChain: a DFS path-builder from leaf through client-presented intermediates to the configured trust anchor. Logic is sound — cryptographic signature checks at each hop, CA basic-constraints enforced on intermediates, validity windows checked on every certificate including the anchor, and a used Set prevents cycles. The issuedBy issuer/subject string comparison may silently reject valid chains when certificates from different PKIs encode the same DN differently. |
| backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts | Integrates the new chain-verification branch into the login flow. The CA-certificate check and existing CN/SAN checks correctly run outside the new branch. Minor: when chain mode is active, the leaf validity checks at lines 194–216 are unreachable and produce different error messages than the chain verifier, creating an inconsistency in operator-facing denial reasons. |
| backend/src/db/migrations/20260624120000_add-verify-client-certificate-chain-to-identity-tls-cert-auth.ts | Adds a nullable-safe boolean column verifyClientCertificateChain defaulting to false. Both up/down migrations guard with hasColumn, making them idempotent. No issues. |
| backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-fns.test.ts | Adds comprehensive unit tests using real generated RSA keys and x509 certificates: valid chain, leaf directly issued by anchor, missing intermediate, wrong root, forged extra intermediate, expired leaf, not-yet-valid leaf, and expired intermediate. Good coverage of the security-relevant edge cases. |
| backend/src/server/routes/v1/identity-tls-cert-auth-router.ts | Correctly exposes verifyClientCertificateChain as an optional boolean on both attach and update endpoints, defaulting to false. Included in audit-log event metadata. No issues. |
| frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm.tsx | Adds a Switch toggle for verifyClientCertificateChain on the Advanced tab with a tooltip explaining the behaviour. Schema default, reset default, and submission all consistently pass the boolean. No issues. |
| docs/documentation/platform/identities/tls-cert-auth.mdx | Documents the new option accurately, describing both modes, the SPIRE use-case motivation, and the constraints that still apply (end-entity only, CN/SAN checks). No issues. |
Comments Outside Diff (1)
-
backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts, line 194-216 (link)Leaf validity checks are unreachable and produce different error messages in chain mode
When
verifyClientCertificateChainistrue, the call toverifyClientCertificateChain(...)already checks the leaf's validity (lines 142-162) and throwsUnauthorizedErrorwith"Access denied: A certificate in the chain is outside its validity period."for any expired or not-yet-valid leaf. If that check passes, the leaf is definitively valid at the time of the call, so the post-verification validity checks here (which use a separatenew Date()snapshot) can never fail in chain mode. The result is an asymmetry: an expired leaf in single-hop mode yields the specific"Access denied: Certificate has expired."message, while the same situation in chain mode yields the generic chain-level message. Neither path is broken, but the inconsistency may confuse operators reading auth denial logs. A brief comment marking this as intentionally redundant for single-hop mode only would prevent future confusion.
Reviews (1): Last reviewed commit: "feat(identity-tls-cert-auth): optional t..." | Re-trigger Greptile
Context
TLS Certificate Auth verifies the client certificate single-hop: the presented leaf has to be signed directly by the configured
caCertificate, and any intermediates the client sends are dropped (extractX509CertFromChain(...)[0]).That's a problem for any PKI where the certificate that signs the leaf isn't long-lived. The case I hit is SPIRE X.509-SVIDs: the signing intermediate rotates frequently (about daily for us) while the root is stable for years. Single-hop forces you to configure the rotating intermediate as the CA and re-
PATCHit on every rotation — and across many trust domains, or cross-cluster where federation only shares roots, that quickly stops being workable.This PR adds an opt-in mode that lets you pin the stable root instead and have Infisical validate the chain the client presents up to it. The motivation is written up in more detail in #7020.
What this changes
A new per-identity option,
verifyClientCertificateChain, defaulting tofalse:false(default): unchanged — the configured CA must be the leaf's direct issuer.true: the configured CA is treated as a trust anchor. The client presents itsleaf + intermediate(s)(which a SPIRE X.509-SVID already is), and login builds a path from the leaf through those intermediates up to the anchor (RFC 5280-style): issuer/subject + signature at each hop, CA basic-constraints on issuers, and validity windows on every certificate involved.Nothing else about the flow moves. The intermediates already arrive in the same client-certificate header —
extractX509CertFromChainparses a multi-cert PEM today, login just ignored everything past the leaf. In chain mode it now uses the rest as candidate issuers. No proxy or header changes.On the surface: a toggle on the Advanced tab of the TLS Cert Auth form (off by default), with the same value shown in the read-only auth-method view.
Security
I wanted to be sure this doesn't loosen anything from #6913, so the new branch is deliberately narrow:
false, so existing identities behave exactly as before — same code path, sameca_verification_failedreason code.Testing
Unit tests cover the path builder against real
leaf -> rotating-intermediate -> stable-rootchains: a valid chain to the root anchor, a leaf issued directly by the anchor, a missing intermediate, the wrong root, a forged extra intermediate presented alongside the real one, and expired / not-yet-valid leaf and intermediate. Backend and frontend both type-check and lint clean.I also ran it end to end against a running instance (migration applied, real HTTP logins) and confirmed both the new behavior and that the defaults are untouched:
falseca_verification_failedca_certificate_not_allowedca_certificate_not_allowedType
Checklist
Closes: #7020