Skip to content

perf: cache OIDC verification keys in k8sjwt#391

Open
botengyao wants to merge 3 commits into
agent-substrate:mainfrom
botengyao:k8sjwt-key-cache
Open

perf: cache OIDC verification keys in k8sjwt#391
botengyao wants to merge 3 commits 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

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.

Implements the TODO formerly at k8sjwt.go:177.
@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?

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 you prompted 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
botengyao added 2 commits July 7, 2026 21:01
- Rename Verifier to CachedVerifier so Verifier stays available as a
  future interface name.
- Document why the issuers map is bounded (only configured trusted
  issuers reach the cache; Verify rejects other issuers first).
- Include throttling and the last-fetch time in the throttled-refetch
  error, and the keyID in the discovery error.
- Use "keyID" consistently in messages and comments for searchability.
- Comment why mu.Unlock() is managed manually instead of deferred.
Unknown-keyID refetches only pick up keys added to the issuer's JWKS.
A key removed from the JWKS (revoked) would previously be trusted until
process restart, because tokens signed by it keep hitting the cache.

Cached keys older than keyMaxAge (5m) now trigger an asynchronous
refresh: requests are served from the current cache while the fetch
runs, so the hot path stays free of network I/O, and revoked keys stop
verifying within one interval. Refresh failures keep the previous keys
and are retried after the next interval.
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