Inject an OpenBao Secret into a Pod

Inject a versioned OpenBao secret into a Kubernetes pod with Agent injection.

Introduction

30 Second Summary

A locked note is only useful when the right person can open it. Software faces the same problem whenever it needs sensitive values without exposing them.

In this project, you will run a Kubernetes pod that uses its own ServiceAccount identity to read a versioned secret from OpenBao. You will prove the full path by moving from a missing file to a rendered secret file with narrowly scoped access.

What You'll Build

You watch a BusyBox application print foo=a beside bar=b from /vault/secrets/my-secret without storing either value in a Kubernetes Secret.

By the end of this project, you'll have:

  • A visible baseline failure that repeatedly prints CONFIG_MISSING: /vault/secrets/my-secret. You can show exactly why the original pod cannot access its configuration.
  • A scoped workload identity where the pod exchanges its ServiceAccount identity for read-only OpenBao access to secret/data/my-secret.
  • An in-memory secret file at /vault/secrets/my-secret that displays the rendered values. The default namespace contains no Kubernetes Secret named my-secret.
  • Secret Mission: Rotate the versioned secret with Check-And-Set. You watch the running pod receive version 2.

Are there any prerequisites?

You need a supported Mac with at least 4 GB of RAM.

Administrator access is required for the Docker Desktop installation.

Before We Start

Before any setup begins, lock in the lab's security goal: a BusyBox workload uses the Kubernetes ServiceAccount named myapp to authenticate to OpenBao for least-privilege read access to the KV v2 secret at secret/data/my-secret through /vault/secrets/my-secret. This keeps the unrestricted power of an OpenBao root token away from the pod.

Prepare Your Local Kubernetes Workstation

OpenBao needs a reachable Kubernetes API before a pod can exchange its ServiceAccount identity for secret access.

Docker Desktop provides that local Kubernetes environment on your Mac.

The kubectl command lets you inspect the cluster. Helm gives you a repeatable way to install OpenBao later.

In this step, get ready to:
  • Install Docker Desktop 4.88.1 with the recommended settings.
  • Create a one-node kind cluster running Kubernetes 1.36.1.
  • Install Helm 3.21.4. Prepare ~/openbao-k8s-lab for the project files.
Check your Mac and install Docker Desktop

Docker Desktop 4.88.1 needs a supported macOS release. Your Mac also needs at least 4 GB of memory.

Docker Desktop licensing

Docker Desktop is free for personal use or education. Qualifying small businesses can also use it for free.

Choose the tab that matches the result on your Mac.

✔️ I have version 4.88.1

  • Press Enter to launch Docker Desktop.
  • Open the version information from the Docker Desktop application menu.
  • Confirm the displayed version is 4.88.1.
  • Accept the applicable terms if Docker Desktop prompts you.
  • Select the recommended settings if the setup screen appears.
  • Wait for Docker Desktop to finish starting.

Your existing installation is ready for the local cluster. Docker Desktop now provides the runtime for this lab.

ⓧ I have an older version

The older installation needs an in-place update. The download can take a while on a slower connection.

  • Open the Docker Desktop release notes.
  • Download Docker Desktop 4.88.1 for your Mac's processor.
  • Complete the interactive installer.
  • Press Cmd+Space to open Spotlight.
  • Type Docker Desktop into Spotlight.
  • Press Enter to launch Docker Desktop.
  • Accept the applicable terms.
  • Select the recommended settings.

The interactive installation places the application at /Applications/Docker.app. Wait for Docker Desktop to finish starting.

ⓧ Docker Desktop is not installed

You need the current installer before you can create the cluster. The download can take a while on a slower connection.

  • Open the Docker Desktop installation guide for Mac.
  • Download Docker Desktop 4.88.1 for your Mac's processor.
  • Complete the interactive installer.
  • Press Cmd+Space to open Spotlight.
  • Type Docker Desktop into Spotlight.
  • Press Enter to launch Docker Desktop.
  • Accept the applicable terms.
  • Select the recommended settings.

The interactive installation places the application at /Applications/Docker.app. Wait for Docker Desktop to finish starting.

Create the local Kubernetes cluster

The cluster gives OpenBao a local Kubernetes API to communicate with. The one-node configuration keeps this lab small.

Why Docker Desktop Kubernetes?

Docker Desktop supplies the local runtime and Kubernetes control plane. It also configures the kubeconfig context used by kubectl.

Its kind provisioner supports Kubernetes version selection. This keeps the lab infrastructure in one application.

  • Return to Docker Desktop from earlier.
  • Select the Kubernetes view.
  • Click Create cluster.
  • Choose kind as the provisioner.
  • Select Kubernetes 1.36.1.
  • Set the node count to 1.

