Vault-backed SecretProvider (op-infra Step 5 phase 2)
Context
Op-infra plan Step 5 phase 1 shipped canopy-secrets with SecretProvider trait + EnvSecretProvider (env-var-backed) + structured audit logging on every secret access. Phase 2 (this issue) adds the production-grade backend: HashiCorp Vault.
Phase 1 is wired into canopy-api's bootstrap, so every service already routes database_url / rabbitmq_url reads through the trait. Phase 2 is a one-construction-site swap once Vault is provisioned.
Acceptance criteria
- New
VaultSecretProviderincrates/canopy-secrets/src/vault.rs(or sibling crate if dependency churn warrants):- Authenticate via AppRole (production) or Kubernetes auth (in-cluster) to a Canopy-owned Vault namespace.
- Resolve secrets from a KV-v2 mount (e.g.
kv/canopy/<env>/). - Cache values per-process with a configurable TTL per secret name (short for rotating credentials, long for static values).
- Emit the same
target = "canopy.secrets"audit-log shape withsource = "vault".
bootstrapselects the provider via env var (e.g.CANOPY_SECRETS_BACKEND=env|vault):env→EnvSecretProvider(current default)vault→VaultSecretProviderconstructed fromVAULT_ADDR+ auth-method config
- End-to-end test against a Vault dev server (
vault server -dev):- Write
kv/canopy/dev/CANOPY_SNAP__DATABASE_URL=postgres://... - Boot canopy-snap with
CANOPY_SECRETS_BACKEND=vault - Assert it connects to the configured DB
- Write
- Phase 2 portion of
runbooks/secret-management.adocfilled out (currently a placeholder with the Phase 2 outlook).
Out of scope
- Live rotation without restart (separate concern — depends on the service's connection-pool churn semantics; the Vault provider can re-fetch on every
get()but the consumer also needs to rebuild long-lived handles likePgPool). - Vault HA cluster operation (operational deployment, not application code).
- Sealed-secrets operator support (alternative to Vault, would be a phase 3).
Source
Op-infra plan Step 5 phase 2; phase 1 shipped 2026-04-30.