Credential Injection - Support RFC 8693 token exchange of the Actor JWT
@thompsonmax is already working on this.
Since Sep 15, 2026.
Assessment
This issue has not been assessed yet.
Description
Summary
Let an EgressPolicy hostname rule ask the egress gateway to inject a credential obtained by exchanging the actor's JWT at an OAuth 2.0 token-exchange endpoint (RFC 8693). The gateway mints an actor JWT bound to the exchange endpoint's audience, presents it as the subject_token, and injects the returned access_token on the TLS-terminated MITM leg. The actor never sees either token.
Builds on #1660 (actor JWT injection).
Motivation
- Most third-party APIs do not understand Substrate actor JWTs. They understand their own access tokens. Token exchange is the standard bridge: the actor proves who it is with a Substrate-signed JWT, and a trusted STS hands back a credential scoped to that identity.
- This removes the last reason to put long-lived cloud credentials in a Kubernetes Secret for the static-injection path. The credential is short-lived, minted per actor, and derived from verified identity rather than provisioned.
- It answers the "federation" and "OAuth token exchange" use cases raised in #586 and gives the in-cluster gateway question in #1228 a concrete path.
Current state
- Control.MintActorJWT exists but has no production caller, hardcodes iss: https://api.ate-system.svc, issues 15-minute ES256 tokens, and ateapi publishes no OIDC discovery document or JWKS. An external STS cannot verify these tokens today.
- #1660 proposes the gateway-side minting, caching, and header injection this issue reuses.
- The gateway's applyEffects (PR #1360) resolves credentials only through the CredentialProvider RPC. There is no in-process credential source.
Proposal
API
Add an exchange-backed effect alongside inject_static_headers and inject_actor_jwt. Exchange endpoints are configured per install and referenced by name, so a policy never carries endpoint URLs or CA material:
message EgressRuleEffects {
repeated CredentialHeaderInjection inject_static_headers = 1;
repeated ActorJWTHeaderInjection inject_actor_jwt = 2;
// Injects a credential obtained by RFC 8693 exchange of the actor JWT.
// +k8s:optional
// +k8s:maxItems=16
// +k8s:listType=map
// +k8s:listMapKey=header
repeated ExchangedTokenHeaderInjection inject_exchanged_token = 3;
}
message ExchangedTokenHeaderInjection {
// +k8s:required
string header = 1; // unique across all three effect lists
// +k8s:optional
string prefix = 2; // e.g. "Bearer "
// +k8s:required
string exchange_provider = 3; // name of a gateway-configured exchange endpoint
// Audience the actor JWT is minted for and sent as the RFC 8693 `audience`.
// For GCP: //iam.googleapis.com/projects/N/locations/global/workloadIdentityPools/P/providers/X
// +k8s:required
string audience = 4;
// +k8s:optional
repeated string scopes = 5; // RFC 8693 `scope`
}
Gateway configuration (flags or a small config file on atenet-egress) defines each named provider: token endpoint URL, optional CA bundle, subject_token_type (default urn:ietf:params:oauth:token-type:jwt), requested_token_type (default urn:ietf:params:oauth:token-type:access_token), and timeout.
Gateway flow
On the egress_tls_mitm leg, when the first matching rule carries inject_exchanged_token:
- Mint an actor JWT with MintActorJWT(actor, uid, [audience]), reusing the UID resolution and token cache from #1660. The JWT's only audience is the exchange audience, so it cannot be replayed against any other relying party.
- POST to the named provider's token endpoint with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, subject_token=, subject_token_type, audience, scope, and requested_token_type. No actor_token; this is impersonation, not delegation.
- Validate the response (access_token present, issued_token_type as requested) and sanitize the token bytes before they become a header value.
- Cache the exchanged token per (actor UID, exchange provider, audience, scopes) until expires_in minus a safety margin, with singleflight so a burst of requests performs one exchange.
- Set header to prefix + access_token with OVERWRITE_IF_EXISTS_OR_ADD.
- Fail closed on mint or exchange failure. The policy promised a credential.
Latency: the first request for an actor pays mint plus an external round trip, and it must fit inside the ext_proc message timeout (5s today, with a 4s policy-fetch budget). Options: a dedicated exchange timeout, warming the cache on the CONNECT leg when the policy has exchange effects, or raising the MITM route's ext_proc timeout. To be decided in the PR.
Where the exchange runs
Recommended: an in-gateway broker in cmd/atenet/internal/router/egress, keyed by named provider config. It needs no new deployment, no extra hop, and matches the "broker within Substrate" direction from the #1360 review.
Alternative: a CredentialProvider plugin that receives the actor JWT in SecretRequestContext and performs the exchange itself. This keeps the gateway simpler and aligns with the intent that the plugin uses the actor JWT to authenticate externally, at the cost of another deployment and hop. The proposal above does not preclude adding this later.
Prerequisites in ateapi
- Publish OIDC discovery and JWKS for the actor-JWT issuer, with a configurable issuer URL. GCP Workload Identity Federation accepts an uploaded JWK set, so the issuer need not be reachable from Google, but the iss string must still be a valid URL that matches the provider configuration exactly. AWS and Azure federation fetch keys from the issuer, so those targets need a reachable endpoint. The JWKS must include inactive authorities so rotation does not break verification. localjwtauthority.Pool.VerificationKeys already returns the whole pool.
- Caller authorization for MintActorJWT (tracked in #1660).
Security notes
- The actor JWT's sub is atespaces::actors:. On the STS side, attribute mapping and conditions (for GCP, google.subject from assertion.sub, with a 127-character limit) decide which actors may federate. Document a recommended mapping so operators can bind IAM to a single actor or an atespace prefix.
- Exchanged tokens live in the shared gateway process, the same blast radius as static injection. Never log token material; log provider name, audience, actor, and status only.
- The token endpoint must be TLS-verified against system roots or a configured CA. No plaintext endpoints.
Out of scope
- Delegation (actor_token) and requesting token types other than access tokens.
- Service-account impersonation as a second hop (GCP generateAccessToken).
- Conditional or sentinel-triggered injection.
- The agentgateway dataplane.
- Dominant language
- Go
- Stars
- 2k
- Forks
- 333
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 275
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from agent-substrate/substrate
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
agent-substrate/substrate#1750 · 1 comment ·
-
area/dev-infra area/microVM kind/cleanup
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
agent-substrate/substrate#1695 · 2 comments ·
-
area/node area/observability good first issue kind/feature prio/P2
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
agent-substrate/substrate#1647 · 1 comment ·
-
area/cli kind/bug kind/docs
Difficulty 2/5 Half a day Newbie friendliness 78/100
agent-substrate/substrate#1642 ·
-
area/demos kind/bug
Difficulty 1/5 Under an hour Newbie friendliness 85/100
agent-substrate/substrate#1579 · 1 comment ·
All issues in agent-substrate/substrate
Similar issues
-
optimization optimization:agents-md-curator
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
githubnext/gh-aw-cao#13143 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
blinklabs-io/bursa#904 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
yanet-platform/ipfw-go#129 ·
-
bug confmap/provider/googlesecretmanagerprovider needs triage
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
open-telemetry/opentelemetry-collector-contrib#51273 · 2 comments ·
-
bug: AI Gateway client filter lists "Unknown" twice when NULL and literal Unknown clients coexist Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 90/100