Cluster creation can take a few minutes while Docker Desktop prepares the Kubernetes components. A short wait here is expected.

  • Click Create.
  • Wait until Docker Desktop reports that the cluster is running.

The local control plane is now running. Next, you will point kubectl at it.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into Spotlight.
  • Press Enter to open Terminal.
  • Switch kubectl to the Docker Desktop cluster by running this command:
kubectl config use-context docker-desktop

What does this command do?

This selects the docker-desktop kubeconfig context. Future kubectl commands now target your local cluster.

Before you check the node, do you expect Kubernetes to report it as ready or still starting?

  • Inspect the local Kubernetes node by running:
kubectl get nodes

What does this check prove?

This asks the Kubernetes API for its registered nodes. A Ready status proves the local node can accept workloads.

You should see the Docker Desktop node with Ready in its status column.

Node not ready?

Confirm Docker Desktop still reports that the Kubernetes cluster is running. Cluster startup may still be finishing.

Repeat the context-switch command if kubectl is targeting another cluster.

Still stuck? Help me diagnose why my Docker Desktop Kubernetes node is not Ready.

Install Helm and prepare the project directory

Helm turns a chart into Kubernetes resources using repeatable commands. This project uses Helm 3.21.4 because it supports Kubernetes 1.36.1.

  • Check the Helm version in the Terminal from earlier by running:
helm version --short

How to use this result

This command prints the installed Helm version. Choose the tab that matches the result in Terminal.

✔️ I see version v3.21.4

Helm 3.21.4 is already installed. Your chart tooling is ready.

ⓧ I see an older version

The official installer updates Helm to the pinned release. It may ask for your Mac password while placing the binary on your system.

  • Install Helm 3.21.4 by running these commands:
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh --version v3.21.4

What do these commands do?

  • The curl command downloads the official Helm 3 installer as get_helm.sh.
  • The permission change allows your Mac to execute the installer.
  • The final command pins the installation to v3.21.4.
  • Verify the updated Helm version by running:
helm version --short

What should I see?

The short version output should report v3.21.4.

Helm update failed?

Check that your Mac has internet access. Run the installer again if the download was interrupted.

Need help? Help me troubleshoot the Helm 3.21.4 installer on my Mac.

ⓧ Command not found

Helm is not installed yet. The official installer may ask for your Mac password while placing the binary on your system.

  • Install Helm 3.21.4 by running these commands:
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh --version v3.21.4

What do these commands do?

  • The curl command downloads the official Helm 3 installer as get_helm.sh.
  • The permission change allows your Mac to execute the installer.
  • The final command installs v3.21.4.
  • Verify the installed Helm version by running:
helm version --short

What should I see?

The short version output should report v3.21.4.

Helm installation failed?

Check that your Mac has internet access. Run the installer again if the download was interrupted.

Need help? Help me troubleshoot the Helm 3.21.4 installation on my Mac.

A dedicated directory keeps the YAML files from later steps together. Your Terminal should remain inside this directory as you build the lab.

  • Create ~/openbao-k8s-lab and enter it by running these commands:
mkdir ~/openbao-k8s-lab
cd ~/openbao-k8s-lab
pwd

What do these commands do?

The first command creates the lab directory in your home folder. The second command moves Terminal into it.

The final command prints your current location. Its output should end with /openbao-k8s-lab.

Before the final check, do you expect both the Kubernetes node and Helm version to confirm the workstation is ready?

  • Verify the cluster and Helm installation together by running:
kubectl get nodes && helm version --short

What does the final check prove?

The first command confirms that kubectl can reach the local cluster. The second command runs only after that check succeeds.

Together, the outputs prove that the Kubernetes API and Helm are ready for the OpenBao installation.

The node row should include Ready. The final line should report v3.21.4.

Final check not passing?

Confirm Docker Desktop is running if the node check fails. Return to the Kubernetes view if the local cluster has stopped.

Return to the matching Helm tab if the version is missing or older.

Still blocked? Help me diagnose my Kubernetes and Helm workstation checks.

Your local Kubernetes workstation is ready. Next, you will run the baseline workload and make its missing-secret problem visible.

Feel the Missing-Secret Problem

Your local Kubernetes cluster is ready to run workloads. Before adding OpenBao, you need proof of the exact gap it solves.

The baseline Pod looks for a configuration file that Kubernetes cannot provide yet. Running this version makes the missing source visible in the application logs.

In this step, get ready to:
  • Create app-before.yaml with a loop that checks for the future secret file.
  • Apply the manifest to the local cluster.
  • Inspect the application logs to observe the missing-secret problem.
