Vault on Kubernetes, deeply

The agent injector, the Secrets Store CSI driver, and External Secrets.

Advanced40 min · lesson 10 of 15

Kubernetes is where most secrets actually get consumed, and it is also where the default story is weakest — a Kubernetes Secret is base64-encoded, readable by anyone with get on the namespace, and mounted into pods as files or env vars. Advanced practice replaces "a Secret sitting in etcd" with "a short-lived secret delivered to the pod at runtime from an external manager." There are three mature integration patterns, and choosing between them is a real design decision.

Pattern 1 — the Vault Agent Injector (sidecar)

The injector is a mutating webhook: annotate a pod and it injects a Vault Agent init/sidecar container that authenticates with the pod’s service account (Kubernetes auth, solving secret-zero), fetches secrets, and renders them to a shared in-memory volume as files. The app reads a file; it never talks to Vault directly and never holds a Vault token. The agent renews leases and re-renders on rotation, and templates let you shape the output into whatever config format the app expects.

pod annotations — agent injector
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "payments" # K8s-auth role
vault.hashicorp.com/agent-inject-secret-db: "database/creds/payments-ro"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/payments-ro" -}}
DB_USER={{ .Data.username }}
DB_PASS={{ .Data.password }}
{{- end -}}
# rendered to /vault/secrets/db (tmpfs); the app just reads the file.

Pattern 2 — the Secrets Store CSI driver

The CSI driver mounts external secrets as a volume through the standard container storage interface, with providers for Vault, AWS, GCP, and Azure. The pod sees files under a mount path; the secret is fetched at pod start and (optionally) rotated. Its distinguishing feature is that it can also sync into a native Kubernetes Secret when something genuinely needs one (an image pull secret, a controller that only reads Secrets) — a controlled bridge rather than the default sprawl. It is a good fit when you want a uniform mount mechanism across clouds.

Pattern 3 — External Secrets Operator (ESO)

ESO takes the opposite stance: it reconciles an ExternalSecret custom resource by pulling from the external manager and creating a normal Kubernetes Secret, refreshed on an interval. You keep working with plain Secrets (maximum compatibility), and the source of truth stays external — rotate in Vault or AWS and ESO refreshes the cluster copy. The tradeoff is that the materialized Secret does live in etcd between refreshes, so this is the most compatible but least "never touches etcd" of the three.

Three ways to get a secret into a pod
Agent Injector
sidecar renders files
tmpfs, no etcd copy
best leases/rotation
Vault-native
CSI driver
volume mount
multi-cloud providers
optional K8s Secret sync
controlled bridge
External Secrets
materializes a Secret
max compatibility
lives in etcd
refreshed on interval
Injector/CSI keep secrets out of etcd; ESO trades that for working with plain Secrets. Pick per workload.
Whatever you choose, fix etcd and RBAC underneath
None of these patterns helps if a Kubernetes Secret still lands in plaintext etcd and everyone has get on it. Enable encryption-at-rest for Secrets with a KMS provider (not the identity provider that does nothing), lock Secret RBAC to the workloads that need it, disable auto-mounted service-account tokens where unused, and remember that any secret rendered to a pod is readable by anyone with exec into that pod. The integration reduces exposure; it does not excuse a soft cluster.