Repository navigation
Description
Description
delegator-oauth covers delegation using RFC 8693 token exchange. For example, a legacy service sits behind one static API key. Or a third-party service like GitHub expects a personal scoped credential that the user creates themselves. Neither has an exchange endpoint to call.
The request is to add a TokenDelegate handler that resolves a downstream credential from Vault at delegation time. For example, Johnny creates a scoped GitHub token, stores it in Vault under his own identity, and his agent's calls are attached to it only while Johnny's inbound token is what authorized the call. Another case would be an agent with an SVID-JWT token, getting exchanged for a downstream legacy api key.
The load-bearing decision is how PPE authenticates to Vault. Where the credential belongs to a caller, PPE logs in to Vault with the caller's own token (Vault JWT auth, policy templated by the authenticated entity) so Vault enforces the per-user isolation. The alternative, PPE authenticating as itself and then choosing the caller's path, leaves PPE holding a credential that can read every user's secret and reduces the isolation to a PPE-side conditional. Only the shared-credential case authenticates as PPE.
Vault auth method follows DelegationSubject:
| subject | Vault login | reads |
|---|---|---|
user |
JWT auth with the inbound user token | that user's path |
caller_workload |
JWT auth with the caller's JWT-SVID (actor_token) |
that workload's path |
client |
JWT auth with the client credential | that client's path |
this_workload |
PPE's own AppRole, Kubernetes, or X.509-SVID cert auth | a shared path |
Scope for this slice is KV v2 reads only. Dynamic secrets, where Vault mints a leased credential per call, are the natural follow-up and are what AttenuationConfig would actually be able to honor.
This is independent of #66. That issue resolves secrets into PPE's own configuration at startup; this one resolves a different credential per request per caller. The two share only a Vault HTTP client, which #66 R19 already keeps private so this can depend on it or fork it.
Acceptance criteria
- A
delegator-vaultbuiltin registers aTokenDelegatehandler and is available behind its own facade feature, absent from the default build. - All Vault calls go through the host-supplied
HttpTransport. No Vault SDK, no second HTTP stack. A KV read is idempotent for retry purposes; a login is not. - Config selects the Vault auth method per subject, explicitly, with no default.
- For
user,client, andcaller_workload, PPE logs in to Vault with that principal's token and never holds a Vault token that can read another principal's path. - The secret path is templated from a configured claim, and the claim used is operator-configurable rather than assumed to be
sub. - A resolved credential populates
RawDelegatedTokenwith the configuredoutbound_header, so a service expectingX-API-Keyworks as well as one expectingAuthorization. expires_atis set from the handler's cache TTL, since a KV secret carries no expiry of its own. The configured TTL is the documented upper bound on how long a revoked credential stays usable.- The credential cache is keyed by the resolved principal identity. A test asserts one caller cannot be served another caller's credential from cache.
- A caller with no secret at their path is distinguishable from a Vault failure, and produces a deny that names the missing enrollment rather than a generic error.
- Vault tokens, inbound tokens, and resolved credentials never appear in errors, logs, serialized state, or
Debugoutput. - Delegation runs only after the policy decision allows the call.
- Tests cover each subject mapping, path templating, a missing secret, a Vault outage, cache isolation between callers, and rotation of the stored secret.
Open questions
- Enrollment. How the credential gets into Vault is out of scope, but the path convention is a contract between whatever Johnny uses to store it and this handler's config. Needs to be written down even though no code implements it.
- Missing secret should probably elicit, not deny. PPE has an elicitation seam. "Your agent needs GitHub access, enroll a token" is a better answer than a 403, and it is the interaction that makes this feature feel finished. Out of scope here; worth designing the deny path so it can become an elicitation later.
scopesonRawDelegatedToken. A stored PAT has no discoverable scopes, and the framework documents monotonic narrowing as an invariant thatTokenDelegateenforces. Needs a decision on what an empty scope list means before this ships.AttenuationConfigcannot be honored for a static stored credential. Decide whether a route that requests attenuation against this handler fails config validation or is ignored with a warning.- Feature name.
vaultcollides with feat(secrets-vault): resolve API keys from Vault KV v2 #66'ssecrets-vaultin the facade's short-name convention. - Scheme prefixes.
RawDelegatedTokencarries one header name and one opaque value. Whether the handler or the consuming plugin owns aBearerortokenprefix is unspecified.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
feat(delegator-vault): add Vault KV v2 delegation handler (praxis-pro…
feat(delegator-vault): add Vault KV v2 delegation handler (praxis-pro…
feat(delegator-vault): add Vault KV v2 delegation handler (praxis-pro…
feat(delegator-vault): add Vault KV v2 delegation handler (praxis-pro…
feat(delegator-vault): mint downstream credentials from Vault KV v2 (#…