Describe the baseline workload

A YAML manifest describes the workload that Kubernetes should run. This one uses BusyBox 1.38.0 to run a small shell loop inside an app container.

  • Start editing app-before.yaml in the ~/openbao-k8s-lab directory by running this command:
nano app-before.yaml

What does this command do?

The nano command opens a terminal text editor. Supplying app-before.yaml creates that file in your current directory when you save it.

  • Fill the blank editor with the baseline manifest by pasting this code:
apiVersion: v1
kind: Pod
metadata:
  name: myapp-before
  labels:
    app: myapp-before
spec:
  containers:
    - name: app
      image: busybox:1.38.0
      command: ["sh", "-c"]
      args:
        - |
          while true; do
            if [ -f /vault/secrets/my-secret ]; then
              echo "SECRET_FOUND"
              cat /vault/secrets/my-secret
            else
              echo "CONFIG_MISSING: /vault/secrets/my-secret"
            fi
            sleep 10
          done

What does this manifest do?

  • The metadata names the Pod myapp-before so you can target it with later commands.
  • The app container uses busybox:1.38.0 to provide the shell used by the loop.
  • The loop checks /vault/secrets/my-secret every ten seconds.
  • The two output branches make the file state visible through the container logs.
  • Press Ctrl+O to open the filename confirmation prompt.

You should see app-before.yaml as the filename to write.

  • Press Enter to save the file.

Nano confirms that it wrote the file.

  • Press Ctrl+X to return to the Terminal prompt.

Having trouble saving the manifest?

Check that the filename prompt shows app-before.yaml before you confirm the save. Make sure you are still working inside ~/openbao-k8s-lab.

Still stuck? Help me save this Kubernetes manifest with nano.

✔️ Awesome, I've got everything!

Your app-before.yaml file is saved. You are ready to launch the baseline Pod.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: Pod
metadata:
  name: myapp-before
  labels:
    app: myapp-before
spec:
  containers:
    - name: app
      image: busybox:1.38.0
      command: ["sh", "-c"]
      args:
        - |
          while true; do
            if [ -f /vault/secrets/my-secret ]; then
              echo "SECRET_FOUND"
              cat /vault/secrets/my-secret
            else
              echo "CONFIG_MISSING: /vault/secrets/my-secret"
            fi
            sleep 10
          done

What should match?

This reference is the exact finished contents of app-before.yaml. The spacing beneath args keeps the shell loop inside the container configuration.

Launch the baseline Pod

Applying the manifest asks the Kubernetes API to create the described Pod. The cluster then starts the app container from the pinned image.

  • Create the baseline Pod from the saved manifest by running:
kubectl apply -f app-before.yaml

What does this command do?

The kubectl apply command submits the resource definition in app-before.yaml to your current docker-desktop context.

Kubectl confirms that it created the Pod resource.

Manifest rejected?

Compare your indentation with the full-file reference above. YAML uses indentation to connect the container configuration to the Pod specification.

Need help? Help me diagnose why app-before.yaml was rejected.

The first image pull can take a moment. The Pod status shows when the container is ready to run its loop.

  • Check the current Pod phase by running:
kubectl get pod myapp-before

What does this status check show?

This command asks Kubernetes for the current state of myapp-before. The status reflects whether its app container has started.

You should see myapp-before with a status of Running. Your baseline application is now executing its file check.

Pod still pending?

  • Wait a moment for Docker Desktop to pull the image.
  • Run the same status check again to confirm the Pod reaches Running.

Still waiting? Help me find why myapp-before is not reaching Running.

Observe the missing-secret loop

The running loop gives you direct evidence about whether the expected configuration file exists. Its logs expose the result without stopping the application.

Before you read the logs, do you think the container can find /vault/secrets/my-secret?

  • Read the baseline application logs by running:
kubectl logs myapp-before

What does this command show?

The kubectl logs command reads text written by the app container. Each recorded loop shows the result of one file check.

You should see CONFIG_MISSING: /vault/secrets/my-secret repeated in the output. This is the designed failure.

Why this failure matters

The application remains healthy because the loop keeps running after each failed check. The missing file proves that the workload has no authenticated source for its configuration yet.

Logs empty or unavailable?

Confirm that the status check from the previous substep reports Running. Wait for another ten-second loop before reading the logs again.

Need a hand? Help me troubleshoot the myapp-before logs.

You have reproduced the exact problem that secret injection needs to solve. Next, you will deploy OpenBao as the source for that missing file.

Deploy OpenBao and Store a Versioned Secret

Your baseline pod is still running. Its repeated CONFIG_MISSING: /vault/secrets/my-secret message proves that the application has no trusted source for its configuration file.

