OpenBao CSI Driver (File-Based) + Stakater Reloader Integration#
This guide demonstrates integrating OpenBao with Stakater Reloader using the Secrets Store CSI Driver in file-based mode, where secrets are delivered directly as mounted files without syncing to Kubernetes Secrets.
Overview#
In the file-based CSI pattern, secrets from OpenBao are mounted directly into pods as files. Reloader monitors the SecretProviderClassPodStatus resource for version changes and triggers pod restarts when secrets are rotated.
┌─────────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Application Namespace │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌──────────────────────────────────┐ │ │
│ │ │ SecretProvider │ │ Application Pod │ │ │
│ │ │ Class │ │ ┌────────────────────────────┐ │ │ │
│ │ │ │ │ │ CSI Volume Mount │ │ │ │
│ │ │ - provider: │ │ │ /mnt/secrets/ │ │ │ │
│ │ │ openbao │ │ │ ├── username │ │ │ │
│ │ │ - parameters │ │ │ └── password │ │ │ │
│ │ │ (no secret- │ │ └────────────────────────────┘ │ │ │
│ │ │ Objects) │ └──────────────────────────────────┘ │ │
│ │ └────────┬────────┘ ▲ │ │
│ │ │ │ │ │
│ │ │ ┌───────────────┴───────────────┐ │ │
│ │ │ │ Secrets Store CSI │ │ │
│ │ │ │ Driver │ │ │
│ │ │ │ │ │ │
│ │ │ │ - Mounts secrets as files │ │ │
│ │ │ │ - Updates version in status │ │ │
│ │ │ └───────────────┬───────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ SecretProviderClassPodStatus │ │ │
│ │ │ │ │ │
│ │ │ status: │ │ │
│ │ │ objects: │ │ │
│ │ │ - id: username │ │ │
│ │ │ version: "abc123..." ◄── Version hash changes │ │ │
│ │ │ - id: password when secret rotates │ │ │
│ │ │ version: "def456..." │ │ │
│ │ └──────────────────────┬──────────────────────────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────┼─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Stakater Reloader │ │
│ │ │ │
│ │ Watches SPCPS for │ │
│ │ version changes │ │
│ │ │ │
│ │ Triggers pod restart │ │
│ │ on version change │ │
│ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ │
│ │ OpenBao CSI │ │
│ │ Provider │ │
│ └───────────┬─────────────┘ │
│ │ │
└─────────────────────────┼───────────────────────────────────────────┘
│
▼
┌─────────────────────┐
│ OpenBao │
│ │
│ KV v2 Secrets │
│ secret/myapp │
│ │
│ Kubernetes Auth │
└─────────────────────┘
When to Use This Pattern#
Use the file-based CSI pattern when:
- Applications read secrets directly from files
- You want to avoid creating Kubernetes Secret objects
- Security policies require secrets to exist only in memory/tmpfs
- You need the simplest possible secret delivery mechanism
Prerequisites#
Complete the common setup steps from the Overview:
- OpenBao installed, initialized, and unsealed
- OpenBao CLI configured (
BAO_ADDRandBAO_TOKENexported) - KV v2 secrets engine enabled
- Test secrets written to
secret/myapp - Stakater Reloader installed
Additional requirements:
- Secrets Store CSI Driver installed with rotation enabled
- OpenBao CSI Provider installed
Note: All
baocommands below assumeBAO_ADDRandBAO_TOKENare set from Overview Step 3.
Step 1: Install Secrets Store CSI Driver#
Install with rotation enabled:
helm repo add secrets-store-csi-driver \
https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm repo update
helm install csi-secrets-store \
secrets-store-csi-driver/secrets-store-csi-driver \
-n kube-system \
--set enableSecretRotation=true \
--set rotationPollInterval=30s
Note: syncSecret.enabled is not required for file-based delivery.
Step 2: Enable OpenBao CSI Provider#
helm upgrade openbao openbao/openbao \
-n openbao \
--set csi.enabled=true \
--reuse-values
Step 3: Configure OpenBao#
Create Read Policy#
bao policy write myapp-read - <<EOF
path "secret/data/myapp" {
capabilities = ["read"]
}
EOF
Enable Kubernetes Auth#
bao auth enable kubernetes
bao write auth/kubernetes/config \
kubernetes_host='https://kubernetes.default.svc.cluster.local'
Create Auth Role#
bao write auth/kubernetes/role/openbao-csi-role \
bound_service_account_names=openbao-csi-sa \
bound_service_account_namespaces=openbao-csi-test \
policies=myapp-read \
ttl=1h
Write Test Secret#
bao kv put secret/myapp username=myuser password=mypassword
Step 4: Create Application Namespace and ServiceAccount#
kubectl create namespace openbao-csi-test
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: openbao-csi-sa
namespace: openbao-csi-test
Step 5: Create SecretProviderClass#
File-based configuration without secretObjects:
# secret-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: openbao-secrets-file
namespace: openbao-csi-test
spec:
provider: openbao
parameters:
vaultAddress: "http://openbao.openbao.svc.cluster.local:8200"
roleName: openbao-csi-role
objects: |
- objectName: "username"
secretPath: "secret/data/myapp"
secretKey: "username"
- objectName: "password"
secretPath: "secret/data/myapp"
secretKey: "password"
Key Differences from K8s Secret Sync:
- No
secretObjectssection - Secrets only exist as mounted files
- No Kubernetes Secret is created
Step 6: Deploy Application#
The deployment uses the CSI-specific Reloader annotation:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: openbao-csi-file-app
namespace: openbao-csi-test
annotations:
# Reloader annotation for CSI-mounted secrets (file-based)
secretproviderclass.reloader.stakater.com/reload: "openbao-secrets-file"
spec:
replicas: 1
selector:
matchLabels:
app: openbao-csi-file-app
template:
metadata:
labels:
app: openbao-csi-file-app
spec:
serviceAccountName: openbao-csi-sa
containers:
- name: app
image: busybox:latest
command:
- "sh"
- "-c"
- |
while true; do
echo "=== Secrets from file ==="
echo "Username: $(cat /mnt/secrets/username)"
echo "Password: $(cat /mnt/secrets/password)"
sleep 30
done
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: openbao-secrets-file
Key Configuration:
secretproviderclass.reloader.stakater.com/reload: "openbao-secrets-file"- Tells Reloader to watch the named SecretProviderClass for version changes- Secrets read directly from
/mnt/secrets/files - No environment variables referencing Kubernetes Secrets
Step 7: Verify Setup#
Apply manifests:
kubectl apply -f serviceaccount.yaml
kubectl apply -f secret-provider-class.yaml
kubectl apply -f deployment.yaml
Check pod is running:
kubectl get pods -n openbao-csi-test -l app=openbao-csi-file-app
Verify secrets are mounted:
kubectl exec -n openbao-csi-test deploy/openbao-csi-file-app -- cat /mnt/secrets/password
Check SecretProviderClassPodStatus:
kubectl get secretproviderclasspodstatus -n openbao-csi-test
View version information:
kubectl get secretproviderclasspodstatus -n openbao-csi-test -o yaml | grep -A5 "objects:"
Example output:
status:
objects:
- id: username
version: "u35OH4bBShH7wgv0YNyr1dzoZzJyKwDFq845rA6Ca_k="
- id: password
version: "hYds2SjGgvcyw2BIk9aYSJtxUV4hbIefYN3oPjO3WhA="
Step 8: Test Secret Rotation#
Record current versions:
kubectl get secretproviderclasspodstatus -n openbao-csi-test -o yaml | grep -A5 "objects:"
Update secret in OpenBao:
bao kv put secret/myapp username=myuser password=rotated-password
Wait for CSI rotation and check version change:
# After ~30-45 seconds (based on rotationPollInterval)
kubectl get secretproviderclasspodstatus -n openbao-csi-test -o yaml | grep -A5 "objects:"
The password version hash should change:
status:
objects:
- id: username
version: "u35OH4bBShH7wgv0YNyr1dzoZzJyKwDFq845rA6Ca_k=" # unchanged
- id: password
version: "yNvjDhGGwPLMPhAplHYNJ4eEWrq6zFzA7XGV7Oz6erQ=" # CHANGED
Reloader detects this change and triggers a pod restart.
How It Works#
- CSI Driver mounts secrets - Secrets are mounted as files in the pod's volume
- SecretProviderClassPodStatus created - CSI driver creates a status resource tracking mounted secrets and their versions
- Rotation polling - CSI driver periodically checks OpenBao for changes
- Version update - When secrets change, CSI driver updates the version hash in SecretProviderClassPodStatus
- Reloader detects change - Reloader watches SecretProviderClassPodStatus for the named SecretProviderClass and triggers pod restart on version changes
Reloader Annotation#
For file-based CSI secrets, use the dedicated annotation on the Deployment:
metadata:
annotations:
secretproviderclass.reloader.stakater.com/reload: "<secretproviderclass-name>"
This is different from the standard Secret/ConfigMap annotations. Reloader watches the SecretProviderClassPodStatus resources and triggers restarts when the version hashes change.
Comparison: File-Based vs K8s Secret Sync#
| Aspect | File-Based | K8s Secret Sync |
|---|---|---|
| Secret storage | Mounted files only | Files + K8s Secret |
| Reloader annotation | secretproviderclass...reload |
reloader...match on Secret |
| Security | Secrets never in etcd | Secrets stored in etcd |
| env var support | Read from file | Native secretKeyRef |
| Complexity | Simpler | More configuration |
Important Notes#
Provider Name#
Use openbao as the provider name:
spec:
provider: openbao
Rotation Must Be Enabled#
The CSI driver must have rotation enabled to detect changes:
--set enableSecretRotation=true
--set rotationPollInterval=30s
Application Must Read Files#
Applications must be designed to read secrets from mounted files rather than environment variables.
Troubleshooting#
Version Not Changing#
- Verify CSI driver has rotation enabled
- Check rotationPollInterval setting
- Wait for the full poll interval after rotating secrets
Secrets Not Mounting#
Check OpenBao CSI provider logs:
kubectl logs -n openbao -l app.kubernetes.io/name=openbao-csi-provider
Reloader Not Triggering Restart#
- Verify the annotation references the correct SecretProviderClass name
- Check Reloader logs for errors
- Ensure SecretProviderClassPodStatus exists and shows version changes