Kubernetes Secrets solve the "how do I inject config into Pods" problem, but they don't solve the "where do secrets live and who manages their lifecycle" problem. In production, secrets live in dedicated secret management systems — Vault, AWS Secrets Manager, GCP Secret Manager — and are synced into Kubernetes. This lesson covers the ecosystem and patterns.

1. Why External Secrets Management?

K8s native Secrets have fundamental limitations for production:

ProblemWhy K8s Secrets Alone Fail
Storage in GitYou can't commit Secret YAML to Git (even base64 is readable)
RotationNo built-in automatic rotation — manual process
Audit trailWho changed a Secret? When? K8s audit logs are limited
Cross-environmentSame secret source for dev/staging/prod — K8s Secrets are per-cluster
Access controlRBAC is namespace-level — no per-secret access policies
Dynamic secretsNo concept of short-lived credentials (e.g., DB passwords that expire)

The Solution Landscape

External Secret Stores HashiCorp Vault AWS Secrets Manager GCP Secret Manager Azure Key Vault CyberArk, 1Password Source of truth for secrets Rotation, audit, access control Sync Layer External Secrets Operator Sealed Secrets Secrets Store CSI Driver SOPS + ArgoCD/Flux K8s Secrets Created/synced automatically Consumed by Pods normally (env vars / volume mounts)
The pattern is always the same: Secrets live in an external system (source of truth). A sync mechanism pulls them into K8s as native Secret objects. Pods consume them normally. The external system handles rotation, audit, and access control. K8s is just the delivery mechanism.

2. External Secrets Operator (ESO)

The most popular approach. ESO is a K8s operator that syncs secrets from external stores into native K8s Secrets. It uses Custom Resources to declare what to sync.

Architecture

# 1. SecretStore — HOW to connect to the external provider
# 2. ExternalSecret — WHAT to fetch and WHERE to put it
# 3. Result: a native K8s Secret (created/updated by ESO controller)

SecretStore (or ClusterSecretStore)

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: eso-service-account   # Uses IRSA (IAM Roles for SAs)

ExternalSecret

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 1h              # ← How often to sync from external store
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-creds                 # ← K8s Secret to create
    creationPolicy: Owner          # ESO owns (deletes when ExternalSecret deleted)
  data:
    - secretKey: username          # ← key in the K8s Secret
      remoteRef:
        key: prod/database         # ← path in AWS Secrets Manager
        property: username         # ← JSON field within the secret
    - secretKey: password
      remoteRef:
        key: prod/database
        property: password

What Happens

  1. ESO controller reads the ExternalSecret CR
  2. Connects to AWS Secrets Manager using the SecretStore credentials
  3. Fetches prod/database and extracts username and password
  4. Creates (or updates) K8s Secret db-creds in namespace production
  5. Every refreshInterval, re-syncs (catches rotations in the external store)
ESO makes external rotation transparent. When the secret is rotated in AWS Secrets Manager, ESO detects the change on its next sync cycle and updates the K8s Secret. If Pods mount it as a volume, they see the new value within ~60s of the sync. No manual intervention.

Supported Providers

ProviderService
AWSSecrets Manager, Parameter Store (SSM)
GCPSecret Manager
AzureKey Vault
HashiCorpVault (KV v1/v2, PKI, dynamic secrets)
OtherIBM Cloud, Oracle, Doppler, 1Password, CyberArk, Delinea
Use ClusterSecretStore (cluster-scoped) when the same external provider serves multiple namespaces. Use namespace-scoped SecretStore for multi-tenant clusters where teams bring their own credentials to different secret stores.

3. Alternative Approaches

Sealed Secrets (Bitnami)

Encrypt Secrets client-side so they can be stored in Git safely.

# Install kubeseal CLI + controller in cluster
# Encrypt a Secret:
kubectl create secret generic db-creds \
  --from-literal=password=s3cr3t \
  --dry-run=client -o yaml | kubeseal \
  --controller-name=sealed-secrets \
  --controller-namespace=kube-system \
  -o yaml > sealed-secret.yaml