This step introduces OpenBao as that dedicated secret authority. You will deploy it to Kubernetes with Helm. You will then store the first version of a KV v2 secret.

Why dev mode for this lab?

Dev mode runs one initialized OpenBao server with memory-backed storage. This keeps the authentication flow visible without adding initialization or unsealing work.

Dev mode uses insecure defaults. Its data disappears when the server restarts.

Use only the fake values from this project. Production credentials do not belong in this disposable server.

In this step, get ready to:
  • Prepare Helm with the dev-mode values file plus the official chart repository.
  • Render chart version 0.29.3 before installing OpenBao 2.6.2.
  • Store version 1 of the fake KV v2 secret.
Prepare the Helm inputs

A YAML values file overrides the chart's defaults. This one selects the memory-backed server plus the Agent Injector needed later in the project.

  • Create openbao-values.yaml inside the ~/openbao-k8s-lab directory from earlier by copying the configuration below into a new file:
server:
  dev:
    enabled: true
injector:
  enabled: true

What does this configuration do?

  • The server section enables the memory-backed development server.
  • The injector section enables the admission component that can modify annotated pods.
  • Both settings use true so the chart creates both parts of the lab.
  • Save openbao-values.yaml.
  • Confirm openbao-values.yaml appears beside app-before.yaml in ~/openbao-k8s-lab.

Values file missing or malformed?

Check that the file is named openbao-values.yaml. A different extension prevents the later Helm commands from finding it.

Match the two-space indentation shown above. YAML uses indentation to place each enabled setting under the correct section.

Still stuck? Help me check my OpenBao values file.

✔️ Awesome, I've got everything!

Great. Double-check that you saved openbao-values.yaml inside ~/openbao-k8s-lab.

ⓧ I'd like to double check the full code

server:
  dev:
    enabled: true
injector:
  enabled: true

Helm uses a repository index to locate chart packages. Adding the official OpenBao repository gives Helm a trusted chart source.

  • Add the OpenBao repository to Helm by running these commands:
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update

What do these commands do?

  • The first command registers the official OpenBao chart repository under the name openbao.
  • The second command refreshes Helm's local repository index.
  • The updated index lets Helm find chart version 0.29.3 during the next substep.
  • Confirm Terminal reports that the repository was added.
  • Confirm the repository update finishes successfully.

Repository command failed?

Check that your Mac still has internet access. Helm needs to download the repository index.

Check that the repository URL exactly matches the command above. A missing character points Helm at the wrong location.

Need help? Help me troubleshoot the OpenBao Helm repository.

Render and install OpenBao

A dry run asks Helm to render the Kubernetes resources without installing them. This catches values-file problems before the cluster changes.

Before you render the chart, predict whether Helm should create any resources during this check.

  • Render chart version 0.29.3 with the values file by running:
helm install openbao openbao/openbao --version 0.29.3 --values openbao-values.yaml --namespace openbao --create-namespace --dry-run

What does this command do?

  • The release name is openbao.
  • The chart reference points to the OpenBao chart in the repository you added.
  • The version flag pins chart 0.29.3.
  • The values flag applies openbao-values.yaml.
  • The dry-run flag renders the resources without creating them.

Helm prints the rendered Kubernetes manifests. The dry run leaves the cluster unchanged.

Dry run failed?

A values parsing problem usually points to indentation in openbao-values.yaml. Compare the file with the full-code tab above.

A chart lookup problem usually means the repository was not added or updated. Return to the repository commands before retrying.

Need a second pair of eyes? Help me diagnose my OpenBao dry run.

The installation can take a few minutes during the first image pull. A quiet pause here usually means Docker Desktop is downloading the server plus injector images.

  • Install the rendered chart in the openbao namespace by running:
helm install openbao openbao/openbao --version 0.29.3 --values openbao-values.yaml --namespace openbao --create-namespace --wait

What changes during installation?

Helm creates the openbao namespace plus the resources defined by the chart. The wait flag keeps the command running until the required resources become ready.

The values file starts the OpenBao dev server plus its injector. The chart uses OpenBao 2.6.2 for this pinned release.

That is the service layer in place. Your cluster now has a secret authority plus the component that can inject secrets into annotated pods.

  • Check the OpenBao namespace pods by running:
kubectl get pods --namespace=openbao

What does this check prove?

This command lists the pods scoped to the openbao namespace. It confirms that the server plus injector reached their running state.

You should see openbao-0 running. You should also see the Agent Injector pod running.

OpenBao pods not running?

Give the first image pull a little longer if either pod is still starting. Run the pod check again after Docker Desktop finishes downloading the images.

