Skip to content

feat(identity-tls-cert-auth): optional trust-anchor chain verification for client certificates#7021

Open
sinnwise wants to merge 3 commits into
Infisical:mainfrom
sinnwise:feat/tls-cert-auth-chain-verification
Open

feat(identity-tls-cert-auth): optional trust-anchor chain verification for client certificates#7021
sinnwise wants to merge 3 commits into
Infisical:mainfrom
sinnwise:feat/tls-cert-auth-chain-verification

Conversation

@sinnwise

@sinnwise sinnwise commented Jun 24, 2026

Copy link
Copy Markdown

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-PATCH it 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 to false:

  • 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 its leaf + 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 — extractX509CertFromChain parses 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:

  • The branch only decides how the leaf is anchored to the CA. The end-entity-leaf requirement (a CA cert presented as the client cert is rejected), the expiry / not-yet-valid checks, and the CN / SAN (feat: add SAN validation for workload identity #6957) checks all sit outside the branch and run in both modes.
  • The configured CA is still the only trusted certificate. A forged or unrelated intermediate can't construct a path to it, so it's rejected.
  • The column defaults to false, so existing identities behave exactly as before — same code path, same ca_verification_failed reason code.
  • The client certificate is still sourced from the trusted-proxy header; the nginx configs are untouched.

Testing

Unit tests cover the path builder against real leaf -> rotating-intermediate -> stable-root chains: 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:

scenario expected result
attach with the flag omitted stored false
default single-hop, CA = direct issuer, leaf 200
default single-hop, CA = root (not the issuer), leaf 401 ca_verification_failed
default mode, CA cert presented as the client cert 401 ca_certificate_not_allowed
chain mode, leaf + intermediate to root anchor 200
chain mode, leaf only (no intermediate) 401
chain mode, CA cert presented as the client cert 401 ca_certificate_not_allowed

Type

  • Feature

Checklist

  • Title follows the conventional commit format
  • Tested locally (unit + end-to-end against a running instance)
  • Updated docs
  • Read the contributing guide

Closes: #7020

…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.
@sinnwise sinnwise force-pushed the feat/tls-cert-auth-chain-verification branch from bd876ed to 5a9a460 Compare June 24, 2026 21:39
@sinnwise sinnwise marked this pull request as ready for review June 25, 2026 12:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-fns.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an opt-in verifyClientCertificateChain flag to TLS Certificate Auth that lets operators pin a stable root CA as a trust anchor and accept client-presented certificate chains (leaf + intermediates) validated up to it via RFC 5280-style path building, rather than requiring the configured CA to be the leaf's direct issuer. The default remains false, keeping existing behavior unchanged.

  • Core chain verifier (identity-tls-cert-auth-fns.ts): DFS path builder with cryptographic signature checks at every hop, CA basic-constraints enforcement on intermediates, validity windows checked on all certificates including the trust anchor, and a used Set to prevent cycles. Security-sensitive checks (end-entity requirement, CN/SAN matching) run outside the new branch and apply in both modes.
  • Database + API: Single idempotent migration adding a NOT NULL DEFAULT FALSE boolean column; exposed on both ATTACH and UPDATE endpoints; included in audit-log metadata.
  • Frontend: Switch toggle on the Advanced tab with a tooltip, schema default, and form reset all consistently using false.

Confidence Score: 4/5

The new chain-verification path is narrowly scoped — it only changes how the leaf is anchored to the configured CA, leaving the end-entity check, validity checks, and CN/SAN matching untouched and running in both modes. The trust model (only the configured CA is trusted) is correctly enforced through cryptographic signature verification at each hop.

Two observations, both non-blocking: the issuedBy issuer/subject string comparison can silently return false for certificates from PKIs with different DN encodings (fine for SPIRE but a latent surprise for other PKIs), and the leaf validity checks after chain verification are unreachable in chain mode while producing slightly different error messages than the chain verifier does for the same scenario. Neither affects correctness for the described use case.

identity-tls-cert-auth-fns.ts for the DN string-comparison caveat in issuedBy; identity-tls-cert-auth-service.ts for the unreachable leaf-validity checks in chain mode.

Important Files Changed

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)

  1. backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts, line 194-216 (link)

    P2 Leaf validity checks are unreachable and produce different error messages in chain mode

    When verifyClientCertificateChain is true, the call to verifyClientCertificateChain(...) already checks the leaf's validity (lines 142-162) and throws UnauthorizedError with "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 separate new 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Identity TLS Cert Auth: optionally validate the client certificate chain against the configured CA as a trust anchor

1 participant