Two Paths to Credential Rotation
Vault's plugin SDK supports multiple approaches to extending its secrets engine surface. For platforms without a native engine, we built and compared two architectures — not to declare a winner, but to make the trade-offs visible so teams can choose deliberately.
flowchart TB
app["Consumer App"]
subgraph vault["HashiCorp Vault"]
engine_a0["vault-auth0-engine\n(Auth0 plugin)"]
engine_sq["vault-sq-engine\n(SonarQube plugin)"]
engine_dd["vault-dd-engine\n(Datadog plugin)"]
transit["Transit Engine"]
end
auth0["Auth0 Mgmt API"]
sonar["SonarQube Web API"]
datadog["Datadog API"]
app -- "TLS" --> engine_a0
app -- "TLS" --> engine_sq
app -- "TLS" --> engine_dd
engine_a0 -- "TLS 1.3" --> auth0
engine_sq -- "TLS 1.3" --> sonar
engine_dd -- "TLS 1.3" --> datadog
engine_a0 & engine_sq & engine_dd -- "Transit\nencrypt/decrypt" --> transit
flowchart TB
app["Consumer App"]
subgraph vault["HashiCorp Vault"]
engine["vault-rest-engine\n(one plugin, all platforms)"]
transit["Transit Engine"]
pki["PKI Engine\nInternal CA"]
end
subgraph broker["cred-rotation-api · mTLS"]
a0["Auth0\nAdapter"]
sq["SonarQube\nAdapter"]
dd["Datadog\nAdapter"]
end
auth0["Auth0 Mgmt API"]
sonar["SonarQube Web API"]
datadog["Datadog API"]
app -- "TLS" --> engine
engine -- "mTLS\nclient cert" --> broker
a0 -- "TLS 1.3" --> auth0
sq -- "TLS 1.3" --> sonar
dd -- "TLS 1.3" --> datadog
engine & broker -- "Transit\nencrypt/decrypt" --> transit
pki -- "issues cert\nfor plugin" --> engine
Request Path — Step by Step
What You Actually Write
To add a new platform (SonarQube in this example), here is what changes under each approach:
// Must write an entire new Vault plugin package sonarqubeplugin // ─ New plugin binary to register with Vault func Factory(ctx context.Context, conf *backend.BackendConfig) (logical.Backend, error) { b := &backend{} b.Backend = &framework.Backend{ Paths: b.paths(), BackendType: logical.TypeLogical, } return b, b.Setup(ctx, conf) } // ─ Implement config, roles, creds paths func (b *backend) paths() []*framework.Path { return []*framework.Path{ b.pathConfig(), b.pathRoles(), b.pathCredsRead(), // rotation logic here } } // ─ Write rotation logic (SonarQube-specific) // ─ Write revocation logic // ─ Register SHA256-pinned binary with Vault // ─ Write tests, handle Vault SDK lifecycle
// Only write a new adapter — plugin unchanged package sonarqube // ─ Implement the Adapter interface (3 methods) type Adapter struct { baseURL string adminToken string httpClient *http.Client } func (a *Adapter) Rotate( ctx context.Context, req adapter.RotateRequest, ) (adapter.Result, error) { // call SonarQube API, return encrypted result } func (a *Adapter) Revoke( ctx context.Context, req adapter.RevokeRequest, ) error { /* revoke token */ } func (a *Adapter) Status( ctx context.Context, credentialID string, ) (adapter.CredentialStatus, error) { /* check */ } func (a *Adapter) Name() string { return "sonarqube" } // ─ Register in main.go: reg.Register(sonarqube.New(cfg)) // ─ No Vault plugin changes. No binary re-registration.
Trade-off Matrix
| Dimension | A — Native Plugin | B — Generic REST Engine |
|---|---|---|
| Hop count (read path) | 4 hops — faster, fewer network roundtrips | 6 hops — 2 extra (plugin → API → plugin) |
| Adding a new platform | New Vault plugin binary — write, test, register SHA256 pin, restart Vault (or hot-reload) | New adapter (~200 lines) — no plugin changes, no Vault restart, no binary re-registration |
| Vault plugin count | 1 plugin per platform — grows with the number of providers | 1 plugin for all platforms — single binary to maintain and register |
| Vault plugin upgrade cycle | Restart or hot-reload Vault per plugin for every update | Restart only cred-rotation-api — Vault itself untouched for adapter changes |
| Plaintext exposure surface | Plaintext in plugin memory — standard Vault pattern, well-understood | Plaintext in API memory only — plugin only ever sees vault:v1: ciphertext; stronger isolation |
| mTLS between components | No extra mTLS layer — fewer TLS connections | mTLS between plugin and API — extra TLS handshake; provides mutual authentication |
| Independent scaling | Plugin runs inside Vault process — cannot scale independently | cred-rotation-api scales independently — Kubernetes HPA, separate resource limits |
| Independent deployment | Plugin tied to Vault lifecycle — deploy Vault to update a provider | Deploy adapter changes separately — zero downtime for Vault consumers |
| Language / framework freedom | Must use Vault Plugin SDK (Go) — all platforms share the same tech stack | cred-rotation-api language-agnostic — adapters could move to other languages behind the interface |
| Operational complexity | Simpler overall — one fewer service to run, monitor, and secure | One extra service — cred-rotation-api needs its own TLS certs, health checks, observability |
| Auth isolation (SPIFFE) | Plugin inherits Vault's identity — harder to attest separately | API gets its own SPIFFE SVID — workload identity is independently attested by SPIRE |
| Best fit | 1–3 platforms, stable provider set, performance-critical paths | 4+ platforms, frequent provider additions, distributed teams, regulated environments requiring isolation |
Security Difference: Plaintext Isolation
The native plugin receives the plaintext credential from the SaaS API response and holds it in the Vault plugin process memory. This is the standard Vault dynamic secrets pattern — Vault core manages the credential lifecycle. Security perimeter: Vault process.
The plaintext credential never reaches the Vault plugin. The rotation API encrypts it with Transit immediately after the SaaS provider responds, and only the vault:v1: ciphertext travels over mTLS back to the plugin. Even a compromised mTLS key yields only ciphertext, not the credential. Security perimeter: cred-rotation-api process only.
Both approaches use Transit envelope encryption (AES-256-GCM96) and TLS 1.3 on all outbound connections. The difference is where the plaintext window exists: in the Vault plugin process (Approach A) or exclusively in the rotation API process (Approach B). In regulated environments where the Vault process boundary is the audit boundary, Approach B's extra isolation layer has real compliance value.