Skip to content

perf: cache OIDC verification keys in k8sjwt#391

Open
botengyao wants to merge 1 commit into
agent-substrate:mainfrom
botengyao:k8sjwt-key-cache
Open

perf: cache OIDC verification keys in k8sjwt#391
botengyao wants to merge 1 commit into
agent-substrate:mainfrom
botengyao:k8sjwt-key-cache

Conversation

@botengyao

@botengyao botengyao commented Jul 4, 2026

Copy link
Copy Markdown

Previously every k8sjwt.Verify call performed a full OIDC discovery (two HTTP round trips: the discovery document plus the JWKS) against the issuer. In JWT auth mode that cost was paid on every authenticated RPC, and MintJWT paid it again for the client token.

Introduce k8sjwt.Verifier, which caches each issuer's verification keys in memory. Keys are only refetched when a JWT presents an unknown key ID (i.e. on key rotation). Since the triggering key ID comes from an unverified token, refetches are rate-limited to one per issuer per 10s so bogus key IDs cannot force a fetch storm, and concurrent misses are coalesced with singleflight. The auth interceptor and the session-identity service share one Verifier.

  • Tests pass
  • Appropriate changes to documentation are included in the PR

@botengyao
botengyao marked this pull request as ready for review July 4, 2026 05:05
Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go
Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go Outdated
flight singleflight.Group

mu sync.Mutex
issuers map[string]*issuerKeys

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it a concern that we could end up with a lot of entries cached over time? Do we need to proactively clear out old entries at all?

@botengyao botengyao Jul 8, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

good point, the eviction for issuer should be bounded by the config now (the expected issuer), but there could be a real gap - revoked keys never aged out and added a 5-minute max-age so revoked keys stop being trusted within one interval, PTAL.

Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go Outdated
Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go Outdated
Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go Outdated
Comment thread cmd/ateapi/internal/k8sjwt/k8sjwt.go Outdated
Previously every k8sjwt.Verify call performed a full OIDC discovery
(two HTTP round trips: the discovery document plus the JWKS) against
the issuer. In JWT auth mode that cost was paid on every authenticated
RPC, and MintJWT paid it again for the client token.

Introduce k8sjwt.CachedVerifier, which caches each issuer's
verification keys in memory:

- Keys are fetched on first use and refetched synchronously when a JWT
  presents an unknown keyID (key rotation). Since that keyID comes
  from an unverified token, refetches are rate-limited to one per
  issuer per 10s so bogus keyIDs cannot force a fetch storm, and
  concurrent misses are coalesced with singleflight.
- Cached keys older than 5m trigger an asynchronous refresh so keys
  removed from the JWKS (revoked) age out; requests are served from
  the current cache while the fetch runs (stale-while-revalidate).
- The issuers map is bounded by the configured trusted issuers:
  Verify rejects other issuers before consulting the cache.

The auth interceptor and the session-identity service share one
CachedVerifier. The package-level Verify remains as an uncached
one-shot wrapper.
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.

2 participants