If a pod keeps failing, inspect the Helm installation output for a chart or values error. The dry run should complete before the real installation.

Need help reading the state? Help me troubleshoot the OpenBao pods.

Write and verify version 1

The dev server provides a KV v2 engine at secret/. Each write creates a numbered version that can be read later.

  • Write the fake foo=a plus bar=b values to my-secret by running:
kubectl exec -n openbao openbao-0 -- bao kv put -mount=secret my-secret foo=a bar=b

What does this command do?

  • The kubectl command executes the OpenBao CLI inside openbao-0.
  • The mount flag selects the KV v2 engine at secret/.
  • The secret name is my-secret.
  • The first write creates version 1 with two fake fields.
  • Confirm the write output reports version 1.

Secret write failed?

Confirm openbao-0 is running before retrying. The CLI command executes inside that pod.

Check the field values for missing equals signs. Each value must use the exact key-value form shown above.

Still blocked? Help me troubleshoot the KV v2 write.

Before you read the secret back, make a prediction about the version number that should appear beside both stored values.

  • Read the stored KV v2 secret by running:
kubectl exec -n openbao openbao-0 -- bao kv get -mount=secret my-secret

What does this verification prove?

The command reads the latest version of my-secret from the secret mount. The response includes version metadata plus the stored data.

Reading the value back proves that OpenBao accepted the write. It also gives you a version to compare against future rotations.

You should see version 1. The data table should contain foo=a plus bar=b.

Values missing from the read?

Check that the read uses the same secret mount plus my-secret path as the write.

If no version exists, rerun the write command first. The read can only return data after a successful write.

Need help comparing the commands? Help me troubleshoot the KV v2 read.

Your OpenBao server now holds the versioned secret that the application needs. Next, you will replace unlimited access with a Kubernetes identity plus a read-only policy.

Configure Scoped Kubernetes Authentication

Your OpenBao server now holds version 1 of the KV v2 secret. The baseline Kubernetes pod still has no identity that OpenBao trusts.

A root token would give the workload unlimited power. Kubernetes authentication combines a ServiceAccount identity with a path-specific policy so the future pod can only read secret/data/my-secret.

In this step, get ready to:
  • Enable Kubernetes authentication inside openbao-0.
  • Create the read-only app-read policy.
  • Bind the myapp role to the future workload identity.
Enable Kubernetes authentication

The Kubernetes auth method validates a pod through its ServiceAccount token. OpenBao must also know how to reach the Kubernetes API that validates that token.

  • Enter the OpenBao server pod by running this command:
kubectl exec -n openbao -it openbao-0 -- /bin/sh

What does this command do?

The command starts an interactive shell inside openbao-0. Commands entered there can use the pod's in-cluster Kubernetes connection values.

  • Configure Kubernetes authentication from inside the pod by running these commands:
bao auth enable kubernetes
bao write auth/kubernetes/config \
  kubernetes_host=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT

How does the configuration work?

  • The first command enables the Kubernetes auth method at its default path.
  • The second command points OpenBao at the Kubernetes API using values already available inside the pod.
  • OpenBao uses its local ServiceAccount token plus the mounted cluster certificate when the reviewer token fields are omitted.
  • Leave the OpenBao pod shell by pressing Ctrl+D.

Both commands return successful write responses. OpenBao can now validate Kubernetes identities against your local cluster.

Kubernetes authentication command failed?

Confirm the shell prompt belongs to openbao-0 before running the bao commands.

Check that the openbao-0 pod is still running in the openbao namespace.

Still stuck? Help me troubleshoot OpenBao Kubernetes auth configuration.

Create the read-only policy

An OpenBao policy translates least privilege into an exact path plus an allowed capability. This policy grants read access to one KV v2 data path.

  • Start writing app-policy.hcl inside ~/openbao-k8s-lab by running this command:
cat > app-policy.hcl

What happens after this command?

The terminal waits for the policy text. It writes everything you enter into app-policy.hcl when you finish the input.

  • Add the read-only policy by pasting this code:
path "secret/data/my-secret" {
  capabilities = ["read"]
}

What does this policy allow?

  • The path targets the KV v2 data endpoint for my-secret.
  • The read capability allows retrieval from that path.
  • The policy grants no write capability to the workload.
  • Save the policy input by pressing Ctrl+D.
  • Display the saved file by running this command:
cat app-policy.hcl

What should the file contain?

You should see the secret/data/my-secret path with only the read capability. This confirms that the local file is ready to upload.

  • Write the local policy into OpenBao by running this command:
kubectl exec -i -n openbao openbao-0 -- bao policy write app-read - < app-policy.hcl

Why stream the policy through standard input?

