Caching and API-server protection
Two request paths in the proxy turn client input into round trips to the
Kubernetes API server: validating a passthrough token costs a TokenReview,
and authorizing inbound impersonation (kubectl --as) costs one
SubjectAccessReview per impersonation header value. Left unbounded, both
scale with client behaviour — a request storm of invalid tokens, or a single
request stuffed with impersonation headers, becomes API-server load.
Three mechanisms bound that load:
| Mechanism | Flags | Defaults |
|---|---|---|
| TokenReview result cache | --token-passthrough-cache-success-ttl, --token-passthrough-cache-failure-ttl | 10s / 10s |
| SubjectAccessReview decision cache | --subject-access-review-cache-allow-ttl, --subject-access-review-cache-deny-ttl | 10s / 10s |
| Impersonation header value cap | --max-impersonation-header-values | 64 |
The caches follow the precedent set by k8s.io/apiserver for delegated
authentication and authorization: 10-second TTLs, definitive results only —
errors are never cached — and 0 disables. Both caches are bounded LRU
caches of at most 8192 entries, so client-influenced input can never grow
the proxy’s memory without bound.
- TokenReview result cache
- SubjectAccessReview decision cache
- Impersonation header value cap
- Observing the caches
- Tuning
- See also
TokenReview result cache
Section titled “TokenReview result cache”Applies only with --token-passthrough.
When a bearer token fails OIDC validation, the proxy falls back to a
TokenReview against the API server. Without a cache, every request carrying
the same failing token — for example during an OIDC issuer outage — becomes its
own API-server round trip. With it, repeated requests are answered from memory:
flowchart LR
C[Client] --> O{OIDC auth}
O -- ok --> F[Impersonate + forward]
O -- fail --> T{TokenReview<br>cache}
T -- hit --> D[Allow / 401]
T -- miss --> R[TokenReview<br>to API server] --> D
- Split TTLs. Successful reviews are cached for
--token-passthrough-cache-success-ttl, unauthenticated results for--token-passthrough-cache-failure-ttl. Either can be0to disable that class; with both at0the reviewer runs exactly as if the cache did not exist. - Errors are never cached. An unreachable API server or a timed-out review fails only the request that saw it and is retried on the next one — the path stays fail-closed.
- Tokens are never stored as keys. Cache keys are an HMAC-SHA256, keyed with a per-instance random key, over the token and configured audiences — so keys cannot be precomputed and a memory dump does not yield tokens.
- Concurrent misses each run their own review — deliberately. A miss runs
on the caller’s own context, so the full configured
--token-passthrough-request-timeoutapplies and cancelling the request cancels its review. The cost is a few duplicate reviews when many requests present the same not-yet-cached token at the same instant; the first to finish populates the cache and everything after is a hit. The cache only ever subtracts load.
SubjectAccessReview decision cache
Section titled “SubjectAccessReview decision cache”When an inbound request carries Impersonate-* headers, the proxy verifies via
SubjectAccessReview that the authenticated user may assume each requested
value (see inbound impersonation).
RBAC grants change rarely, so without a cache most of these calls ask the API
server the same question again and again:
flowchart LR
A[Request with<br>Impersonate-* headers] --> B{Decision<br>cache}
B -- hit --> C[Reuse allow / deny]
B -- miss --> D[SubjectAccessReview<br>to API server]
D -- allow --> E[Cache for allow TTL]
D -- deny --> F[Cache for deny TTL]
D -- error --> G[Never cached]
- Split TTLs, tuned per failure mode. A cached allow means revoking an
RBAC impersonation grant takes up to
--subject-access-review-cache-allow-ttlto be enforced. A cached deny means a newly granted permission takes up to--subject-access-review-cache-deny-ttlto be honoured. Set either to0to re-check that class on every request; both at0disables the cache entirely. - The key is the whole question. Each entry is keyed on the exact
serialized
SubjectAccessReviewSpecsent to the API server — requester username, groups, extras and UID, plus every resource attribute — so two distinct principals or questions can never share a decision. - Oversized questions are never cached. Specs over 10 000 bytes (e.g. absurdly long header values) are authorized live and skipped by the cache, so a client cannot fill it with huge keys.
- Concurrent identical misses share one review (singleflight). If the shared call dies because another request was cancelled, waiters fall back to their own live check.
- A hit behaves exactly like a live review. Cached denials produce the
identical error type, so
errors.Isclassification,403semantics, and fail-closed handling are unchanged.
Impersonation header value cap
Section titled “Impersonation header value cap”Each impersonation header value — the Impersonate-User value plus every
Impersonate-Group, Impersonate-Uid, and Impersonate-Extra-* value — costs
one SubjectAccessReview, and the count is entirely client-controlled. Values
are not deduplicated, and a single request can carry tens of thousands of them
before hitting HTTP header size limits — so a cap, not a cache, is the right
tool. The kube-apiserver tolerates unbounded counts because its per-value check
is in-process; here every value is a network call.
The proxy counts values before sending anything and rejects over-cap requests with HTTP 431 Request Header Fields Too Large:
sequenceDiagram
participant C as Client
participant P as kube-oidc-proxy
participant A as kube-apiserver
C->>P: Request with N impersonation values
alt N > cap (default 64)
P-->>C: 431 Request Header Fields Too Large
Note over P,A: zero SubjectAccessReviews sent
else N <= cap
loop one per value (cache miss only)
P->>A: SubjectAccessReview
A-->>P: Allowed / Denied
end
P->>A: Forward request (impersonated)
end
- Values are counted exactly the way the impersonation handler consumes them
(case-insensitive
Impersonate-prefix, every value of every key), so case variants or duplicate header keys cannot bypass the cap. - At or under the cap, behaviour is byte-for-byte identical to having no cap:
which identities are authorized,
403handling, and auditing are unchanged. - The cap must be greater than
0. Raise it (maxImpersonationHeaderValuesin the chart) only if identities legitimately carry more values; see troubleshooting for the 431 symptom.
Observing the caches
Section titled “Observing the caches”Every cache consultation and every live review is a DEBUG record on the normal
log stream, so -v=1 turns cache behaviour into something you can count rather
than infer. All three carry request_id, so they join the request’s
request.access.decided record.
event_type | Level | Fields | Emitted when |
|---|---|---|---|
cache.tokenreview.lookup | DEBUG | request_id, cache_result; authenticated on a hit | Each consultation of the TokenReview result cache. |
cache.sar.lookup | DEBUG | request_id, cache_result; decision on a hit | Each consultation of the SubjectAccessReview decision cache. Fires once per impersonation header value, so one request can emit several. |
authz.sar.completed | DEBUG | request_id, decision, duration_ms, request_coalesced, target_kind | A live or shared SubjectAccessReview returned — that is, a lookup that did not come from the cache. |
cache_result is one of:
hit— answered from memory, no API-server round trip.miss— nothing cached, or the entry had expired; a live review follows.bypass— the cache was not consulted at all: the TTL is0, or the SAR spec was too large to key. A live review follows.
request_coalesced=true on authz.sar.completed means singleflight shared one
in-flight review between concurrent requests rather than issuing a second one,
which is how a request storm for the same identity stays a single round trip.
duration_ms is the live call duration and is absent on a cache hit.
Neither cache key, the token, nor the serialized SubjectAccessReview spec is
ever logged — the cache identity is in the event type, not in a field.
To see the hit rate for a window, count cache_result on the lookup record:
kubectl -n kube-oidc-proxy logs deploy/kube-oidc-proxy --since=10m \ | jq -r 'select(.event_type == "cache.sar.lookup") | .cache_result' \ | sort | uniq -cSee the logging reference for the full field reference and more worked queries.
Tuning
Section titled “Tuning”| You want | Set |
|---|---|
| Token revocation enforced per-request on the passthrough path | --token-passthrough-cache-success-ttl=0 |
| A just-issued token accepted immediately after a failed attempt | --token-passthrough-cache-failure-ttl=0 |
| RBAC impersonation revocation enforced per-request | --subject-access-review-cache-allow-ttl=0 |
| New impersonation grants honoured immediately | --subject-access-review-cache-deny-ttl=0 |
| Less API-server load from hot identities | Raise the TTLs — and accept the matching revocation/grant lag |
| Identities with more than 64 impersonation values | Raise --max-impersonation-header-values |
See also
Section titled “See also”- Configuration reference — the full flag tables.
- Architecture — where these steps sit in the handler chain.
- Logging reference — the record shape, the event registry and worked queries.
- Operations: security and troubleshooting — symptoms of cache lag and the 431 rejection.