You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
The gateway's applyEffects (PR egress: add credential injector #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 Credential Injection - Support Actor JWT #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.
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).
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
Current state
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:
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:
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
Security notes
Out of scope