The -i flag passes the contents of app-policy.hcl into the pod. The final - tells bao policy write to read that policy from standard input.

OpenBao stores the policy under the name app-read. That name becomes the access boundary attached to the workload role.

Policy upload failed?

Confirm that app-policy.hcl appears when you display the current folder's files.

Compare the path plus braces against the reference below. A missing quote or brace prevents OpenBao from parsing the policy.

Need a hand? Help me diagnose my OpenBao policy upload.

✔️ Awesome, I've got everything!

Your app-policy.hcl file now matches the scoped read policy.

ⓧ I'd like to double check the full code

path "secret/data/my-secret" {
  capabilities = ["read"]
}
Bind the scoped role

The policy controls what a token can do. The role controls which Kubernetes identity can receive a token carrying that policy.

  • Create the myapp role by running this command:
kubectl exec -n openbao openbao-0 -- bao write auth/kubernetes/role/myapp \
  bound_service_account_names=myapp \
  bound_service_account_namespaces=default \
  token_policies=app-read \
  ttl=1h

How is the role scoped?

  • The ServiceAccount name must be myapp.
  • The ServiceAccount must belong to the default namespace.
  • Tokens issued through the role receive the app-read policy.
  • Each issued token has a time to live of 1h.

The role can exist before Kubernetes creates the matching ServiceAccount. The next workload must present the exact identity named by these bindings.

Before you run the check, predict the complete scoped role configuration.

  • Read the saved role by running this command:
kubectl exec -n openbao openbao-0 -- bao read auth/kubernetes/role/myapp

What should you see?

The output lists myapp under bound_service_account_names. It lists default under bound_service_account_namespaces.

The token policy contains app-read. The token time to live reflects 1h.

That is the security boundary locked in. OpenBao now trusts one future workload identity with one read-only secret path.

Role output missing a binding?

Check each field name in the role write command. OpenBao only saves the bindings that the command sends successfully.

Confirm that the role name is myapp in both the write command and the read command.

Need help comparing the output? Help me troubleshoot the scoped OpenBao role.

Your scoped identity is ready. Next, the application pod can use that identity to receive its secret file.

Inject and Read the Secret

Your OpenBao role now connects the future myapp ServiceAccount to the read-only app-read policy. The baseline Pod still reports a missing file because it has no Kubernetes identity for that role.

The Agent Injector closes this gap when its mutating admission webhook sees an annotated workload. It adds an init container plus a sidecar that renders the requested KV v2 values into a shared in-memory file.

In this step, get ready to:
  • Replace the secret-blind baseline Pod with an annotated workload.
  • Start the application with injected Agent containers.
  • Read the secret file without creating a Kubernetes Secret object.
Replace the baseline workload

The baseline Pod has proved that the application cannot find /vault/secrets/my-secret by itself. Its replacement carries the identity plus annotations needed to request that file.

  • Delete the baseline Pod by running this command:
kubectl delete -f app-before.yaml

What does this command do?

The command uses app-before.yaml to identify the baseline Pod. It removes the running resource while keeping the local manifest available.

  • Confirm the command reports that myapp-before was deleted.

Baseline Pod still present?

  • Confirm your Terminal is still using ~/openbao-k8s-lab.
  • Confirm app-before.yaml still exists in that directory.

Still stuck? Help me remove the baseline Pod safely.

A YAML manifest can hold multiple resources separated by ---. The first resource creates the workload identity expected by the existing OpenBao role.

  • Create app-injected.yaml inside ~/openbao-k8s-lab using the file-editing method from earlier.
  • Add the ServiceAccount resource by copying this code into app-injected.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp
  namespace: default
---

What does this resource define?

  • The ServiceAccount gives the workload a dedicated identity named myapp.
  • The default namespace matches the binding in the existing OpenBao role.
  • The document separator prepares the file for the Pod resource.
  • Save app-injected.yaml.
  • Confirm the first resource ends with the document separator.

ServiceAccount section look uneven?

  • Keep name aligned with namespace beneath metadata.
  • Keep the document separator at the far left.

Need a second pair of eyes? Help me check the ServiceAccount YAML.

The Pod metadata selects the scoped role plus the secret path. Its specification assigns the new identity before the application starts reading the injected file.

  • Complete app-injected.yaml by copying this Pod resource below the document separator:
apiVersion: v1
kind: Pod
metadata:
  name: myapp
  namespace: default
  labels:
    app: myapp
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "myapp"
    vault.hashicorp.com/agent-inject-secret-my-secret: "secret/data/my-secret"
    vault.hashicorp.com/agent-inject-template-my-secret: |
      {{- with secret "secret/data/my-secret" -}}
      foo={{ .Data.data.foo }}
      bar={{ .Data.data.bar }}
      {{- end -}}
    vault.hashicorp.com/template-static-secret-render-interval: "30s"
