Part 2 - Kubernetes Vault Integration - ESO

Introduction Link to heading

We now want to migrate from CSI Driver + Vault + syncSecret

→ to External Secrets Operator (ESO) + Reloader, while maintaining:

  • Vault as the source of secrets
  • Kubernetes Secrets for env: injection
  • Auto pod restart when secrets change

Since we are using CSI with Vault, the following is already configured:

Requirement : Description Vault server is reachable from K8s : we already use vaultAddress: http://<Vault-VM-IP>:8200 in CSI Vault uses Kubernetes auth : SecretProviderClass uses roleName: sockshop-app Kubernetes ServiceAccount exists : You’re using default (in CSI) and referencing it in ESO Vault has a policy to access secrets : CSI already accesses secret/data/sockshop/database Kubernetes has network access to Vault : Confirmed from CSI’s successful mounts

We don’t need any new Vault-side config.


✅ Required Components Link to heading

  1. Vault configuration
  2. ESO CRDs: SecretStore, ExternalSecret
  3. Kubernetes Secret consumption in the Deployment
  4. Reloader annotations to auto-restart pods on secret change

----------External Secrets Operator (ESO) + Reloader----------

            ┌──────────────────────────────────┐
            │   Kubernetes Deployment Starts   │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │   ExternalSecret CR is created   │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │ ESO Controller watches CRs       │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │ Auth to Vault via SA or AppRole  │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │ Fetch secret from Vault path     │
            │ (e.g., kv-v2/secret/my-app)      │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │  Create or update K8s Secret     │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │ Pod mounts secret as env/volume  │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │  Reloader watches secret changes │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │   On change, restarts the Pod    │
            └────────────┬─────────────────────┘
            ┌──────────────────────────────────┐
            │      App uses updated secret     │
            └────────────┬─────────────────────┘
                     ┌────────┐
                     │  Done  │
                     └────────┘

🧱 Step-by-Step: Migrate Setup Link to heading


🔧 1. Install External Secrets Operator (ESO) Link to heading

If not already installed:

helm repo add external-secrets https://charts.external-secrets.io
helm upgrade --install external-secrets external-secrets/external-secrets \
--namespace external-secrets --create-namespace

🔐 2. Create a SecretStore for Vault Link to heading

This defines how ESO authenticates to Vault.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: default
spec:
  provider:
    vault:
      server: "http://<Vault-VM-IP>:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "sockshop-app"
          serviceAccountRef:
            name: default

📦 3. Create the ExternalSecret Resource Link to heading

This will sync Vault secrets into a Kubernetes Secret:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: sockshop-db-secrets
  namespace: default
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: sockshop-db-secrets         # Name of K8s Secret created
    creationPolicy: Owner
  data:
    - secretKey: DB_USERNAME
      remoteRef:
        key: secret/data/sockshop/database
        property: username
    - secretKey: DB_PASSWORD
      remoteRef:
        key: secret/data/sockshop/database
        property: password

🚀 4. Create the Deployment YAML Link to heading

Replace the CSI volume + mount setup with ESO-based secret usage:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: your-sockshop-service
  annotations:
    reloader.stakater.com/auto: "true"   # 👈 Triggers restart on secret change
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sockshop-nginx
  template:
    metadata:
      labels:
        app: sockshop-nginx
    spec:
      serviceAccountName: default
      containers:
        - name: your-app-container
          image: nginx:latest
          env:
            - name: DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: sockshop-db-secrets
                  key: DB_USERNAME
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: sockshop-db-secrets
                  key: DB_PASSWORD

📝 we can also annotate the deployment with:

annotations:
  reloader.stakater.com/secret: "sockshop-db-secrets"

If we want to be explicit.


🔁 5. Install Reloader (if not already) Link to heading

helm repo add stakater https://stakater.github.io/stakater-charts
helm upgrade --install reloader stakater/reloader \
  --namespace kube-system --create-namespace

🧪 Optional: Clean Up CSI Link to heading

  • Remove the SecretProviderClass
  • Remove volumeMounts and CSI volumes in pod spec
  • Optionally delete the synced Secrets CSI generated

Commands to execute for testing

# View secret data (base64 encoded)
kubectl get secret sockshop-db-secrets -n default -o yaml

# Decode specific secret values
kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_USERNAME}' | base64 -d
echo ""  # Add newline
kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
echo ""  # Add newline

# # Or decode all at once
# kubectl get secret sockshop-db-secrets -n default -o json | jq -r '.data | to_entries[] | "\(.key): \(.value | @base64d)"'

Screenshot-1

ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_USERNAME}' | base64 -d
sockuser-1⏎

ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
sockpassword123-2⏎

Screenshot-2

ubuntu@k3s1 ~>
#### After Change
ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
sockpassword123-4⏎
ubuntu@k3s1 ~>
ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
sockpassword123-4⏎
ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
sockpassword123-4⏎

Screenshot-3

ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
sockpassword123-5⏎
ubuntu@k3s1 ~>

Screenshot-4

ubuntu@k3s1 ~ [124]> kubectl exec -it testdeployment-dbcd7c7c9-z9mwt -- env | grep -i db_
DB_USERNAME=sockuser-1
DB_PASSWORD=sockpassword123-5
STAKATER_SOCKSHOP_DB_SECRETS_SECRET=6ef6e5f1728ba68db52780773680d60df96ded2b
ubuntu@k3s1 ~>

# After updating the password in vault, notice how the pod name has changed. This is because reloader has \
# restarted the pod after the secret change. I only noticed this because when I was testing the env by \
# exec'ing into the pod, it threw an error due to incorrent pod name.
ubuntu@k3s1 ~> kubectl get po
NAME                                     READY   STATUS    RESTARTS   AGE
testdeployment-664c7cdb4c-vrg6q          1/1     Running   0          52s
your-sockshop-service-5bc8fc6c6c-7fw9z   1/1     Running   0          89m
ubuntu@k3s1 ~>
ubuntu@k3s1 ~ [1|1]> kubectl exec -it testdeployment-664c7cdb4c-vrg6q -- env | grep -i db_password
DB_PASSWORD=newtestpassword-0
ubuntu@k3s1 ~> kubectl get secret sockshop-db-secrets -n default -o jsonpath='{.data.DB_PASSWORD}' | base64 -d
newtestpassword-0⏎

✅ Final Result Link to heading

  • ✅ Vault remains the source of truth
  • ✅ ESO syncs secrets to Kubernetes Secret sockshop-db-secrets
  • ✅ Deployment consumes that Secret via env:
  • ✅ Reloader watches the Secret and auto-restarts pod on change