Architecture Decision Record

ADR 003: Vault + External Secrets Operator as the Only Secrets Path

One source of truth for every secret, synced into Kubernetes at runtime — chosen after living with the alternative.

Status: Accepted  ·  Date: Aug 2025  ·  ← All ADRs


Context

Early on, secrets lived wherever was convenient: manually created Kubernetes Secrets, ArgoCD’s credential store, values files. Retrofitting out of that state was painful enough to earn its own entry in Lessons Learned.

I’ll be straight about the process: I didn’t run a bake-off. I use Vault at work, wanted to understand it more deeply, and knew it would do the job — so I went with it. That’s a legitimate way to pick a tool, and pretending otherwise would be dishonest. For the record, the alternatives I’d have been choosing against:

Vault won on merits I already trusted rather than on a comparison I ran — and the deep-end learning was part of the point.

Decision

Vault is the single source of truth. One KV v2 mount holds everything. A single ClusterSecretStore connects External Secrets Operator to Vault using Kubernetes auth — ESO’s service account authenticates with its projected token, so there is no static Vault credential anywhere in the cluster. Roughly 47 ExternalSecret resources across ~25 namespaces sync on a 1-hour refresh.

Access paths are per-consumer: humans authenticate to Vault via OIDC through Authentik (ADR 004); Terraform reads secrets at plan time (using ephemeral values so they never persist in state where the provider allows it); one legacy AppRole exists for a single edge service.

Reasoning

Tradeoffs

Outcome

Every secret a workload consumes arrives through the same pipeline: Vault → ESO → Kubernetes Secret, declared in Git as an ExternalSecret that contains no secret material. Rotations have been exercised for real — including a full cluster CA rotation, which is exactly the drill that proves the recovery paths work.