Implement X.509-SVID OAuth client authentication (spiffe_x509)
@jhrozek is already working on this.
Since Aug 14, 2026.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Description
Implement the spiffe_x509 client-authentication method: a workload presents
its X.509-SVID on a mutually authenticated TLS connection, and the
authorization server authenticates the OAuth client from the verified
certificate instead of from a shared secret.
This is the path the spiffee-authserver branch proved out, and the reason
#6082 called option D the strongest answer for Kubernetes: there is no secret
to create, mount, rotate or revoke, and the client ID is the SPIFFE ID, so it
is knowable before the workload ever connects.
Scope
- Add TLS and optional mutual-TLS listener support to the relevant proxy
transports. - Validate X.509-SVID chains against the configured trust-domain bundle from
sub-issue 2. - Extract the SPIFFE ID from the certificate and resolve it through the shared
client-association policy from sub-issue 1. - Require an explicit
client_idassociated with the presented SPIFFE ID. - Advertise
spiffe_x509only when the method is fully configured and
operational. - Keep authorization-server certificate validation separate from SPIFFE workload
certificate validation — the AS's own WebPKI trust is not the workload trust
bundle.
Prior art
The branch implemented this as a custom fosite.ClientAuthenticationStrategy
that authenticates from a verified X.509-SVID and delegates to the default
strategy when no SPIFFE ID is present, which is what leaves browser and
public-client flows untouched. Clients auto-register just-in-time with
Public: false and no secret, enabled by a three-line seam in shared code
(registration.Config.SkipSecretHash).
Fail-closed was verified on the branch: a caller presenting
client_id=spiffe://… without a client certificate falls through to the
default strategy and fails there. The public-client rejection was not relaxed.
Preserve both properties.
Two weaknesses on the branch must be fixed, not carried: a nil policy meant
allow-all, and auto-registered clients received every supported scope and every
allowed audience. Per-identity narrowing comes from sub-issue 1's association
policy.
Acceptance criteria
Certificate shape:
- The certificate is a leaf certificate with
CA=false. - The certificate has the
digitalSignaturekey-usage bit. - The certificate has exactly one URI SAN containing a valid, non-root SPIFFE
ID. Additional DNS SANs are allowed; additional URI SANs are rejected. - The chain validates against the configured trust-domain bundle.
Client resolution:
- The request contains an explicit
client_idassociated with the certificate's
SPIFFE ID.
Fail-closed:
- A missing certificate cannot produce a SPIFFE-authenticated principal.
- A malformed or unauthorized certificate cannot fall back to public or
secret-based client authentication. - Existing authorization-code and browser flows continue to work where
configured.
Dependencies
- Sub-issue 1 (identity, association and configuration model)
- Sub-issue 2 (trust-domain bundle management) — for chain validation material
Can proceed in parallel with the JWT-SVID sub-issue once both foundations exist.
Coordination with the sibling credential type
#6202 and #6203 replace the same fosite hook: a single
ClientAuthenticationStrategy
(func(context.Context, *http.Request, url.Values) (fosite.Client, error)).
#6202 dispatches on a SPIFFE ID placed in the request context by the mTLS
listener; #6203 dispatches on client_assertion_type in the request form.
That dispatch is introduced once, separately, as a no-behaviour-change change
before either issue starts — both SPIFFE arms stubbed, everything else falling
through to fosite's default strategy. This issue owns one arm only and must
not modify the dispatch itself. A needed change there goes back to the shared
change rather than being patched in place.
Two things to know when filling in an arm:
- Fosite's default strategy already dispatches on
client_assertion_typefor
RFC 7523urn:ietf:params:oauth:client-assertion-type:jwt-bearer. The SPIFFE
assertion type is different and must be matched exactly, or existing
private-key-JWT client authentication breaks. - Both arms resolve through
SPIFFEAssociationRegistry.Resolvefrom sub-issue
1, which already fails closed on unknown association, wrong client ID, and
disabled method. Do not add a parallel lookup — that registry is what makes
the "equivalent authorization outcome" guarantee enforceable rather than
merely tested.
Related
- Part of the SPIFFE client-authentication epic
- #6082 (option D)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.