Skip to content

Egress policies can inject tokens from RFC 8693 token exchange - #3349

Open
Max Thompson (thompsonmax) wants to merge 10 commits into
agent-substrate:mainfrom
thompsonmax:token-exchange-e2e
Open

Max Thompson (thompsonmax) wants to merge 10 commits into
agent-substrate:mainfrom
thompsonmax:token-exchange-e2e

Conversation

@thompsonmax

@thompsonmax Max Thompson (thompsonmax) commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1661. Stacked on #3345. The new commits are "e2e: test RFC 8693 token exchange through the egress gateway" and "docs: document injecting RFC 8693 exchanged tokens".

  • testserver gains POST /token, a fake RFC 8693 endpoint. It checks the grant and token types and that audience is the subject JWT's only aud, and issues exchanged.<sub>.<scope> without verifying the JWT.
  • egresscredinject deploys it as a second origin, sts, whose https and http rules exchange actor JWTs at its own /token, and checks the echoed token on both legs. A separate origin keeps the existing echo checks independent of the dataplane: agentgateway, which doesn't exchange tokens, must deny the fetch with a 403 (AtenetDataplane.InjectsExchangedTokens). The suite also checks that ateapi rejects a policy whose token URL no https rule allows.
  • hack/setup-e2e-egress-tls-kind.sh gives the Envoy gateway's ext-proc --token-exchange-ca-file with the service-DNS trust bundle it already mounts.
  • docs/egress-credential-injection.md documents tokenExchange, its outcomes, private token endpoints, and a Google Cloud Workload Identity Federation recipe. T-29 in the threat model says where exchanged tokens live.

The recipe's Google Cloud limits (a 127-character google.subject, 8-key JWKS uploads) come from Google Cloud's docs. The agentgateway 403 row rests on integration tests against agentgateway, not on code in this repo.

Testing: unit tests for /token. A module-wide go test -race ./... passes.

AI use: AI-assisted (Claude Code) for code, tests, and docs.

  • Tests pass
  • Docs updated

A replace_headers entry can now set token_exchange, naming an RFC 8693
token endpoint, an audience, and scopes. The egress gateway will exchange
the actor's JWT there and inject the returned access token.

ateapi requires token_url to be https with a DNS hostname and no
userinfo, query, or fragment. It also rejects a policy unless one of its
https rules allows the token_url's host and port, so the endpoint stays
within what the policy already allows. EgressPolicyTemplate gets the same
check.

The gateway does not exchange tokens yet and denies a request that
carries a token_exchange header with a 500. Without that case the entry
would reach the credential provider with an empty URI.

Signed-off-by: Max Thompson <maxthomp@google.com>
Egress policies will let the gateway exchange an actor JWT for an access
token at an RFC 8693 endpoint. Add the client for that. It posts the token
exchange form, caps the response at 1 MiB, follows no redirects, ignores
proxy environment variables, and maps endpoint errors to gRPC codes that
the gateway can turn into a 403, 500, or 503.

Add --token-exchange-ca-file for endpoints with private certificates. The
router builds the client at startup so a bad file fails startup. Nothing
calls the client yet.

Signed-off-by: Max Thompson <maxthomp@google.com>
Add tokenexchange.Broker, which mints a 300-second actor JWT bound to the
exchange's audience, exchanges it, and caches the access token per actor,
token URL, audience, and scopes. A token is reused until a third of its
lifetime or 30 seconds remains, whichever comes first, and for 60 seconds
when the endpoint returns no expires_in.

Concurrent requests for one token share a single exchange. The exchange
outlives a caller that gives up, so a slow endpoint still fills the cache
for the next request, and it has its own 10-second deadline so an endpoint
that never answers cannot hold the exchange forever. Mint failures wrap
ErrMint so the gateway can map them like other actor JWT mint failures.

