Skip to main content

The Guide to OpenBao - Kubernetes Integration - Part 10

With authentication and secrets engines configured, the next step is enabling applications to consume secrets. This article covers three integration patterns on OpenShift:

  1. the Agent Injector for sidecar injection

  2. the CSI Provider for volume mounts

  3. the External Secrets Operator (ESO) for syncing to native Kubernetes Secrets.

Introduction

There are three main patterns for Kubernetes workloads to consume OpenBao secrets:

MethodHow It WorksProsCons

Agent Injector

Sidecar injects secrets as files under /vault/secrets/

Automatic renewal, templates, dynamic secrets

Extra container per pod

CSI Provider

Mounts secrets as volumes (optional sync to a Secret)

No sidecar needed

No continuous auto-renewal (refresh on pod restart)

External Secrets Operator

Syncs to native Kubernetes Secrets

GitOps-friendly, standard secretKeyRef / volumes

Additional operator + NetworkPolicy considerations on OpenShift

OpenBao keeps a Vault-compatible API. Agent Injector annotations still use the vault.hashicorp.com/ prefix, and ESO uses its Vault provider. The CSI provider, however, uses provider: openbao and baoAddress / baoCACertPath parameters. Still, it should be easy to understand the differences and map everything to HashiCorp Vault.

Lab assumptions

