KV Engine vs Dynamic Secrets Engine
Architecture
flowchart TB
subgraph consumers["Applications / Consumers"]
app1["App A\nvault read generic/creds/my-role"]
app2["App B\nvault read auth0/creds/my-role"]
end
subgraph spire["SPIFFE / SPIRE Identity Plane"]
spire_server["SPIRE Server\nOIDC provider · JWT-SVID CA"]
spire_agent["SPIRE Agent\nworkload attestation API"]
end
subgraph vault["HashiCorp Vault 1.18+ · TLS :8200"]
engine1["vault-rest-engine\n(Generic REST Plugin)"]
engine3["vault-auth0-engine\n(Auth0 Native Plugin)"]
transit["Transit Engine\ncred-rotation-key\naes256-gcm96"]
pki["PKI Engine\nInternal CA"]
kv["KV v2\nTransit-encrypted\nadapter config"]
jwt_auth["JWT Auth Method\nSPIFFE trust domain"]
end
subgraph broker["Credential Rotation API · mTLS :8443"]
direction LR
a0["Auth0\nAdapter"]
sp["Splunk\nAdapter"]
sq["SonarQube\nAdapter"]
gh["GitHub\nAdapter"]
dd["Datadog\nAdapter"]
pd["PagerDuty\nAdapter"]
end
subgraph saas["SaaS Providers · TLS 1.3"]
auth0["Auth0\nMgmt API"]
splunk["Splunk\nREST API"]
sonar["SonarQube\nWeb API"]
github["GitHub\nTokens API"]
datadog["Datadog\nAPI Keys API"]
pagerduty["PagerDuty\nREST API v2"]
end
app1 -- "TLS 1.3" --> engine1
app2 -- "TLS 1.3" --> engine3
engine1 -- "mTLS\nPKI client cert" --> a0
engine3 -- "TLS 1.3" --> auth0
a0 -- "TLS 1.3" --> auth0
sp -- "TLS 1.3" --> splunk
sq -- "TLS 1.3" --> sonar
gh -- "TLS 1.3" --> github
dd -- "TLS 1.3" --> datadog
pd -- "TLS 1.3" --> pagerduty
engine1 & broker -- "Transit encrypt/decrypt\nenvelope encryption" --> transit
broker -- "Transit-decrypt\nadapter config at boot" --> kv
pki -- "issues client cert\nfor vault-rest-engine" --> engine1
spire_agent -- "workload API\ngRPC unix socket" --> broker
spire_server -- "OIDC JWKS\nJWT-SVID validation" --> jwt_auth
broker -- "POST /v1/auth/jwt/login\nJWT-SVID credential" --> jwt_auth
The AuthZEN Pattern Parallel
Just as AuthZEN standardises the authorization decision interface so any PDP can implement it, the Credential Rotation API standardises the credential lifecycle interface so any SaaS adapter can implement it.
POST /access/v1/evaluation
{ subject, resource, action, context }
→ { decision: bool }
Any PDP implements this contract. Callers don't know if it's OPA, OpenFGA, or an in-memory PDP.
POST /v1/credentials/rotate
{ provider, resource_id, lease_id }
→ { encrypted_value, expires_at }
Any adapter implements this contract. Vault doesn't know — or care — which platform is behind it.
Security Model — Defence in Depth
All Vault API traffic (consumer → Vault). Enforced by Vault; cannot be disabled in production mode.
Plugin → Credential Rotation API. Both sides present certificates. API rejects any client without a cert signed by the internal PKI CA. The cert IS the plugin's identity — no bearer tokens.
New credentials are Transit-encrypted by the API before being sent to the plugin over mTLS. Each rotate call generates a unique DEK (AES-256-GCM) — the same credential rotated twice produces two unrelated ciphertexts. Correlation and replay attacks blocked at the envelope level.
Each adapter's admin credentials (API keys, management tokens) stored in KV encrypted via Transit. The KV backend at rest cannot be read without the Transit master key. Plaintext credentials never touch Vault storage.
Vault's backend storage (Raft) is always encrypted with Vault's own seal key. This baseline applies even before Transit adds its layer.
cred-rotation-api authenticates to Vault using a SPIFFE JWT-SVID — a short-lived (5 min), cryptographically-attested workload identity. No static Vault token, no AppRole secret stored on disk. SPIRE Agent attests the workload at startup via platform-level proofs (OS process, Kubernetes pod metadata, etc.) and issues SVIDs only to verified workloads.
The plaintext credential exists only: (1) in the SaaS API's TLS response at the adapter, and (2) in the Vault plugin's process memory immediately before returning to the consumer. It is never logged, never stored in KV, never written to disk.
Credential Rotation Flow
sequenceDiagram
participant App as Consumer App
participant Vault as HashiCorp Vault
participant Plugin as vault-rest-engine
participant Transit as Transit Engine
participant API as cred-rotation-api
participant Provider as SaaS Platform API
App->>Vault: vault read generic/creds/my-role (TLS)
Vault->>Plugin: dispatch to plugin process (gRPC + mTLS)
Plugin->>API: POST /v1/credentials/rotate (mTLS + client cert)
API->>Provider: rotate credential via platform API (TLS 1.3)
Provider-->>API: new_credential (plaintext, within TLS channel)
API->>Transit: POST /v1/transit/encrypt/cred-rotation-key
Note over API,Transit: Envelope encryption — unique DEK per call
Transit-->>API: vault:v1:AbC123... (ciphertext)
API-->>Plugin: { encrypted_value: "vault:v1:AbC123..." } (mTLS)
Note over API,Plugin: plaintext never leaves API unencrypted
Plugin->>Transit: POST /v1/transit/decrypt/cred-rotation-key
Transit-->>Plugin: plaintext credential (in plugin memory only)
Plugin-->>Vault: { secret_value, lease_id, ttl: 86400 }
Vault-->>App: secret (TLS)
POST /v1/credentials/revoke — the platform immediately invalidates the old credential. No long-lived credential window exists.Credential Rotation API Contract
{ provider, resource_id, lease_id, context? }{ provider, resource_id, lease_id, credential_id }Implementation Phases
Quick Start
Go 1.27+, Vault 1.18+ (brew install hashicorp/tap/vault), Docker (for SPIRE integration tests), and credentials for the target platform (admin API key or management token).
# Clone and configure git clone https://github.com/jralmaraz/vault-secrets-broker.git cd vault-secrets-broker cp .env.example .env # edit .env with your platform admin credentials # Run Phase 1 — starts Vault and configures all engines make setup # Load the Vault environment source .vault-env # Verify the platform admin credential is Transit-encrypted (not plaintext) vault kv get secret/cred-rotation-api/adapters/auth0 # admin_token_encrypted = vault:v1:... ← ciphertext ✓ # Verify Transit decrypt round-trip CIPHER=$(vault kv get -field=admin_token_encrypted \ secret/cred-rotation-api/adapters/auth0) vault write -field=plaintext transit/decrypt/cred-rotation-key \ ciphertext="$CIPHER" | base64 -d # → your original client_secret ✓
Supply Chain Security
go.sum. Tampered modules fail immediately.