Description
Add a Vault KV v2 backend for the SecretProvider trait, registered as a provider kind so a declared secret can resolve from Vault instead of a file or an environment variable. Implement the minimal Vault HTTP API through the host-supplied HttpTransport. No Vault SDK, no generated client, no second HTTP stack: PPE performs no outbound HTTP of its own, and a Vault client built on its own reqwest would bypass the host's TLS, egress, timeout, retry, and response-size policy.
This is #66 narrowed. The trait, the registry, the declared secret block, startup ordering, refresh policy, and the consumer belong to the seam and assertion issues; what is left here is the backend.
Acceptance criteria
- A
vault provider kind registers as a SecretProviderFactory, available behind a secrets-vault facade feature and absent from the default build.
- Reference grammar
<mount>/<path>#<field>, mapped to GET /v1/<mount>/data/<path>, with the field read from data.data. An absent path, absent field, non-string value, or empty value is an error rather than an empty result.
- Every call goes through
HttpTransport. A KV read takes an idempotent retry policy; a login and a renew-self do not, since a retried login that actually succeeded mints a second token and a second lease.
- Auth method is explicit with no default. Kubernetes and AppRole only.
- Kubernetes login rereads the mounted service-account token on every login. Kubelet rotates a projected token in place, so a token cached at startup stops working within the hour.
- AppRole
secret_id comes from an environment variable, a file, or a clearly documented development-only literal.
- The address must be
https:// unless the operator explicitly sets insecure_http.
- A renewable token is renewed before expiry and a non-renewable one is obtained again. A
403 during a read triggers one reauthentication attempt and a retry, distinguished from "the secret does not exist".
- Vault's
{"errors": [...]} envelope is a distinct error from a transport failure. Vault tokens, bootstrap credentials, and retrieved values never appear in errors, logs, serialized state, or Debug output, and are cleared on drop where practical.
- Tests cover both auth methods, field extraction, renewal, reauthentication on
403, rotation, startup failure, refresh failure, and concurrent reads. A pinned integration test documents the Vault server version it was run against.
make audit stays green with no new license exception.
Token renewal and background tasks
Secret refresh belongs to the seam, but the Vault session's own lifecycle does not, and it carries a hazard this repo has already been bitten by. A spawned ticker binds to whichever runtime called initialize(), and a host that initializes on a short-lived runtime has it cancelled before it ticks once. That is issue #29: the JWKS refresh task died silently and rotation never recovered, which is why identity-jwt now refreshes from the verify path instead (builtins/plugins/identity-jwt/src/resolver.rs:487).
Token renewal has no request path to hang off, since after startup nothing calls Vault until the next refresh tick. The reactive reauthentication on 403 covers most of it. This issue should decide whether a renewal timer exists at all, or whether renewal happens lazily on the next read, and say what happens on a host with no long-lived runtime.
The concrete renewal schedule, as a fraction of lease_duration with jitter and a backoff on a failed attempt, also needs a value here rather than at review time.
Out of scope
KV v1, secret writes, dynamic secrets, runtime delegation, and OpenAPI-driven code generation. Per-caller credential resolution is a separate delegator issue and shares only this client.
Depends on
#92
Description
Add a Vault KV v2 backend for the
SecretProvidertrait, registered as a providerkindso a declared secret can resolve from Vault instead of a file or an environment variable. Implement the minimal Vault HTTP API through the host-suppliedHttpTransport. No Vault SDK, no generated client, no second HTTP stack: PPE performs no outbound HTTP of its own, and a Vault client built on its ownreqwestwould bypass the host's TLS, egress, timeout, retry, and response-size policy.This is #66 narrowed. The trait, the registry, the declared secret block, startup ordering, refresh policy, and the consumer belong to the seam and assertion issues; what is left here is the backend.
Acceptance criteria
vaultprovider kind registers as aSecretProviderFactory, available behind asecrets-vaultfacade feature and absent from the default build.<mount>/<path>#<field>, mapped toGET /v1/<mount>/data/<path>, with the field read fromdata.data. An absent path, absent field, non-string value, or empty value is an error rather than an empty result.HttpTransport. A KV read takes an idempotent retry policy; a login and arenew-selfdo not, since a retried login that actually succeeded mints a second token and a second lease.secret_idcomes from an environment variable, a file, or a clearly documented development-only literal.https://unless the operator explicitly setsinsecure_http.403during a read triggers one reauthentication attempt and a retry, distinguished from "the secret does not exist".{"errors": [...]}envelope is a distinct error from a transport failure. Vault tokens, bootstrap credentials, and retrieved values never appear in errors, logs, serialized state, orDebugoutput, and are cleared on drop where practical.403, rotation, startup failure, refresh failure, and concurrent reads. A pinned integration test documents the Vault server version it was run against.make auditstays green with no new license exception.Token renewal and background tasks
Secret refresh belongs to the seam, but the Vault session's own lifecycle does not, and it carries a hazard this repo has already been bitten by. A spawned ticker binds to whichever runtime called
initialize(), and a host that initializes on a short-lived runtime has it cancelled before it ticks once. That is issue #29: the JWKS refresh task died silently and rotation never recovered, which is whyidentity-jwtnow refreshes from the verify path instead (builtins/plugins/identity-jwt/src/resolver.rs:487).Token renewal has no request path to hang off, since after startup nothing calls Vault until the next refresh tick. The reactive reauthentication on
403covers most of it. This issue should decide whether a renewal timer exists at all, or whether renewal happens lazily on the next read, and say what happens on a host with no long-lived runtime.The concrete renewal schedule, as a fraction of
lease_durationwith jitter and a backoff on a failed attempt, also needs a value here rather than at review time.Out of scope
KV v1, secret writes, dynamic secrets, runtime delegation, and OpenAPI-driven code generation. Per-caller credential resolution is a separate delegator issue and shares only this client.
Depends on
#92