Automate Kubernetes Secret Rotation with ESO and OpenBao

Updated on Sep 3, 2026
Mila H
6 MINS READ
Table of Contents
Kubernetes Secret Rotation with ESO and OpenBao

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:

Bash
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:

Bash
helm search repo external-secrets/external-secrets --versions | head -n 5

Once installed, check that everything is running:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
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:

Bash
nano secretstore.yaml

For one namespace:

YAML
apiVersion: external-secrets.io/v1kind: SecretStoremetadata:  name: openbao-backend  namespace: my-appspec:  provider:    vault:      server: "https://bao.example.com:8200"      path: "kv"      version: "v2"      auth:        kubernetes:          mountPath: "kubernetes"          role: "eso-role"          serviceAccountRef:            name: "external-secrets"            namespace: "external-secrets"

If you want one store shared across every namespace, use ClusterSecretStore instead:

YAML
apiVersion: external-secrets.io/v1kind: ClusterSecretStoremetadata:  name: openbao-clusterspec:  provider:    vault:      server: "https://bao.example.com:8200"      path: "kv"      version: "v2"      auth:        kubernetes:          mountPath: "kubernetes"          role: "eso-role"          serviceAccountRef:            name: "external-secrets"            namespace: "external-secrets"

Apply it and check its status:

Bash
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:

Bash
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:

Bash
nano externalsecret.yaml
YAML
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:  name: database-credentials  namespace: my-appspec:  refreshInterval: 15m  secretStoreRef:    name: openbao-cluster    kind: ClusterSecretStore  target:    name: database-credentials    creationPolicy: Owner  data:    - secretKey: username      remoteRef:        key: my-app/database        property: username    - secretKey: password      remoteRef:        key: my-app/database        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:

Bash
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.

Bash
nano externalsecret-template.yaml
YAML
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:  name: database-connection-string  namespace: my-appspec:  refreshInterval: 15m  secretStoreRef:    name: openbao-cluster    kind: ClusterSecretStore  target:    name: database-connection-string    creationPolicy: Owner    template:      engineVersion: v2      data:        DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@postgres.my-app.svc:5432/appdb"  data:    - secretKey: username      remoteRef:        key: my-app/database        property: username    - secretKey: password      remoteRef:        key: my-app/database        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:

Bash
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:

Bash
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:

Bash
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.

Not yet. It uses ESO's Vault provider, since OpenBao's API matches Vault's.

1.35 and 1.36, per the official ESO support page, until version 2.9 comes out.

No. Reuse the kv engine from your OpenBao setup and just add a new path under it for each app.