Learn Vault - Integrating Vault with Kubernetes (K8s Auth & Agent Injector)
Episode 17 of 26

Learn Vault - Integrating Vault with Kubernetes (K8s Auth & Agent Injector)

Authenticate Kubernetes Pods to Vault via the Kubernetes Auth Method, then automate secret provisioning with the Vault Agent Sidecar Injector and the Vault Secrets Operator (VSO) that syncs secrets into native Kubernetes Secrets.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After covering direct application integration into Vault via the SDK in episode 16 — complete with retry, renewal, and fallback patterns — this episode covers the most widely used integration in the cloud-native world: Vault with Kubernetes.

Why is this topic important? Almost every company using Vault in production runs its applications on Kubernetes. The problem: the traditional way of giving secrets to Pods — writing secret values directly into a Kubernetes Secret — still stores secrets statically in etcd, is easily readable by anyone with access, and is never authenticated to Vault. In this episode we'll solve that problem with three main mechanisms: Kubernetes Auth Method for authentication, Vault Agent Sidecar Injector for automatic secret injection into Pods, and Vault Secrets Operator (VSO) for synchronizing secrets into native Kubernetes Secrets. Let's dissect them one by one.

Main Discussion

Kubernetes Auth Method (auth/kubernetes)

The Kubernetes Auth Method is how Pods authenticate to Vault without needing to store a token or secret ID. The concept: every Pod in Kubernetes automatically has a ServiceAccount, and every ServiceAccount has an attached JWT token. This JWT becomes the Pod's "identity proof" to Vault.

The authentication flow looks like this:

Kubernetes Auth Method flow
Pod (SA: web-sa)
   │  1. send ServiceAccount JWT to Vault: auth/kubernetes/login

Vault  ──2. verify JWT──▶ Kubernetes API (TokenReview)
   ▲                          │ 3. valid / invalid + SA & namespace claims
   └──4. issue Vault token ───┘

Vault doesn't guess whether the JWT is authentic — it asks the Kubernetes API Server to verify the token via the TokenReview endpoint. That way, Vault fully trusts that the token really belongs to a specific ServiceAccount in a specific namespace.

Enabling & Configuring K8s Auth

The first step is enabling the auth method and connecting it to the cluster. Vault needs three things: the Kubernetes API address, the cluster's CA certificate, and the JWT of a service account authorized to perform TokenReview:

Enable & configure Kubernetes Auth
vault auth enable kubernetes
 
vault write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc" \
  token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
  kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt

Important

The config command above is run from inside the cluster (e.g. via kubectl exec or a bootstrap Job), so token_reviewer_jwt is taken from the active service account. In a more secure setup, token_reviewer_jwt is generated once then stored as a secret — or use tooling like the vault-kubernetes-auth helper run when the cluster is first built.

Creating a Role Bound to a ServiceAccount

After configuration, we create a role that binds a specific ServiceAccount to Vault policies. This is the crucial access control point: only Pods with an SA named web-sa in the default namespace may log in through the web role:

Create a role bound to SA & namespace
vault write auth/kubernetes/role/web \
  bound_service_account_names="web-sa" \
  bound_service_account_namespaces="default" \
  policies="web-app" \
  ttl="1h"

Also note the Vault policy attached to the role must allow access to the paths the app needs:

web-app-policy.hcl
path "secret/data/myapp" {
  capabilities = ["read"]
}

Warning

Always specify bound_service_account_names and bound_service_account_namespaces. A role without these bindings is like a door without a lock — every ServiceAccount in the cluster can log in using that role and inherit its policies. This is one of the most dangerous misconfigurations in production.

Vault Agent Sidecar Injector

The Kubernetes Auth Method lets Pods authenticate, but we still need a way to deliver secrets into the Pod. That's where the Vault Agent Sidecar Injector comes in: a mutating admission webhook that automatically adds a Vault Agent sidecar container to Pods based on annotations.

How it works:

  1. You write a normal Deployment with vault.hashicorp.com/... annotations.
  2. When the Pod is created, the injector webhook adds a vault-agent sidecar container and a /vault/secrets memory volume.
  3. The sidecar auto-auths via the Kubernetes Auth Method, reads secrets from Vault, and renders files to /vault/secrets/config.
  4. The application reads those files — without needing to know about Vault at all.

Preparing the Service Account & Installing the Injector

Before the deployment runs, there are two mandatory preparations. First, create the ServiceAccount web-sa — the identity that will prove itself to Vault:

Kubernetesservice-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: web-sa
  namespace: default

