HashiCorp Vault — External Secrets

2 minute read

The External Secrets Operator (ESO) syncs secrets from Vault into native Kubernetes Secret objects — ideal for applications that expect to read from K8s Secrets.


How It Works

1. ExternalSecret CR defines what to sync from Vault
2. ESO controller reads secrets from Vault
3. ESO creates/updates Kubernetes Secret objects
4. Pods use K8s Secrets normally (env vars, volumes)
5. ESO periodically refreshes (configurable interval)

Install External Secrets Operator

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets external-secrets/external-secrets \
    --namespace external-secrets \
    --create-namespace

Configure Vault Connection

SecretStore (namespace-scoped)

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: default
spec:
  provider:
    vault:
      server: "http://vault.vault.svc.cluster.local:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "external-secrets"
          serviceAccountRef:
            name: "external-secrets-sa"

ClusterSecretStore (cluster-wide)

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-cluster
spec:
  provider:
    vault:
      server: "http://vault.vault.svc.cluster.local:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "external-secrets"
          serviceAccountRef:
            name: "external-secrets-sa"
            namespace: "external-secrets"

Sync Secrets

Single Secret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-db-secret
  namespace: default
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: myapp-db-secret          # K8s Secret name
    creationPolicy: Owner          # ESO owns the Secret
  data:
    - secretKey: username          # Key in K8s Secret
      remoteRef:
        key: myapp/db              # Vault path (under secret/)
        property: username         # Key in Vault secret
    - secretKey: password
      remoteRef:
        key: myapp/db
        property: password

All Keys from a Secret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-config
spec:
  refreshInterval: 30m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: myapp-config
  dataFrom:
    - extract:
        key: myapp/config          # Sync ALL keys from this Vault path

Use the Synced Secret

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          envFrom:
            - secretRef:
                name: myapp-config    # All keys as env vars
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: myapp-db-secret
                  key: password

Vault Setup for ESO

# Create policy for ESO
vault policy write external-secrets - <<EOF
path "secret/data/*" {
  capabilities = ["read"]
}
path "secret/metadata/*" {
  capabilities = ["read", "list"]
}
EOF

# Create Kubernetes auth role
vault write auth/kubernetes/role/external-secrets \
    bound_service_account_names=external-secrets-sa \
    bound_service_account_namespaces=external-secrets \
    policies=external-secrets \
    ttl=1h
# Service account for ESO
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-secrets-sa
  namespace: external-secrets

ESO vs Agent Injector vs CSI

Feature ESO Agent Injector CSI Provider
Creates K8s Secrets ✅ Yes ❌ No ✅ Optional
Sidecar needed ❌ No ✅ Yes ❌ No
Auto-refresh ✅ Configurable interval ✅ Continuous ❌ On pod restart
Templates ❌ Limited ✅ Full Go template ❌ No
Dynamic secrets ❌ Not ideal ✅ Yes (with renewal) ❌ Limited
Best for Static secrets, env vars Dynamic secrets, files Volume-mounted secrets

Quick Reference

Resource Purpose
SecretStore Namespace-scoped Vault connection
ClusterSecretStore Cluster-wide Vault connection
ExternalSecret Defines what to sync
refreshInterval How often to re-sync

Next: Practice Exercises