# sealed-secret.yaml can be committed to Git safely
# The SealedSecret controller decrypts it inside the cluster
# and creates a regular K8s Secret
ProsCons
Secrets in Git (GitOps-native)No rotation support
Simple — no external system neededSealed to one cluster (can't reuse across clusters)
Encryption with cluster's public keyIf cluster key is lost, all secrets unrecoverable

Secrets Store CSI Driver

Mounts secrets from external stores directly into Pods as volumes — bypasses K8s Secrets entirely.

# SecretProviderClass defines what to fetch:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-db-creds
spec:
  provider: vault
  parameters:
    vaultAddress: "https://vault.company.com"
    roleName: "web-app"
    objects: |
      - objectName: "db-password"
        secretPath: "secret/data/db"
        secretKey: "password"

# Pod mounts it:
spec:
  volumes:
    - name: secrets
      csi:
        driver: secrets-store.csi.k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: vault-db-creds
  containers:
    - volumeMounts:
        - name: secrets
          mountPath: /mnt/secrets
          readOnly: true
ProsCons
Secrets never stored in etcd at allMore complex setup (CSI driver + provider)
Direct integration with Vault, AWS, GCP, AzureSecrets only available as files (or synced to K8s Secret optionally)
Automatic rotation via providerPod must tolerate file changes during runtime

SOPS + GitOps (Flux/ArgoCD)

Encrypt secret values in YAML files using SOPS (Mozilla). GitOps controller decrypts on apply.

# Encrypt specific fields in a Secret YAML:
sops --encrypt --in-place --encrypted-regex '^(data|stringData)$' secret.yaml

# Flux or ArgoCD configured with decryption provider
# On apply: decrypts in memory → creates K8s Secret
# Secret YAML committed to Git (encrypted values only)

HashiCorp Vault — Direct Integration

Vault offers the Vault Agent Injector: a mutating webhook that injects a Vault sidecar into Pods. The sidecar fetches secrets from Vault and writes them to a shared volume.

# Annotations trigger the injector:
metadata:
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "web-app"
    vault.hashicorp.com/agent-inject-secret-db: "secret/data/db"
    vault.hashicorp.com/agent-inject-template-db: |
      {{- with secret "secret/data/db" -}}
      export DB_PASS={{ .Data.data.password }}
      {{- end -}}
Vault Agent Injector is powerful but adds sidecar overhead to every Pod. For simpler setups, ESO + Vault provider gives you the same secret access without sidecars. Use the Agent Injector when you need Vault's dynamic secrets (short-lived DB credentials, PKI certificates) that refresh during Pod lifetime.

4. Choosing the Right Approach

ApproachSource of TruthRotationGitOpsComplexityBest For
ESOExternal store✅ Auto-sync✅ (CRs in Git)MediumMost production setups
Sealed SecretsGit (encrypted)❌ Manual✅ NativeLowSmall teams, no external store
CSI DriverExternal store✅ Provider-driven✅ (CRs in Git)HighZero-etcd-storage requirement
SOPS + GitOpsGit (encrypted)❌ Manual✅ NativeLow-MedGitOps-first teams
Vault AgentVault✅ Dynamic⚠️ AnnotationsHighDynamic secrets, PKI, advanced Vault features

Decision Flowchart

Do you have an external secret store (Vault/AWS/GCP/Azure)?
├─ YES → Do you need secrets that never touch etcd?
│         ├─ YES → Secrets Store CSI Driver
│         └─ NO  → External Secrets Operator (simplest, most flexible)
└─ NO  → Do you use GitOps?
          ├─ YES → Sealed Secrets or SOPS
          └─ NO  → Native K8s Secrets + encryption at rest

5. Production Patterns

Secret Rotation Flow (ESO)

  1. Secret rotated in AWS Secrets Manager (manual or Lambda-based auto-rotation)
  2. ESO syncs new value on next refreshInterval
  3. K8s Secret updated
  4. If volume-mounted: kubelet syncs file in ~60s → app reads new value
  5. If env var: kubectl rollout restart needed (or config hash annotation pattern)

Multi-Cluster Secret Sync

# Same ExternalSecret in all clusters pointing to same AWS path:
# dev cluster:  remoteRef.key = dev/database
# prod cluster: remoteRef.key = prod/database
# Single source of truth per environment
# Rotation in AWS propagates to all clusters automatically
The golden rule: Never store the plaintext secret in Git. Store only a reference (ExternalSecret CR pointing to a path in the external store) or an encrypted form (SealedSecret, SOPS). The actual secret value exists only in the external store and in the K8s Secret at runtime.

Summary

ConceptKey Point
External storeSource of truth for secrets — handles rotation, audit, access control
ESOOperator that syncs external secrets → K8s Secrets via CRDs
SecretStoreConnection config to the external provider
ExternalSecretDeclares what to fetch, where to put it, how often to sync
refreshIntervalHow often ESO re-syncs (catches rotations)
Sealed SecretsEncrypt secrets for Git — no external store needed
CSI DriverMount secrets directly from external store — bypasses etcd
Vault AgentSidecar injects dynamic secrets — short-lived credentials

📝 Quiz: External Secrets Management

Q1: A database password is rotated in AWS Secrets Manager. ESO has refreshInterval: 1h. When does the K8s Secret update?

Within 1 hour (the next sync cycle). ESO polls the external store at the refresh interval. It detects the changed value and updates the K8s Secret. For faster propagation, reduce refreshInterval (e.g., 5m), but this increases API calls to the external store.

Q2: What's the main advantage of Secrets Store CSI Driver over ESO?

Secrets never touch etcd. The CSI driver mounts secrets directly from the external store into the Pod's volume at runtime. There's no intermediate K8s Secret object stored in the cluster. This eliminates the etcd attack surface entirely.

Q3: You're using Sealed Secrets. You need to rotate a database password. What's the process?

Manual process: (1) Generate new password. (2) Create a new SealedSecret YAML with kubeseal. (3) Commit to Git. (4) GitOps controller applies it, creating an updated K8s Secret. (5) Restart Pods if using env vars. Sealed Secrets have no automatic rotation — this is their main limitation.

Q4: What's the difference between SecretStore and ClusterSecretStore in ESO?

SecretStore is namespaced — only ExternalSecrets in the same namespace can reference it. ClusterSecretStore is cluster-scoped — any ExternalSecret in any namespace can reference it. Use ClusterSecretStore for a shared provider; use SecretStore for multi-tenant isolation (each team has their own store credentials).

Q5: When would you choose Vault Agent Injector over ESO with a Vault provider?

When you need dynamic secrets — short-lived credentials generated on-demand (e.g., a database username/password that's valid for 1 hour, or a PKI certificate). The Vault Agent sidecar can renew leases during Pod lifetime. ESO fetches static (or relatively static) KV secrets. For simple KV secrets, ESO is simpler.

Q6: Your team uses GitOps (ArgoCD). What can you safely commit to Git?

Safe to commit: ExternalSecret CRs (contain only a reference/path, not the actual secret), SealedSecret YAMLs (encrypted), SOPS-encrypted files. Never commit: Plain K8s Secret YAML (even base64 — it's trivially decoded), encryption keys, tokens, or any cleartext credentials.