Second, install the Vault Agent Injector in the cluster. The most common way is via the Helm chart hashicorp/vault with the injector sub-chart (or installing the standalone injector chart):

KubernetesInstall the Vault Agent Injector via Helm
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
  --set injector.enabled=true \
  --set injector.logLevel=info
KubernetesVerify the injector webhook is active
kubectl get mutatingwebhookconfiguration
kubectl get pods -n vault

Tip

The injector only processes Pods that have the annotation vault.hashicorp.com/agent-inject: "true" — other Pods in the cluster are unaffected. This is a huge design advantage: you can enable Vault per-application gradually without changing the whole cluster.

Deployment with Injector Annotations

Kubernetesdeployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "web"
        vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp"
        vault.hashicorp.com/agent-inject-template-config: |
          {{- with secret "secret/data/myapp" }}
          DB_HOST={{ .Data.data.DB_HOST }}
          DB_PASSWORD={{ .Data.data.DB_PASSWORD }}
          {{- end }}
    spec:
      serviceAccountName: web-sa
      containers:
        - name: myapp
          image: myapp:1.0.0
          command: ["sh", "-c", "sleep infinity"]

Explanation of the key annotations:

  • vault.hashicorp.com/agent-inject: "true" — marker that the injector must process this Pod.
  • vault.hashicorp.com/role: "web" — the K8s auth role the sidecar uses to log in (must match bound_service_account_names).
  • vault.hashicorp.com/agent-inject-secret-config — the destination file name (config) separated from the secret path (secret/data/myapp) by a -. The result is the secret rendered to /vault/secrets/config.
  • agent-inject-template-config — the custom template that controls the file contents. Without this template, the injector uses the built-in template (YAML format) which is often not a good fit.

The Rendered Result Inside the Pod

After the Pod runs, let's look at what the sidecar produced:

KubernetesCheck the secret file inside the Pod
kubectl exec deploy/myapp -- cat /vault/secrets/config
Contents of the rendered /vault/secrets/config
DB_HOST=postgres.internal.local
DB_PASSWORD=a9f3!sVx#2kQ

Tip

The /vault/secrets volume is an emptyDir of type memory (medium: Memory) — secret files live in RAM, not on disk, so secrets are never written to node persistent storage. This is one reason the sidecar injector is safer than a plain static Secret.

What Happens When the Secret in Vault Is Rotated?

This is the injector's main advantage: if the secret in Vault changes, the sidecar agent detects the change (via polling Vault) and re-renders the file automatically. An app that reads the file every time it needs a connection will automatically use the latest value. But note — an app that already holds old values in memory (e.g. a connection pool built at startup) won't be affected until it re-reads the file or is restarted. For apps that need to respond to rotation quickly, add a reloader like automatic restart when the file changes.

Vault Secrets Operator (VSO)

The sidecar injector solves the injection problem, but there's another case: applications (or tooling like Helm charts, ingress, etc.) actually need a native Kubernetes Secret — not a file inside the Pod. That's where the Vault Secrets Operator (VSO) comes in: this operator continuously syncs secrets from Vault into native Kubernetes Secrets, complete with automatic rotation.

Its architecture consists of several CRDs (Custom Resource Definitions):

  • VaultConnection — the Vault address to connect to.
  • VaultAuth — authentication credentials (e.g. via the Kubernetes Auth Method).
  • VaultStaticSecret — the secret source and sync destination (destination).
  • (There's also VaultDynamicSecret for dynamic credentials, and VaultPKISecret for certificates — let's focus on static first.)
Kubernetesvso.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
  name: vault
  namespace: default
spec:
  address: http://vault:8200
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  name: vault-auth
  namespace: default
spec:
  method: kubernetes
  mount: kubernetes
  kubernetes:
    role: web
    serviceAccount: web-sa
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: myapp-secret
  namespace: default
spec:
  vaultAuthRef: vault-auth
  mount: secret
  type: kv-v2
  path: myapp
  refreshAfter: 1h
  destination:
    name: myapp-secrets
    create: true
    overwrite: true

Explanation of the important parts:

  • vaultAuthRef points to the VaultAuth that uses Kubernetes Auth with the role web and service account web-sa.
  • mount: secret + type: kv-v2 + path: myapp means the source is secret/data/myapp on the KV v2 secrets engine.
  • refreshAfter: 1h determines the sync interval — every hour the operator compares the secret version in Vault and updates the Kubernetes Secret if there are changes.
  • destination defines the Secret named myapp-secrets that will be created/updated.

VSO also supports dynamic credentials via the VaultDynamicSecret CRD. For example, fetching temporary database credentials from the database secrets engine and syncing them to a Secret:

Kubernetesvso-dynamic.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
  name: db-creds
  namespace: default
spec:
  vaultAuthRef: vault-auth
  mount: database
  role: web-role
  destination:
    name: db-creds
    create: true
  revoke: true
  renewalPercent: 60

Important

On VaultDynamicSecret, the revoke: true attribute ensures dynamic credentials are revoked when the Kubernetes Secret is deleted or the CRD is removed — keeping the short-lived credentials principle. Meanwhile renewalPercent: 60 makes the operator renew the lease once it reaches 60% of its TTL, so the app never uses near-expired credentials.

The result: a native Kubernetes Secret ready for the app to use:

KubernetesNative secret from sync
kubectl get secret myapp-secrets -o jsonpath='{.data}'
Secret values (base64)
{
  "DB_HOST": "cG9zdGdyZXMuaW50ZXJuYWwubG9jYWw=",
  "DB_PASSWORD": "YTlmMyFzVngjMmtR"
}

Note

VSO handles rotation intelligently: when the secret in Vault changes (a new KV v2 version), the operator automatically updates the Kubernetes Secret and — if enabled via refreshAfter/events — triggers a deployment rollout consuming that secret. This eliminates the manual "update secret, restart pod" work.

Sidecar Injector vs VSO: When to Use Which?

AspectAgent Injector (Sidecar)Vault Secrets Operator (VSO)
Deliverable formFile at /vault/secrets/... inside the PodNative Kubernetes Secret
Secret stored in etcdNo (RAM emptyDir)Yes (as a regular Secret)
App accessRead a local fileStandard K8s secret mount / env
Ideal forApps that need secrets as files/configHelm charts, ingress TLS, tooling that needs a Secret
Vault API involvementHandled by the sidecarHandled by the operator (transparent)
RotationFile re-rendered; app needs a reloadSecret updated; can trigger rollout

Warning

Because VSO stores secrets as a regular Kubernetes Secret, all values ultimately reside in etcd (in base64, not strongly encrypted). If your regulations require secrets to never sit in storage, use the Sidecar Injector. VSO is better suited for tooling that genuinely needs native Secret objects.

Common Vault-Kubernetes Integration Mistakes

MistakeSymptomSolution
Role without bound_service_account_*All SAs can log in — security holeAlways specify SA name + namespace
Annotation template wrongly indented (block scalar ``)Rendered file invalid / empty
Vault policy doesn't allow the secret pathSidecar error permission deniedMake sure the web role policy covers secret/data/myapp
SA not created or wrong namespacePod login fails: service account not foundVerify serviceAccountName & create the SA via RBAC
token_reviewer_jwt from an SA without TokenReview permissionLogin always rejected by VaultMake sure the bootstrap SA has the ClusterRole system:auth-delegator
Forgetting serviceAccountName in the pod specInjector uses the default SA — bound failsExplicitly set serviceAccountName
Wrong KV v2 path (secret/myapp vs secret/data/myapp)Secret not foundFor kv-v2 always use secret/data/...
Assuming VSO also provisions dynamic credentialsOnly VaultStaticSecret gets syncedUse VaultDynamicSecret for dynamic creds
Native secret from VSO mounted as envValue not updated until pod restartUse a volume mount + reloader if you need live rotation

Caution

The correct troubleshooting order when a sidecar fails: (1) check kubectl logs <pod> vault-agent for login/policy errors, (2) verify the SA↔namespace↔policy role binding, (3) test path access with vault read secret/data/myapp using a token from that role. Don't immediately change configuration without knowing which layer the problem is in.

Conclusion

In this episode 17 we've covered the Vault-Kubernetes integration thoroughly: Kubernetes Auth Method which verifies the ServiceAccount JWT through the TokenReview API, Vault Agent Sidecar Injector which automatically injects a sidecar to render secrets to /vault/secrets/config in RAM, and Vault Secrets Operator (VSO) which syncs secrets into native Kubernetes Secrets with automatic rotation.

The core lesson of this episode: a Pod's identity in Kubernetes (ServiceAccount) is a strong authentication key to Vault — no more storing tokens anywhere. Choose the injector when you want secrets to end up in the Pod's memory, choose VSO when your tooling needs native Secret objects.

In episode 18 we move into the world of development automation: Vault integration in CI/CD pipelines (GitHub Actions & GitLab CI) — how pipelines fetch temporary credentials from Vault without storing static secrets in the repository. Keep your enthusiasm up!

Learn Vault - Integrating Vault with Kubernetes (K8s Auth & Agent Injector) | Learn Secret Management with HashiCorp Vault