These examples assume:

  • OpenBao is installed in namespace openbao with TLS (see the series articles on OpenShift deployment and TLS, and test case TC-OPENBAO-OCP-001).

  • Kubernetes auth is enabled and configured (TC-OPENBAO-AUTH-001 / Part 7).

  • A KV v2 mount exists at secret/ (Part 8).

  • Policy myapp allows read on secret/data/myapp/* (and metadata list as needed).

Related Test Cases are:

  1. TC-OPENBAO-KV-001 — Enable and Use KV v2 Secrets Engine

  2. TC-OPENBAO-KV-003 — KV Secret Rotation Pattern with CAS

  3. TC-OPENBAO-UC-003 — Multi-Tenant Secret Isolation

  4. TC-OPENBAO-DB-001 — Dynamic PostgreSQL Credentials

These and other test cases are available on tests.stdin.at.

Create the lab namespace and ServiceAccount. This namespace will be reused for all three patterns below:

oc new-project myapp
oc create serviceaccount myapp-sa -n myapp

Store sample secrets and bind the auth role to this namespace:

OpenBao must be installed and you need proper access to the API.

# Brief reminder:
export BAO_CACERT="$HOME/openbao-tests/openbao-ca.crt"
export BAO_ADDR='https://127.0.0.1:8200'
export BAO_TOKEN=$(jq -r '.root_token' openbao-init.json)
bao status
# Allow the myapp identity to read KV v2 data (and list metadata) under secret/myapp/
bao policy write myapp - <<'EOF'
path "secret/data/myapp/*" {
  capabilities = ["read", "list"]
}
path "secret/metadata/myapp/*" {
  capabilities = ["list"]
}
EOF

# Map ServiceAccount myapp-sa in namespace myapp to that policy via Kubernetes auth
bao write auth/kubernetes/role/myapp \
  bound_service_account_names=myapp-sa \
  bound_service_account_namespaces=myapp \
  policies=myapp \
  ttl=1h

# Seed sample secrets for Injector / CSI / ESO examples later in this article
bao kv put secret/myapp/config db_host=pg.example.com db_password=s3cret
bao kv put secret/myapp/api key=api-key-123
If cas_required is enabled on the KV mount (TC-OPENBAO-KV-002), every write must include -cas=<current_version>. When updating values that ESO or CSI consume, rewrite all properties the consumer expects — a partial put replaces the whole version and drops missing keys.

Pattern 1: Agent Injector

The Agent Injector uses a mutating webhook to inject an init container and an optional sidecar that authenticates to OpenBao and renders secrets under /vault/secrets/.

How It Works

1. Pod created with vault.hashicorp.com/* annotations
2. Mutating webhook intercepts admission
3. vault-agent-init (and optionally vault-agent sidecar) injected
4. Agent authenticates with the pod ServiceAccount JWT (Kubernetes auth)
5. Agent retrieves secrets and writes templated files
6. Application reads from /vault/secrets/
7. Sidecar renews tokens / leases and re-renders as needed

Prerequisites

On the OpenShift Helm deployment used in this series, the injector is enabled with the chart (typically two replicas) as part of TC-OPENBAO-OCP-001:

injector:
  enabled: true
  replicas: 2

Verify it is running:

oc get pods -n openbao -l app.kubernetes.io/name=openbao-agent-injector

# Check webhook
oc get mutatingwebhookconfigurations | grep openbao

TLS trust in the application namespace

With a TLS listener, the Agent must trust the OpenBao CA. Copy the CA into the app namespace and reference it from annotations (tls-secret / ca-cert):

oc create secret generic openbao-ca -n myapp \
  --from-literal=ca.crt="$(oc get secret openbao-ca-secret -n openbao -o jsonpath='{.data.ca\.crt}' | base64 -d)" \
  --dry-run=client -o yaml | oc apply -f -

Deploy an application with annotations

The Deployment below follows TC-OPENBAO-INT-003 (OpenBao Agent Sidecar Injector). That test case validates the full path: mutating webhook injection, Kubernetes auth as ServiceAccount myapp-sa, OpenBao role myapp, TLS trust via the openbao-ca secret copied earlier, and rendered files under /vault/secrets/.

In practical terms, the annotations tell the Agent to:

  • authenticate with role myapp (bound to myapp-sa in namespace myapp);

  • fetch KV v2 paths secret/data/myapp/config and secret/data/myapp/api;

  • write templated output to /vault/secrets/config.txt and /vault/secrets/api-key.

cat <<EOF | oc apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
      annotations:
        vault.hashicorp.com/agent-inject: "true" (1)
        vault.hashicorp.com/role: "myapp" (2)
        vault.hashicorp.com/tls-secret: "openbao-ca" (3)
        vault.hashicorp.com/ca-cert: "/vault/tls/ca.crt"
        vault.hashicorp.com/agent-inject-secret-config.txt: "secret/data/myapp/config" (4)
        vault.hashicorp.com/agent-inject-template-config.txt: |
          {{- with secret "secret/data/myapp/config" -}}
          DB_HOST={{ .Data.data.db_host }}
          DB_PASSWORD={{ .Data.data.db_password }}
          {{- end }}
        vault.hashicorp.com/agent-inject-secret-api-key: "secret/data/myapp/api"
        vault.hashicorp.com/agent-inject-template-api-key: |
          {{- with secret "secret/data/myapp/api" -}}
          {{ .Data.data.key }}
          {{- end }}
    spec:
      serviceAccountName: myapp-sa (5)
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: curlimages/curl:latest
          command: ["sleep", "infinity"]
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]
            runAsNonRoot: true
            seccompProfile:
              type: RuntimeDefault
EOF
1Enable injection (API-compatible vault.hashicorp.com annotations).
2Kubernetes auth role name in OpenBao.
3Secret in the app namespace that holds the OpenBao CA.
4KV v2 path includes the /data/ segment for the Agent template.
5Must match bound_service_account_names / namespaces on the role.

Verify injection

oc get pods -n myapp
# Expect READY 2/2 (app + vault-agent) when the sidecar is enabled

oc get pods -n myapp -o jsonpath='{.items[0].spec.containers[*].name}{"\n"}'
# Expected: app vault-agent

oc get pods -n myapp -o jsonpath='{.items[0].spec.initContainers[*].name}{"\n"}'
# Expected: vault-agent-init

oc exec -it deploy/myapp -c app -n myapp -- cat /vault/secrets/config.txt
# Expected: DB_HOST=pg.example.com
#           DB_PASSWORD=s3cret

oc exec -it deploy/myapp -c app -n myapp -- cat /vault/secrets/api-key
# Expected: api-key-123

Common annotations

AnnotationDescription

vault.hashicorp.com/agent-inject

Enable/disable injection ("true" / "false")

vault.hashicorp.com/role

OpenBao Kubernetes auth role

vault.hashicorp.com/tls-secret

Secret containing the CA used to verify OpenBao TLS

vault.hashicorp.com/ca-cert

Path to the CA file inside the Agent (for example /vault/tls/ca.crt)

vault.hashicorp.com/agent-inject-secret-<name>

Secret path; creates /vault/secrets/<name>

vault.hashicorp.com/agent-inject-template-<name>

Consul Template syntax for rendering the file

vault.hashicorp.com/agent-inject-status

Set to "update" to force re-injection

vault.hashicorp.com/agent-pre-populate-only

"true" = init container only, no sidecar

vault.hashicorp.com/agent-revoke-on-shutdown

"true" = revoke the token when the pod terminates

vault.hashicorp.com/tls-skip-verify

Skip TLS verification (lab only — prefer a real CA)

Init-container-only mode

By default the injector adds both vault-agent-init and a long-running vault-agent sidecar. Set agent-pre-populate-only: "true" when the application only needs secrets during the init phase (for example: static KV, config files, API keys). The init container still authenticates, renders files under /vault/secrets/, then exits; the sidecar is omitted, so the pod stays 1/1 Ready and uses less CPU and memory.

Trade-off: there is no continuous lease renewal or re-render. If OpenBao rotates a secret or a dynamic credential expires, the application will not see the update until the pod restarts.

Minimal annotations for this mode:

annotations:
  vault.hashicorp.com/agent-inject: "true" (1)
  vault.hashicorp.com/agent-pre-populate-only: "true" (2)
  vault.hashicorp.com/role: "myapp" (3)
  vault.hashicorp.com/tls-secret: "openbao-ca" (4)
  vault.hashicorp.com/ca-cert: "/vault/tls/ca.crt"
  vault.hashicorp.com/agent-inject-secret-config.txt: "secret/data/myapp/config" (5)
1Enable the mutating webhook for this pod.
2Init container only — skip the sidecar.
3Kubernetes auth role (must match SA name/namespace bindings).
4CA Secret in the app namespace so the Agent trusts OpenBao TLS.
5KV v2 path to render as /vault/secrets/config.txt (add further agent-inject-secret-* / template annotations as needed).
For dynamic database credentials with lease renewal, prefer the full sidecar mode, see TC-OPENBAO-UC-002 (Agent sidecar + PostgreSQL role).

Pattern 2: CSI Provider

The Secrets Store CSI Driver mounts secrets as files without an application sidecar. On OpenShift the lab flow is: install the driver (OperatorHub/Software Catalog preferred), manage ClusterCSIDriver, grant the provider SCC, enable the OpenBao CSI DaemonSet on the existing Helm release, then create a SecretProviderClass.

This section aligns with TC-OPENBAO-INT-002 and the OpenBao CSI documentation.

Installing the Secrets Store CSI Driver

Prefer the OpenShift Operator:

Install Secrets Store CSI Driver for Red Hat OpenShift from OperatorHub. Search for "Secrets Store CSI Driver for Red Hat OpenShift" and install the operator with the default settings.

OperatorHub/Software Catalog — Secrets Store CSI Driver for Red Hat OpenShift

On OpenShift, each CSI driver is switched on with a cluster-scoped ClusterCSIDriver whose metadata.name must match the driver identifier — here it is secrets-store.csi.k8s.io.

Create that object next:

apiVersion: operator.openshift.io/v1
kind: ClusterCSIDriver
metadata:
  name: secrets-store.csi.k8s.io (1)
spec:
  managementState: Managed
1Must equal the Secrets Store CSI driver name

OpenBao CSI provider on OpenShift

The provider uses hostPath and needs the privileged SCC:

oc adm policy add-scc-to-user privileged -z openbao-csi-provider -n openbao

Enable CSI on the existing OpenBao Helm release (chart 0.18.0+), mounting the OpenBao CA into the DaemonSet — same steps (Step 4) as TC-OPENBAO-INT-002:

If this was done already, you can skip this step. The important part here is to enable the CSI part in OpenBao.
helm upgrade --install openbao openbao/openbao -n openbao --reuse-values \
  --set csi.enabled=true \
  --set csi.daemonSet.securityContext.container.privileged=true \
  --set 'csi.volumes[0].name=openbao-ca' \
  --set 'csi.volumes[0].secret.secretName=openbao-ca-secret' \
  --set 'csi.volumeMounts[0].name=openbao-ca' \
  --set 'csi.volumeMounts[0].mountPath=/openbao/tls' \
  --set 'csi.volumeMounts[0].readOnly=true' \
  --set 'csi.agent.extraArgs[0]=-ca-path=/openbao/tls/ca.crt'

Verify:

oc get ds -n openbao openbao-csi-provider
# Desired/Ready should match the node count

Creating a SecretProviderClass

A SecretProviderClass tells the Secrets Store CSI driver which provider to call and which secrets to mount. The CSI node plugin talks to the OpenBao CSI provider DaemonSet over gRPC; that provider then authenticates to OpenBao (Kubernetes auth role myapp in our test case) and fetches the listed objects.

Unlike the Agent Injector annotations and ESO’s Vault provider, the CSI parameters are OpenBao-specific. The CA file path must match the volume mounted into the provider DaemonSet in the Helm step above (/openbao/tls/ca.crt), so TLS verification succeeds.
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: openbao-secrets
  namespace: myapp
spec:
  provider: openbao (1)
  parameters:
    baoAddress: "https://openbao.openbao.svc:8200" (2)
    baoCACertPath: "/openbao/tls/ca.crt" (3)
    roleName: "myapp" (4)
    objects: |
      - objectName: "db_password"
        secretPath: "secret/data/myapp/config"
        secretKey: "db_password"
      - objectName: "api_key"
        secretPath: "secret/data/myapp/api"
        secretKey: "key"
  secretObjects: (5)
    - secretName: myapp-synced
      type: Opaque
      data:
        - objectName: db_password
          key: db_password
        - objectName: api_key
          key: api_key
1OpenBao CSI provider name — do not use vault here.
2In-cluster Service DNS and HTTPS listener; avoid the public Route for node-local mounts.
3CA path inside the CSI provider DaemonSet (from the Helm csi.volumeMounts), not a path in the app pod.
4Kubernetes auth role; the mounting pod’s ServiceAccount must match the role bindings.
5Optional: also sync mounted files into a native Secret (myapp-synced) for secretKeyRef / env vars.

Using CSI in a Pod

The application pod does not talk to OpenBao itself. It mounts an ephemeral CSI volume that references the SecretProviderClass. On start, kubelet asks the Secrets Store CSI driver; the driver calls the OpenBao provider, which logs in with the pod’s ServiceAccount JWT and writes the secret objects into the volume (and, if configured, into the synced Secret).

Use the same ServiceAccount (myapp-sa) that the OpenBao role allows. Unlike the Agent Injector, there is no sidecar — the pod stays a single container, and secret files appear under the mount path you choose (here /mnt/secrets).

This time a simple Pod …​ because I am too lazy to create a Deployment.

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  namespace: myapp
spec:
  serviceAccountName: myapp-sa (1)
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: curlimages/curl:latest
      command: ["sleep", "infinity"]
      securityContext:
        allowPrivilegeEscalation: false
        capabilities:
          drop: ["ALL"]
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      volumeMounts:
        - name: secrets
          mountPath: /mnt/secrets (2)
          readOnly: true
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: myapp-synced (3)
              key: db_password
  volumes:
    - name: secrets
      csi:
        driver: secrets-store.csi.k8s.io (4)
        readOnly: true
        volumeAttributes:
          secretProviderClass: openbao-secrets (5)
1Must match bound_service_account_names / namespaces on role myapp.
2Files such as db_password and api_key appear here (objectName from the SecretProviderClass).
3Optional env consumption via the synced Secret from secretObjects — only works if that block was defined.
4Driver registered by the ClusterCSIDriver earlier in this section.
5Links this volume to the SecretProviderClass in the same namespace.

Verify:

oc exec -it myapp-pod -n myapp -- ls -la /mnt/secrets/
oc exec -it myapp-pod -n myapp -- cat /mnt/secrets/db_password

Pattern 3: External Secrets Operator (ESO)

The External Secrets Operator (ESO) synchronises secrets from OpenBao into native Kubernetes Secrets. It is a controller that talks to an external secrets backend (OpenBao here) and creates a native Secret in the namespace for the application to consume. The OpenShift lab path is TC-OPENBAO-INT-001.

Installing External Secrets Operator

On OpenShift, install External Secrets Operator for Red Hat OpenShift from OperatorHub/Software Catalog. After the Operator is installed, create an ExternalSecretsConfig named cluster. This is a cluster-scoped object and will start the controller.

The default NetworkPolicies for the Red Hat ESO operand deny egress. Without an allow rule to OpenBao on TCP/8200, SecretStore login fails with context deadline exceeded.
apiVersion: operator.openshift.io/v1alpha1
kind: ExternalSecretsConfig
metadata:
  name: cluster
spec:
  controllerConfig:
    networkPolicies:
      - name: allow-openbao-egress
        componentName: ExternalSecretsCoreController
        egress:
          - ports:
              - protocol: TCP
                port: 8200
            to:
              - namespaceSelector:
                  matchLabels:
                    kubernetes.io/metadata.name: openbao

Verify the deployment:

oc get pods -n external-secrets

Creating a SecretStore

A SecretStore is a namespaced object that defines the connection to OpenBao: it tells ESO how to reach OpenBao and how to authenticate. It does not create application Secrets by itself — that is the job of an ExternalSecret in the next step.

Because the lab uses TLS, first fetch the CA certificate from the OpenBao CA Secret and use it for the SecretStore.

# Base64 CA PEM (one line) for caBundle
oc get secret openbao-ca-secret -n openbao -o jsonpath='{.data.ca\.crt}{"\n"}'

ESO has no separate “OpenBao” provider. Use the Vault provider against OpenBao’s compatible API, with Kubernetes auth and the same myapp / myapp-sa bindings as Injector and CSI:

Replace the <BASE64_CA_PEM> with the base64 encoded CA certificate.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: openbao-backend
  namespace: myapp
spec:
  provider:
    vault: (1)
      server: "https://openbao.openbao.svc:8200" (2)
      path: "secret" (3)
      version: "v2"
      caBundle: "<BASE64_CA_PEM>" (4)
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "myapp" (5)
          serviceAccountRef:
            name: myapp-sa
1Vault provider → OpenBao’s compatible HTTP API (there is no openbao: block in ESO yet).
2In-cluster Service over HTTPS; the ESO controller must be allowed to reach it (NetworkPolicy above).
3KV mount path without trailing slash; version.
4Paste the base64 value from openbao-ca-secret (do not wrap or re-encode).
5Role and SA must match OpenBao’s Kubernetes auth bindings for namespace myapp.
oc get secretstore openbao-backend -n myapp
# STATUS should indicate Ready / Valid
If the store stays Not Ready, check NetworkPolicy egress to OpenBao, the caBundle, and that role myapp still allows myapp-sa in myapp.
For cluster-wide access, use a ClusterSecretStore with a ServiceAccount that OpenBao trusts (role + policy bound to that SA/namespace).

Creating an ExternalSecret

An ExternalSecret maps remote OpenBao keys onto a native Kubernetes Secret. ESO periodically authenticates via the SecretStore, reads the listed properties, and writes (or updates) the target Secret. Applications then use ordinary secretKeyRef or volume mounts — they never call OpenBao directly. This keeps applications secret-agnostic so developers can keep using ordinary Kubernetes Secret consumption.

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: myapp-secrets
  namespace: myapp
spec:
  refreshInterval: "5m" (1)
  secretStoreRef:
    name: openbao-backend
    kind: SecretStore
  target:
    name: myapp-secrets (2)
    creationPolicy: Owner
    template:
      type: Opaque
  data:
    - secretKey: db_host (3)
      remoteRef:
        key: myapp/config (4)
        property: db_host
    - secretKey: db_password
      remoteRef:
        key: myapp/config
        property: db_password
    - secretKey: api_key
      remoteRef:
        key: myapp/api
        property: key
1How often ESO re-reads OpenBao; use force-sync (below) for an immediate refresh in the lab.
2Name of the Kubernetes Secret that ESO creates or owns in this namespace.
3Key name inside the Kubernetes Secret (what the app will reference).
4Path relative to the store’s path: secretwithout the KV v2 /data/ segment.
For KV v2, ESO’s remoteRef.key is typically myapp/config, while Agent/CSI templates use secret/data/myapp/config. Mixing the two path styles is a common source of sync errors.

Verify:

The first command will show the status of the sync process. The second command will fetch the actual generated secret and show the keys and values.

oc get ExternalSecret myapp-secrets -n myapp
# STATUS should be SecretSynced

oc get secret myapp-secrets -n myapp -o jsonpath='{.data}' | \
  jq 'to_entries[] | {key: .key, value: (.value | @base64d)}'
# Should show db_host, db_password, api_key

Fetching all keys from a specific path

When every field under one OpenBao secret should become a Kubernetes Secret key, use dataFrom.extract instead of listing each property. Here all keys from secret/myapp/config (for example db_host, db_password) are copied into myapp-all-config:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: myapp-all-config
  namespace: myapp
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: openbao-backend
    kind: SecretStore
  target:
    name: myapp-all-config
  dataFrom:
    - extract:
        key: myapp/config

Using the generated Secret

Once the ExternalSecret is synced, treat myapp-secrets like any other Opaque Secret: environment variables via secretKeyRef, or files via a secret volume. No Injector annotations or CSI SecretProviderClass are required on the workload.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-eso
  namespace: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp-eso
  template:
    metadata:
      labels:
        app: myapp-eso
    spec:
      containers:
        - name: app
          image: registry.access.redhat.com/ubi9/ubi-minimal:latest
          command: ["sleep", "infinity"] (1)
          env:
            - name: DB_HOST
              valueFrom:
                secretKeyRef:
                  name: myapp-secrets (2)
                  key: db_host
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: myapp-secrets
                  key: db_password
          volumeMounts:
            - name: config
              mountPath: /etc/config
              readOnly: true
      volumes:
        - name: config
          secret:
            secretName: myapp-secrets (3)
1Keeps the lab Pod running so oc exec verification works; replace with the real application entrypoint in production.
2Keys match secretKey values from the ExternalSecret data list.
3Same Secret mounted as files under /etc/config (one file per key).

Verify:

# Environment variables from secretKeyRef
oc exec -n myapp deploy/myapp-eso -- printenv DB_HOST DB_PASSWORD
# Expect the decoded values from myapp-secrets

# Files from the secret volume (one file per key)
oc exec -n myapp deploy/myapp-eso -- ls -l /etc/config
# Expect db_host, db_password, api_key

Refresh after an OpenBao update

KV v2 writes replace the entire version. When you rotate a password, rewrite every property the ExternalSecret still expects, or those keys disappear from the synced Secret. If cas_required is enabled, include -cas=<current_version> (see TC-OPENBAO-KV-002).

# Rewrite all fields the ExternalSecret reads (KV v2 put replaces the version)
bao kv put --cas=<latest_version> secret/myapp/config \
  db_host=pg.example.com \
  db_password=newP@ssw0rd

# Force an immediate sync (do not wait for refreshInterval)
oc annotate externalsecret myapp-secrets -n myapp force-sync=$(date +%s) --overwrite

Verifying ExternalSecret status

Confirm both the ExternalSecret condition and the materialised Secret contents:

oc get externalsecret -n myapp
# Expect SecretSynced / Ready

oc get secret myapp-secrets -n myapp -o jsonpath='{.data}' | \
  jq 'to_entries[] | {key: .key, value: (.value | @base64d)}'

Comparing integration methods

FeatureAgent InjectorCSI ProviderESO

Auto-renewal

Yes

No (pod lifecycle)

Yes (refresh interval / force-sync)

Sidecar needed

Yes

No

No

GitOps-friendly

Annotations on workloads

SecretProviderClass + pod volume

Yes (ExternalSecret CRs)

Standard K8s Secret

No (files; optional app logic)

Optional via secretObjects

Yes

Dynamic secrets

Yes

Limited

Limited

Template support

Yes (Agent templates)

No

Yes (ESO templates)

OpenShift extras

Injector CA secret

ClusterCSIDriver, SCC, Helm CSI

ExternalSecretsConfig + NetworkPolicy

Best practices

Choose the right pattern

Security considerations

# Bind roles to a single namespace and SA
bao write auth/kubernetes/role/myapp \
  bound_service_account_names=myapp-sa \
  bound_service_account_namespaces=myapp \
  policies=myapp \
  ttl=1h

# Prefer short TTLs for dynamic database roles
bao write database/roles/myapp-readonly \
  default_ttl=1h \
  max_ttl=4h

Always trust OpenBao TLS with a real CA (caBundle, baoCACertPath, or tls-secret) instead of tls-skip-verify outside throwaway labs.

Conclusion

This article covered three patterns for integrating OpenBao with Kubernetes and OpenShift:

  • Agent Injector: dynamic secrets and auto-renewal via annotated pods

  • CSI Provider: volume mounts (and optional synced Secrets) with the OpenBao CSI provider

  • External Secrets Operator: GitOps-friendly native Secrets, including OpenShift NetworkPolicy and TLS details

Choose the pattern that fits each workload, or combine them. Step-by-step validation lives in the OpenBao plan on tests.stdin.at: TC-OPENBAO-INT-001 — ESO, TC-OPENBAO-INT-002 — CSI, TC-OPENBAO-INT-003 — Injector.


Discussion

Previous
Use arrow keys to navigate
Next