How to Use Reloader with Vault CSI Driver (K8s Secret) Pattern#
This guide explains how to set up HashiCorp Vault with the Secrets Store CSI Driver using Kubernetes authentication, combined with Stakater Reloader for automatic pod restarts when secrets change. This pattern uses secretObjects to sync CSI-mounted secrets into a Kubernetes Secret, which Reloader then watches.
See also: CSI Driver File-Based Pattern - an alternative that skips the K8s Secret entirely and uses Reloader's CSI integration to watch
SecretProviderClassPodStatusdirectly.
Overview#
┌─────────────────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Application Pod │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ App Container │ │ │
│ │ │ │ │ │
│ │ │ - Reads secrets from env vars (K8s Secret) │ │ │
│ │ │ - Optionally reads secrets from mounted files │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │
│ │ │ CSI Volume Mount (/mnt/secrets) │ │ │
│ │ │ - username │ │ │
│ │ │ - password │ │ │
│ │ └─────────────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────┼─────────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Vault CSI │ │ K8s Secret │ │ Stakater │ │
│ │ Provider │───►│ (app-secrets) │───►│ Reloader │ │
│ │ │ │ │ │ │ │
│ │ - Authenticates │ │ annotation: │ │ Watches secret │ │
│ │ via K8s auth │ │ reloader.../ │ │ changes and │ │
│ │ - Fetches from │ │ match: "true" │ │ restarts pods │ │
│ │ Vault KV v2 │ │ │ │ │ │
│ │ - Syncs via │ │ (secretObjects) │ │ │ │
│ │ secretObjects │ │ │ │ │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Vault │ │
│ │ Server │ │
│ │ │ │
│ │ KV v2 engine │ │
│ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Prerequisites#
Complete the common setup steps from the Overview:
- Vault installed, initialized, and unsealed
- Vault CLI configured
- KV v2 secrets engine enabled
- Test secrets written to
secret/myapp - Read policy (
myapp-read) created - Stakater Reloader installed
Additional requirements:
- Secrets Store CSI Driver installed with
syncSecret.enabled=true - Vault CSI Provider installed
Install Secrets Store CSI Driver#
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set syncSecret.enabled=true \
--set enableSecretRotation=true \
--set rotationPollInterval=30s
Important: syncSecret.enabled=true is required for the CSI driver to create Kubernetes Secrets from mounted volumes. Without this, the secretObjects field will not work.
Install Vault CSI Provider#
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault -n vault \
--set "server.enabled=false" \
--set "injector.enabled=false" \
--set "csi.enabled=true"
Note: If Vault is already installed in the cluster, install only the CSI provider by setting
server.enabled=falseandinjector.enabled=false.
Step 1: Create Vault Policy#
vault policy write myapp-read - <<EOF
path "secret/data/myapp" {
capabilities = ["read"]
}
path "secret/metadata/myapp" {
capabilities = ["read"]
}
EOF
Step 2: Enable and Configure Kubernetes Auth#
# Enable Kubernetes auth method (skip if already enabled)
vault auth enable kubernetes
# Configure Kubernetes auth
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc.cluster.local"
Step 3: Create Vault Role#
vault write auth/kubernetes/role/csi-role \
bound_service_account_names=vault-csi-sa \
bound_service_account_namespaces=vault-csi-test \
policies=myapp-read \
ttl=1h
Step 4: Write Secrets#
vault kv put secret/myapp \
username="admin-user" \
password="super-secret-password"
Step 5: Create Application Namespace and ServiceAccount#
kubectl create namespace vault-csi-test
kubectl create serviceaccount vault-csi-sa -n vault-csi-test
Step 6: Create SecretProviderClass#
Create secret-provider-class.yaml:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-secrets
namespace: vault-csi-test
spec:
provider: vault
# Sync to Kubernetes Secret - Required for Reloader integration
secretObjects:
- secretName: app-secrets
type: Opaque
annotations:
reloader.stakater.com/match: "true"
data:
- key: username
objectName: username
- key: password
objectName: password
parameters:
vaultAddress: "http://vault.vault.svc.cluster.local:8200"
roleName: "csi-role"
objects: |
- objectName: "username"
secretPath: "secret/data/myapp"
secretKey: "username"
- objectName: "password"
secretPath: "secret/data/myapp"
secretKey: "password"
Apply:
kubectl apply -f secret-provider-class.yaml
Step 7: Deploy Application#
Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-csi-test-app
namespace: vault-csi-test
annotations:
reloader.stakater.com/search: "true"
spec:
replicas: 1
selector:
matchLabels:
app: vault-csi-test-app
template:
metadata:
labels:
app: vault-csi-test-app
spec:
serviceAccountName: vault-csi-sa
containers:
- name: app
image: busybox:latest
command:
- "sh"
- "-c"
- |
while true; do
echo "Username: $APP_USERNAME"
echo "Password: $APP_PASSWORD"
sleep 30
done
# Reference secrets from K8s Secret (required for Reloader)
env:
- name: APP_USERNAME
valueFrom:
secretKeyRef:
name: app-secrets
key: username
- name: APP_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: password
# CSI volume mount required to trigger secret sync
volumeMounts:
- name: vault-secrets
mountPath: /mnt/secrets
readOnly: true
volumes:
- name: vault-secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-secrets
Apply:
kubectl apply -f deployment.yaml
Step 8: Verify the Setup#
Check Pod Status#
kubectl get pods -n vault-csi-test
Pod should be Running.
Check SecretProviderClassPodStatus#
kubectl get secretproviderclasspodstatuses -n vault-csi-test
Verify Secrets are Populated#
# Check K8s Secret was created
kubectl get secret app-secrets -n vault-csi-test -o jsonpath='{.data.password}' | base64 -d
# Check app logs
kubectl logs -n vault-csi-test -l app=vault-csi-test-app
Step 9: Test Secret Rotation#
Update Secret in Vault#
vault kv put secret/myapp \
username="admin-user" \
password="new-rotated-password"
Wait and Verify#
Wait 30–60 seconds for CSI rotation and Reloader restart:
# Check secret was updated
kubectl get secret app-secrets -n vault-csi-test -o jsonpath='{.data.password}' | base64 -d
# Check pod was restarted (new pod name)
kubectl get pods -n vault-csi-test -l app=vault-csi-test-app
Key Configuration Points#
SecretProviderClass Configuration#
| Field | Description |
|---|---|
provider |
Must be vault |
parameters.vaultAddress |
Vault server URL |
parameters.roleName |
Vault Kubernetes auth role name |
parameters.objects |
YAML string defining which secrets to mount |
Objects Configuration#
Each object in the parameters.objects field maps a Vault secret field to a file:
| Field | Description |
|---|---|
objectName |
Name of the mounted file and key in secretObjects |
secretPath |
Full Vault path (KV v2: secret/data/<path>) |
secretKey |
Specific field within the Vault secret |
secretObjects Configuration#
The secretObjects field syncs mounted secrets to a Kubernetes Secret:
secretObjects:
- secretName: app-secrets
type: Opaque
annotations:
reloader.stakater.com/match: "true"
data:
- key: username
objectName: username # Must match objectName in parameters.objects
- key: password
objectName: password
Reloader Annotations#
| Resource | Annotation |
|---|---|
| Deployment | reloader.stakater.com/search: "true" |
| Secret | reloader.stakater.com/match: "true" (via secretObjects) |
Why CSI Volume Mount is Required#
Even if your app only uses environment variables from the K8s Secret, the CSI volume must be mounted because:
- The CSI driver only syncs secrets when a pod mounts the volume
- Without the mount, the K8s Secret (
secretObjects) won't be created