First-request 403: cold exchange exceeds the gateway's 200ms ext_authz timeout (cilium#48354) #53
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lab/authz-bridge#53
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The first RFC 8693 token exchange to a freshly-Ready replica is dropped: the
client gets a 403 (empty body,
server: envoy) at ~230ms. This is the residualcold-start denial that #51's readiness gate + retry reduced but did not
eliminate. The user's position is firm: zero dropped requests, not "fewer."
Repro (live,
portablecluster): deploy a fresh pod, wait for Ready, then send thefirst exchange request with the service-account token. It 403s; the second request
(cache hit) is 200 in ~80ms. Consistent across fresh pods (229/238/239ms).
Root cause (measured + source-confirmed)
The gateway's ext_authz call is hard-limited to 200ms, and a cold exchange is
~330ms. The 200ms is not configurable anywhere we control.
source/extensions/filters/http/ext_authz/ config.hhasstatic constexpr uint64_t DefaultTimeout = 200;(ms), applied whenenvoy.config.core.v3.GrpcService.timeoutis unset. It is a per-request Envoyasync-client timer on the gRPC
CheckRPC — not a cluster/route/gRPC-protocoldeadline.
operator/pkg/model/translation/envoy_http_connection_manager.go,buildExtAuthzHTTPFilter(): the GRPC branch buildsextauthzv3.ExtAuthzwithouta
Timeout, while the HTTP branch setsTimeout: 10s. The asymmetry is anoversight, not a design choice.
ExternalAuthfilter (v1.6.1) has notimeoutfield — not under
externalAuth,.grpc, or.http. Rule-leveltimeouts.request/backendRequestare upper bounds (15s default) and don't extendthe 200ms ext_authz timer.
failure_mode_allowdefaults false,status_on_errorunset →Forbidden). That matches the observed response exactly.~230ms = 200ms + ~30ms overhead.
(status carried in the
CheckResponse); the bridge returns 503 on exchange failure,403 only for unknown host. The empty-body
server: envoy403 is the gateway'sfail-closed, not a bridge denial.
Why the cold exchange is ~330ms (kanidm logs + source)
The dominant cost is inside kanidm, not the connection. From kanidm's structured
timing logs:
The cold cost is kanidm's DB read of the service account (
internal_search_uuidon the SAT's
account_id) — a cold page/block-cache read that cools back down overtime (re-cooled within ~35min between measurements). Token issuance adds a constant
~43–87ms. So cold ≈ 200ms server-side + ~30ms network ≈ 230ms; warm ≈ 50ms
server-side ≈ 100ms total.
Why the existing fixes don't cover this
servable), but the pod becomes Ready before the IdP's server-side state is warm.
Readiness can guarantee config is loaded; it cannot guarantee the IdP is fast.
Warmer) — removes the ~27ms handshake, butthe ~300ms is IdP server-side processing, which no bridge-side connection warming
can touch.
WithoutCancelretry — warms the per-replica cache (and the IdP state) forthe next request, but completes into a dead stream: the gateway already sent the
403 at 200ms. No in-bridge retry can save the in-flight request.
Workarounds that do NOT work
CiliumEnvoyConfigto add atimeout: the operatorreconciles the Gateway-owned CEC and reverts manual edits in <1s (controller owner
reference). Confirmed both in source and in the upstream issue below.
before the service-account DB read, so it never warms the account's block — and the
bridge doesn't hold a valid SAT to pre-exchange anyway.
Upstream
cilium/cilium#48354 (open CFP,
no assignee/milestone/PR) describes this exact bug for the OIDC code-exchange case.
Our case is RFC 8693 token exchange against Kanidm — a second independent data
point for the same "synchronous outbound IdP call inside
Check()" GEP-1494 use case.The issue's proposed minimal fix is to set
GrpcService.Timeout(10s, matching HTTPmode) in the GRPC branch of
buildExtAuthzHTTPFilter().Fix options (open — needs a decision)
GrpcService.Timeout: 10sin the GRPC branch(matches HTTP mode). Unblocks us after a Cilium upgrade; small and easy to review.
The "configurable" follow-up (Gateway API field or Cilium annotation) is larger.
returns OK+302 (within 200ms) to a bridge HTTP exchange endpoint; the slow exchange
happens there (15s route timeout, no ext_authz limit); the client follows the
redirect and gets the resource. Zero 403s. Mirrors the OIDC flow's existing redirect
pattern. Complexity: multi-replica cache coherence (a retry could land on a different
cold replica) — needs a shared cache or a bounded redirect.
read is always warm (~7ms), making even a cold exchange ~100ms total. Outside this
repo.
warm cache. Rejected: the client still sees a 403, which is unacceptable.
Current state
The 4-gate readiness (
Bootstrapped+Servable+ listeners bound + IdP connectionswarmed) and the TLS
Warmerare implemented and live onportable(working tree,uncommitted). The first cold exchange still 403s for the reasons above
cilium grpc timeoutto First-request 403: cold exchange exceeds the gateway's 200ms ext_authz timeout (cilium#48354)