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

  1. New VaultSecretProvider in crates/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 with source = "vault".
  2. bootstrap selects the provider via env var (e.g. CANOPY_SECRETS_BACKEND=env|vault):
    • envEnvSecretProvider (current default)
    • vaultVaultSecretProvider constructed from VAULT_ADDR + auth-method config
  3. 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
  4. Phase 2 portion of runbooks/secret-management.adoc filled 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 like PgPool).
  • 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.