Introduction
Your secret lives in OpenBao. Now you will deliver it to the cluster as an ordinary Kubernetes Secret, kept in sync by the External Secrets Operator (ESO). Your manifests reference a path in OpenBao; the secret value never appears in them.
ESO works through two resources: a SecretStore that says how to reach a backend, and an ExternalSecret that says which values to pull and what Kubernetes Secret to write.
Step 1 — Install the External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
-n external-secrets-system --create-namespace --wait
Confirm the operator is running:
kubectl get pods -n external-secrets-system
NAME READY STATUS RESTARTS AGE
external-secrets-8977b889d-vt4sq 1/1 Running 0 80s
external-secrets-cert-controller-5d4dc587db-pkckx 1/1 Running 0 80s
external-secrets-webhook-85d855b54-4w9hq 1/1 Running 0 80s
Step 2 — Give ESO a token and a SecretStore
ESO authenticates to OpenBao with a token. OpenBao is Vault-compatible, so you use ESO’s vault provider. Create a namespace, a Kubernetes Secret holding the OpenBao token (root in dev mode, base64-encoded as cm9vdA==), and a SecretStore pointing at the OpenBao service:
kubectl create namespace demo-app
kubectl apply -f- <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: openbao-token
namespace: demo-app
data:
token: cm9vdA== # base64("root")
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: openbao-backend
namespace: demo-app
spec:
provider:
vault:
server: "http://openbao.openbao.svc:8200"
path: "secret"
version: "v2"
auth:
tokenSecretRef:
name: "openbao-token"
key: "token"
EOF
In production you would give ESO a scoped token from a dedicated policy, with read access to only the paths it needs.
Step 3 — Create an ExternalSecret
The ExternalSecret names the source path and the keys to pull, and the target Secret to create:
kubectl apply -f- <<'EOF'
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: myapp-config
namespace: demo-app
spec:
refreshInterval: "15s"
secretStoreRef:
name: openbao-backend
kind: SecretStore
target:
name: myapp-config
data:
- secretKey: api_key
remoteRef:
key: myapp/config
property: api_key
- secretKey: db_password
remoteRef:
key: myapp/config
property: db_password
EOF
Step 4 — Verify the synced Secret
Check that the ExternalSecret synced:
kubectl -n demo-app get externalsecret myapp-config
NAME STORE REFRESH INTERVAL STATUS READY
myapp-config openbao-backend 15s SecretSynced True
ESO created a native Kubernetes Secret with the values from OpenBao:
kubectl -n demo-app get secret myapp-config -o jsonpath='{.data.api_key}' | base64 -d
s3cr3t-123
The value matches what you stored in OpenBao, and it never appeared in a manifest.
Step 5 — Use it in a workload
myapp-config is an ordinary Secret, so a Pod consumes it the usual way:
envFrom:
- secretRef:
name: myapp-config
When you rotate the value in OpenBao, ESO refreshes the Secret on its refreshInterval, and your workload picks up the new value on its next restart.
Clean up
kubectl delete namespace demo-app
helm uninstall external-secrets -n external-secrets-system
What’s next
You now have the open-source foundation: OpenBao storing secrets and ESO syncing them into Kubernetes. From here you can add per-path policies, dynamic secrets, and audit logging — and run the whole thing as a managed, multi-tenant platform with Kubermatic SecureGuard.
Summary
- The External Secrets Operator syncs secrets from a backend into native Kubernetes
Secretobjects. - A
SecretStoredefines how to reach OpenBao (the Vault-compatible provider, a token, the KV path and version). - An
ExternalSecretnames the source path and keys and the targetSecretto write. - Workloads consume the result as a normal
Secret; rotation in OpenBao flows through on the refresh interval.