spec:
  serviceAccountName: myapp
  containers:
    - name: app
      image: busybox:1.38.0
      command: ["sh", "-c"]
      args:
        - |
          while true; do
            echo "Injected secret:"
            cat /vault/secrets/my-secret
            sleep 10
          done

How does the injected workload work?

  • The injection annotations select the myapp role plus secret/data/my-secret.
  • The template renders foo plus bar into the file named my-secret.
  • The refresh annotation asks the Agent to render the static secret every 30s.
  • The serviceAccountName field assigns the identity expected by the OpenBao role.
  • The BusyBox application prints the injected file every ten seconds.
  • Save app-injected.yaml.
  • Confirm the file ends with sleep 10 followed by done.

Pod resource not aligned?

  • Keep spec aligned with metadata.
  • Keep every annotation key aligned beneath annotations.
  • Keep the shell loop indented beneath the block marker.

Still unsure? Help me check the injected Pod manifest.

✔️ Awesome, I've got everything!

Your app-injected.yaml file now contains the dedicated ServiceAccount plus the annotated Pod. Double-check that you saved it.

ⓧ I'd like to double check the full code

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp
  namespace: default
---
apiVersion: v1
kind: Pod
metadata:
  name: myapp
  namespace: default
  labels:
    app: myapp
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "myapp"
    vault.hashicorp.com/agent-inject-secret-my-secret: "secret/data/my-secret"
    vault.hashicorp.com/agent-inject-template-my-secret: |
      {{- with secret "secret/data/my-secret" -}}
      foo={{ .Data.data.foo }}
      bar={{ .Data.data.bar }}
      {{- end -}}
    vault.hashicorp.com/template-static-secret-render-interval: "30s"
spec:
  serviceAccountName: myapp
  containers:
    - name: app
      image: busybox:1.38.0
      command: ["sh", "-c"]
      args:
        - |
          while true; do
            echo "Injected secret:"
            cat /vault/secrets/my-secret
            sleep 10
          done
Apply the injected workload

Pod creation is when the webhook reads the annotations. The webhook mutates the Pod before its containers start.

  • Create the ServiceAccount plus annotated Pod by running this command:
kubectl apply -f app-injected.yaml

What does this command create?

The command submits both resources from app-injected.yaml to the cluster. The injector processes the Pod annotations during that request.

The command reports that both manifest resources were created.

Manifest rejected?

  • Compare app-injected.yaml with the full-code tab above.
  • Confirm the document separator remains between the two resources.
  • Check that each indentation level uses spaces consistently.

Need help with the failure? Help me diagnose why app-injected.yaml was rejected.

Before you check the Pod, how many containers do you expect the injector to add around the application?

  • Check the new Pod by running this command:
kubectl get pod myapp

What does this check show?

This check reports the Pod's readiness plus its current status. The injected design has the application beside the Agent sidecar.

  • Run the same check again until startup finishes.
  • Stop when the Pod reports 2/2 Running.

That is the identity handoff complete. Your application now runs beside the Agent sidecar.

Pod not reaching 2/2 Running?

  • Confirm the OpenBao server Pod still runs in the openbao namespace.
  • Confirm the role annotation contains myapp.
  • Confirm the Pod specification uses the myapp ServiceAccount.

Still waiting? Help me troubleshoot the injected Pod.

Verify the secret boundary

The application logs show whether the injected file was ready when the application started. A direct file check then confirms the exact values available inside the container.

  • Read the latest application log lines by running this command:
kubectl logs myapp -c app --tail=4

What do these logs prove?

The command selects the app container inside the multi-container Pod. It returns the latest four lines from the application loop.

You should see Injected secret: followed by foo=a plus bar=b.

Logs show a missing file?

  • Confirm the Pod reached 2/2 Running before reading its logs.
  • Confirm the template requests secret/data/my-secret.
  • Confirm the Pod uses the myapp ServiceAccount.

Need help tracing the file? Help me troubleshoot the injected secret file.

Before you inspect the file, do you expect its values to match version 1 in OpenBao?

  • Read the injected file directly from the application container by running this command:
kubectl exec myapp -c app -- cat /vault/secrets/my-secret

What does this command inspect?

The command runs cat inside the app container. It reads the same shared file that the application loop consumes.

You should see foo=a on one line. You should see bar=b on the next line.

You have closed the original missing-file gap. The workload now exchanges its own identity for narrowly scoped secret access.

The file lives in the injected shared memory volume. No Kubernetes Secret object needs to contain the value.