Signed-off-by: Max Thompson <maxthomp@google.com>
TestSecretLikeFieldsAreLabeled flags fields whose names look like
credentials. token_exchange holds an endpoint, an audience, and scopes,
and token_url can carry no userinfo or query, so neither holds a secret.

The token URL check now skips headers that use another source, and the
token_url doc names the DNS hostname rule. The http-rule test case now
fails on the protocol alone, and the scopes limits have test cases.

Signed-off-by: Max Thompson <maxthomp@google.com>
A malformed token response's JSON error can quote the endpoint's own
text, such as an expires_in number a megabyte long. Clip it like the
other endpoint-supplied error fields.

Exchange's doc now names each error code by outcome. New tests cover DEL
and a space in access_token and a truncated response body, and the
deadline test uses an expired context instead of waiting.

Signed-off-by: Max Thompson <maxthomp@google.com>
Each token_exchange entry costs a pass over every rule when ateapi
checks that an https rule allows its token URL. A policy with thousands
of entries could cost seconds of CPU. Reject more than 16, the same
bound as one rule's replace_headers, before running the check.

Signed-off-by: Max Thompson <maxthomp@google.com>
A replace_headers entry with token_exchange now gets the access token the
broker exchanges for the actor, where the gateway used to deny it with a
500. The gateway first checks that an https rule in the actor's policy
allows the token URL's host and port, as ateapi does at admission, and
denies with a 500 if none does.

An endpoint that refuses the actor denies the request with a 403, a
transient endpoint failure with a 503, and a misconfigured endpoint with
a 500. A failed mint of the subject JWT maps the same way as for actor
JWT injection.

Signed-off-by: Max Thompson <maxthomp@google.com>
The egress gateway's --token-exchange-ca-file adds PEM certificates it
trusts for token exchange endpoints with private certificates. ate-setup
server-side applies the egress Deployment on every install, so a manual
patch of the ext-proc args would not survive the next one.

When the setting names a ConfigMap, the installer mounts its ca.crt into
the ext-proc container and sets the flag. The admin owns the ConfigMap:
deploy ate-system and deploy atenet fail before changing the cluster if
it or its ca.crt key is missing. The setting requires the Envoy
dataplane.

Signed-off-by: Max Thompson <maxthomp@google.com>
testserver gains POST /token, a fake RFC 8693 endpoint. It checks the
grant and token types and that the audience is the subject JWT's only
aud, and issues "exchanged.<sub>.<scope>" without verifying the JWT.

egresscredinject deploys it as a second origin, sts, whose https and http
rules exchange actor JWTs at its own /token. A fetch of the sts origin
must echo the token issued for this actor on both legs. Agentgateway,
which does not support token exchange, must deny it with a 403. The suite
also checks that ateapi rejects a policy whose token URL no https rule
allows.

hack/setup-e2e-egress-tls-kind.sh gives the Envoy gateway's ext-proc
--token-exchange-ca-file with the service-DNS trust bundle it already
mounts, so it trusts the sts origin's certificate.

Signed-off-by: Max Thompson <maxthomp@google.com>
Egress policies can now exchange an actor JWT for an access token at an
RFC 8693 token endpoint and inject it. Document the tokenExchange fields
and limits, the rule that an https rule in the same policy must allow the
token URL, what the gateway sends and accepts, how long it reuses a
token, and the outcomes in the gateway's table. Add the admin setting for
endpoints with private certificates, a Google Cloud Workload Identity
Federation recipe, and the 127-character google.subject limit that long
atespace and actor names hit.

Note exchanged tokens in the threat model's credential leak entry, and in
the API guide's list of what calls MintActorJwt.

Signed-off-by: Max Thompson <maxthomp@google.com>
@thompsonmax
Max Thompson (thompsonmax) marked this pull request as ready for review November 13, 2026 23:47

This branch has not been deployed

No deployments
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.

Credential Injection - Support RFC 8693 token exchange of the Actor JWT

1 participant