Skip to content

added basic provisioner refresh on jwks cache miss - #2769

Draft
APWHY wants to merge 1 commit into
smallstep:masterfrom
APWHY:jwks-reload-on-unknown-kid
Draft

added basic provisioner refresh on jwks cache miss#2769
APWHY wants to merge 1 commit into
smallstep:masterfrom
APWHY:jwks-reload-on-unknown-kid

Conversation

@APWHY

@APWHY APWHY commented Aug 20, 2026

Copy link
Copy Markdown

step-ca only refetches the JWKS from the issuer once the cached set has expired (which comes from the JWKS endpoint's Cache-Control: max-age). This means if the issuer rotates keys all future requests through the affected provisioners will fail until the cache expires naturally.

This PR adds a refresh mechanism that runs at most once every minute if there is an unfamiliar received kid, following the recommendation at: https://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys . Concurrent reload requests are grouped as well.

go-oidc has done something similar where they've completely removed the Cache-control checks entirely, but I wanted to minimise the changes this PR made so I've left them in for now.

An additional fix has been added to getKeysFromJWKsURI so it rejects non 2xx/3xx codes and returns an error. Previously, if the error had a decodeable JSON body it would attempt to do so and empty the keystore (and error out otherwise). It's not entirely related and I can split this out into a separate PR if needed.

@github-actions github-actions Bot added the needs triage Waiting for discussion / prioritization by team label Aug 20, 2026
@APWHY
APWHY force-pushed the jwks-reload-on-unknown-kid branch from e89a5d5 to 37eb0e8 Compare August 25, 2026 07:30
keyStore only refetched the JWK Set once the cached set had expired, and that
expiry comes from the endpoint's Cache-Control max-age. An identity provider
that advertises a long max-age and rotates its signing key mid-window therefore
made every token it issued fail to validate until the cache expired. An Amazon
EKS OIDC issuer serves max-age=604800, so a rotation there breaks validation
for up to seven days, recoverable only by restarting the CA.

Reload when a key id is missing from the cached set, as OpenID Connect Core
10.1.1 recommends. A minimum interval between reloads bounds a sequential
stream of key ids that no reload can resolve, and coalescing bounds a
concurrent burst, as androidCRLCache already does in this package. The
Cache-Control handling is left in place, so the expiry stays a proactive
refresh rather than the only trigger.

The GCP, Azure and OIDC provisioners all share this key store, so all three
are affected.

Also reject a JWKs response carrying an error status code. Such a response
decoded into an empty key set without an error whenever its body was valid
JSON, and reload then installed that in place of a usable key set; reloading
on an unknown key id makes the path far more reachable. Reload failures are
now logged rather than discarded, so a JWKs endpoint that has become
unreachable is visible. The expiry now observes the same minimum interval as
an unknown key id, because it is only advanced by a reload that succeeded: an
expired key set and a failing endpoint otherwise reload, and so log, on every
token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@APWHY
APWHY force-pushed the jwks-reload-on-unknown-kid branch from 37eb0e8 to 8afa09b Compare August 25, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs triage Waiting for discussion / prioritization by team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants