Hardcoded Kubernetes secrets become a problem once you're running more than one app. This guide shows you how to set up the External Secrets Operator with OpenBao, so credentials live in one self-hosted vault instead of sitting in YAML files or CI/CD pipelines.
By the end, you'll have ESO installed with Helm, a self-hosted OpenBao backend, and app secrets syncing automatically into Kubernetes, with rotation handled for you.
What You Need
Before you start, confirm the following:
- A running Kubernetes cluster, such as K3s, kubeadm, or managed, on a supported version.
- kubectl configured to talk to that cluster.
- Helm 3.x installed locally.
- OpenBao already running, unsealed, and reachable from your cluster, with the
kv engine enabled. If you haven't set it up yet, check this guide on self-hosting OpenBao on Ubuntu.
- Network access between the cluster and your OpenBao address, ideally over a private network, not the public internet.
If you're picking hardware for the cluster, you can compare plans on the dedicated server hosting page.
Step 1: Install External Secrets Operator 2.8 with Helm
First, add the Helm repository and install the chart into its own namespace:
helm repo add external-secrets https://charts.external-secrets.iohelm repo update helm install external-secrets external-secrets/external-secrets \ --namespace external-secrets \ --create-namespace \ --version 0.19.2 \ --set installCRDs=true
Chart versions don't always match the app version number, so check which chart version maps to the app 2.8.0 before you run the install:
helm search repo external-secrets/external-secrets --versions | head -n 5
Once installed, check that everything is running:
kubectl get pods -n external-secretskubectl get crds | grep external-secrets.io
You should see externalsecrets.external-secrets.io, secretstores.external-secrets.io, and clustersecretstores.external-secrets.io in the list. This confirms ESO is ready, which is what you need before you configure External Secrets Operator with OpenBao in the next steps.
Step 2: Add Kubernetes Auth to Your Existing OpenBao Server
Your OpenBao server already has TLS and a kv v2 engine from the setup guide, so this step only adds a login method for Kubernetes. From your own machine, point the bao CLI at your server and log in if you're not already:
export BAO_ADDR="https://bao.example.com:8200"bao login <root-token>
Turn on Kubernetes auth, so ESO can log in using its own ServiceAccount token instead of the root token:
bao auth enable kubernetes bao write auth/kubernetes/config \ kubernetes_host="https://<your-k8s-api-server>:6443"
Replace <your-k8s-api-server> with your cluster's real API address. Add a policy that only allows read access to your app's path under the existing kv engine:
cat <<EOF | bao policy write eso-read -path "kv/data/my-app/*" { capabilities = ["read"]}EOF
Then link that policy to a role tied to the ServiceAccount ESO will use:
bao write auth/kubernetes/role/eso-role \ bound_service_account_names=external-secrets \ bound_service_account_namespaces=external-secrets \ policies=eso-read \ ttl=1h
Step 3: Configure SecretStore for OpenBao Access
This is where you connect the two systems. Since OpenBao works like Vault, the SecretStore uses ESO's vault provider block, pointed at your OpenBao address and your existing kv mount.
Create the SecretStore YAML file:
1apiVersion: external-secrets.io/v12kind: SecretStore3metadata:4 name: openbao-backend5 namespace: my-app6spec:7 provider:8 vault:9 server: "https://bao.example.com:8200"10 path: "kv"11 version: "v2"12 auth:13 kubernetes:14 mountPath: "kubernetes"15 role: "eso-role"16 serviceAccountRef:17 name: "external-secrets"18 namespace: "external-secrets"
If you want one store shared across every namespace, use ClusterSecretStore instead:
1apiVersion: external-secrets.io/v12kind: ClusterSecretStore3metadata:4 name: openbao-cluster5spec:6 provider:7 vault:8 server: "https://bao.example.com:8200"9 path: "kv"10 version: "v2"11 auth:12 kubernetes:13 mountPath: "kubernetes"14 role: "eso-role"15 serviceAccountRef:16 name: "external-secrets"17 namespace: "external-secrets"
Apply it and check its status:
kubectl apply -f secretstore.yamlkubectl get secretstore -n my-appkubectl describe secretstore openbao-backend -n my-app
A Valid status means ESO can reach and log in to OpenBao.
Step 4: Sync Application Credentials with ExternalSecret
Write a test secret into your existing kv engine, under its own app path so it stays separate from the kv/myapp example in the setup guide:
bao kv put kv/my-app/database \ username="app_user" \ password="S3cur3P@ss!"
Then create an ExternalSecret that copies it into a normal Kubernetes Secret:
1apiVersion: external-secrets.io/v12kind: ExternalSecret3metadata:4 name: database-credentials5 namespace: my-app6spec:7 refreshInterval: 15m8 secretStoreRef:9 name: openbao-cluster10 kind: ClusterSecretStore11 target:12 name: database-credentials13 creationPolicy: Owner14 data:15 - secretKey: username16 remoteRef:17 key: my-app/database18 property: username19 - secretKey: password20 remoteRef:21 key: my-app/database22 property: password
refreshInterval: 15m tells ESO to check OpenBao every 15 minutes and update the Kubernetes Secret if the value changed. Apply it and check:
kubectl apply -f externalsecret.yamlkubectl get externalsecret -n my-appkubectl get secret database-credentials -n my-app -o yaml
Shape Secret Output with ESO Templates
Some apps expect one connection string, not separate keys. ESO's template feature lets you build that string at sync time.
nano externalsecret-template.yaml
1apiVersion: external-secrets.io/v12kind: ExternalSecret3metadata:4 name: database-connection-string5 namespace: my-app6spec:7 refreshInterval: 15m8 secretStoreRef:9 name: openbao-cluster10 kind: ClusterSecretStore11 target:12 name: database-connection-string13 creationPolicy: Owner14 template:15 engineVersion: v216 data:17 DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@postgres.my-app.svc:5432/appdb"18 data:19 - secretKey: username20 remoteRef:21 key: my-app/database22 property: username23 - secretKey: password24 remoteRef:25 key: my-app/database26 property: password
This template runs on every refresh, so if the password changes, the built DATABASE_URL changes with it, automatically.
Rotating Secrets Without Any Manual Redeploy
To rotate secrets without any manual redeploy, change the password directly in OpenBao:
bao kv put kv/my-app/database \ username="app_user" \ password="N3wR0t@tedP@ss!"
Don't touch Kubernetes Secret or restart anything. Within the refreshInterval window, ESO picks up the change and updates the target Secret for you:
kubectl get externalsecret database-credentials -n my-app -o jsonpath='{.status.refreshTime}'kubectl get secret database-credentials -n my-app -o jsonpath='{.data.password}' | base64 -d
You'll see the new password there without running kubectl apply again. If your app doesn't reload secrets on its own, add a tool like Reloader, so pods restart automatically when the Secret changes.
To force a refresh immediately instead of waiting, you can add a quick annotation:
kubectl annotate externalsecret database-credentials -n my-app \ force-sync=$(date +%s) --overwrite
A Few Security Tips
-
Always use https:// for the server: address, and pass OpenBao's CA certificate through caProvider. Don't skip TLS checks, since your OpenBao server already runs with TLS from the setup guide.
-
Keep auth roles limited: one role per team or namespace, not one shared role for everyone.
-
Match refreshInterval to how sensitive the secret is. Short-lived tokens need faster checks than a database password.
-
Use store rules to stop one team from reading another team's secret paths under the shared kv engine.
-
Keep using the snapshot backups from the Ubuntu setup guide, since OpenBao now holds every credential your apps need.
Conclusion
At this point, you have a working self-hosted secrets setup. OpenBao stores the credentials, ESO 2.8 watches them, and Kubernetes secrets update themselves when something changes. Once you configure External Secrets Operator with OpenBao this way, rotating a password takes one bao kv put command instead of a manual redeploy across every environment.
We hope you enjoy this guide. For more details on which ESO versions stay supported and for how long, check the official Stability and Support policy before upgrading.