Before the final check, do you expect the default namespace to contain an object named my-secret?

  • List the Secret objects in the default namespace by running this command:
kubectl get secrets

What does this final check prove?

This command lists Kubernetes Secret objects in the current namespace. The injected file is independent of those objects.

You should not see a Secret object named my-secret. The values remain in OpenBao until the Agent renders them into the Pod's shared memory.

Secret boundary check failed?

  • Confirm the direct file check displays both expected lines.
  • Confirm the Secret list does not contain an object named my-secret.
  • Confirm the current namespace is default.

Need help comparing the checks? Help me verify the OpenBao secret boundary.

Strong finish. Your Pod reads the scoped OpenBao secret from an injected file without receiving a root token.

Secret mission

Rotate the Secret Live

Your pod already reads the first version of its injected secret. Now rotate that secret with Check-And-Set. You will watch the running Agent sidecar refresh the shared file without replacing the pod.

Clean Up Your Resources

Clean Up Your Resources

This lab runs locally inside Docker Desktop. It creates no cloud usage charge.

Choose a cleanup option below.

Cost warning

Docker Desktop is free for personal use, education, non-commercial open source, and qualifying small businesses with fewer than 250 employees and less than $10 million USD in annual revenue.

Other covered commercial use requires a paid subscription.

Resources you used:

  • Local Kubernetes cluster managed by Docker Desktop.
  • OpenBao Helm release openbao in namespace openbao, including the dev server, Agent Injector, services, webhook configuration, and KV v2 data.
  • ServiceAccount myapp plus injected Pod myapp in namespace default.
  • Local project files in ~/openbao-k8s-lab, including app-before.yaml, app-injected.yaml, app-policy.hcl, and openbao-values.yaml.

Keep everything running

No action needed. Choose this if you want to keep testing secret injection or version rotation.

  • Keep Docker Desktop running so the local cluster remains available.
  • Keep the openbao release running for further experiments.
  • Use fake values only in the dev-mode OpenBao server.
  • Keep ~/openbao-k8s-lab so you can reuse its YAML and HCL files.

Pause - I'll come back to this later

Stop the local cluster to free its local resources. Keep the project files so you can rebuild the disposable lab later.

  • In Docker Desktop, open the Kubernetes view.
  • Select Stop for the local cluster.
  • Keep ~/openbao-k8s-lab in place for your next session.

Your YAML and HCL files remain on disk. The OpenBao dev server stores its data in memory, so treat its secrets as disposable.

Delete - I don't want to use this again

Deleting this lab is final for its local data. The YAML and HCL files disappear when you remove the folder.

  • Copy any project files you want to keep out of ~/openbao-k8s-lab.
  • Remove the project's Kubernetes resources by running these commands in the Terminal from earlier:
kubectl delete -f app-injected.yaml
helm uninstall openbao --namespace openbao
kubectl delete namespace openbao

What do these commands remove?

  • The first command deletes Pod myapp plus ServiceAccount myapp from the default namespace.
  • The second command uninstalls Helm release openbao.
  • The final command deletes namespace openbao plus any remaining lab resources inside it.

Cleanup command failed?

A missing resource message usually means that resource was already removed. Check that your active context is docker-desktop if another cleanup command cannot find the cluster resources.

Help me diagnose a Kubernetes cleanup command that failed.

  • Remove the local project folder by running these commands:
cd ~
rm -rf ~/openbao-k8s-lab

What do these commands remove?

The first line moves Terminal out of the lab folder. The second line permanently removes the folder plus its YAML and HCL files.

  • In Docker Desktop, return to the Kubernetes view.
  • Select the docker-desktop cluster.
  • Use the cluster removal control only if no other local projects depend on it.

Your project workloads, OpenBao namespace, local files, and dedicated cluster are now removed. Docker Desktop remains installed for future projects.

Nice Work!

Nice Work!

Outstanding work! Your Kubernetes workload now exchanges its ServiceAccount identity for narrowly scoped OpenBao access. It reads a rotating secret from an in-memory file without copying the value into a cluster Secret object.

You've learned how to:

  • Make the missing-secret problem visible through a healthy pod that reports CONFIG_MISSING: /vault/secrets/my-secret.
  • Manage a versioned KV v2 secret at secret/data/my-secret through a read-only policy.
  • Authenticate the myapp pod through its least-privilege identity. Use the Agent Injector to render /vault/secrets/my-secret in memory without creating a cluster Secret object.
  • Secret Mission: Rotated the KV v2 secret with Check-And-Set while preserving version 1. The running Agent sidecar refreshed /vault/secrets/my-secret to version 2 without restarting the application container.

Ready to quiz yourself?