HashiCorp Vault Agent Sidecar on Kubernetes
Every team that adopts Vault eventually hits the same wall: now every application needs to know how to talk to Vault. That means pulling in a Vault client library, writing token renewal logic, handling lease expiration, and dealing with auth failures gracefully. For a single service, it's manageable. For 30 services across a platform, it's a maintenance burden that creeps into every codebase.
Vault Agent solves this. It's a daemon that runs alongside your application, handles authentication, fetches secrets, and writes them to a shared location your app can read. Your app reads a file. That's it.
Updated March 2026: The Vault Secrets Operator (VSO), released in 2023, is now the recommended approach for Kubernetes-native workloads. It uses Kubernetes CRDs (
VaultStaticSecret,VaultDynamicSecret) to sync Vault secrets into Kubernetes Secrets, and integrates better with GitOps workflows. Vault Agent Injector is still widely used and fully supported, but VSO is worth evaluating for new deployments.
The Problem with Direct Vault Access
When an app fetches secrets directly, it needs:
- A Vault token or credentials for an auth method
- Logic to renew that token before it expires
- Error handling for Vault being temporarily unavailable
- Knowledge of which secret paths to read
This couples your application code to your secrets infrastructure. It also means every developer needs to understand Vault's auth model to write a new service. That's not a reasonable expectation.
Vault Agent Basics
Vault Agent runs as a process that authenticates to Vault using a supported auth method (Kubernetes, AWS IAM, AppRole, etc.) and then either writes secrets to files or acts as a caching proxy. The critical piece for Kubernetes is the Kubernetes auth method — it lets a pod authenticate using its service account token, which Kubernetes issues automatically.
A basic Vault Agent config looks like this:
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "my-app-role"
}
}
sink "file" {
config = {
path = "/vault/token"
}
}
}
template {
source = "/vault/templates/config.tpl"
destination = "/vault/secrets/config"
}
The auto_auth block handles token acquisition and renewal. The template block uses consul-template syntax to render secrets into files. Your application reads /vault/secrets/config. It never talks to Vault directly.
The Vault Agent Injector
Running Vault Agent manually in a sidecar container is possible but tedious — you'd need to modify every pod spec. The Vault Agent Injector automates this through a Kubernetes mutating admission webhook. When a pod is created, the injector intercepts the request and patches in the Vault Agent containers automatically, based on annotations on the pod.
Install the injector via Helm:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
--set "injector.enabled=true" \
--set "server.dev.enabled=false"
Annotation-Based Injection
Once the injector is running, you enable injection per-deployment with annotations:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "my-app-role"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/my-app/config"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/my-app/config" -}}
export DB_PASSWORD="{{ .Data.data.db_password }}"
export API_KEY="{{ .Data.data.api_key }}"
{{- end }}
spec:
serviceAccountName: my-app
containers:
- name: my-app
image: my-app:latest
The annotation vault.hashicorp.com/agent-inject-secret-config tells the injector which secret to fetch. The template annotation (agent-inject-template-<name>) controls how it's rendered. The injector writes the result to /vault/secrets/config inside the pod.
Init Container vs Sidecar
By default, the injector adds both an init container and a sidecar container. This is intentional:
- The init container (
vault-agent-init) runs first and blocks the app container from starting until secrets are written. This ensures your app never starts without its secrets. - The sidecar (
vault-agent) runs for the lifetime of the pod, handling token renewal and template re-rendering when secrets rotate.
If you only need static secrets and don't need dynamic renewal, you can disable the sidecar:
vault.hashicorp.com/agent-pre-populate-only: "true"
This runs only the init container. Simpler, but you won't get secret updates without a pod restart.
Kubernetes Auth Method Setup
On the Vault side, you need to configure the Kubernetes auth method to trust your cluster:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$(kubectl get svc kubernetes -o jsonpath='{.spec.clusterIP}'):443" \
kubernetes_ca_cert=@/path/to/ca.crt \
token_reviewer_jwt=@/path/to/reviewer-token
vault write auth/kubernetes/role/my-app-role \
bound_service_account_names=my-app \
bound_service_account_namespaces=default \
policies=my-app-policy \
ttl=1h
The role binds a Vault policy to a Kubernetes service account. Only pods running as my-app in the default namespace can use this role. The TTL controls how long the issued token is valid before renewal.
Why This Approach Works
The thing I like about Vault Agent Injector is that it's entirely transparent to the application. The app reads files. It doesn't care where they came from. You can swap out Vault, change secret paths, or rotate credentials without touching application code.
The tradeoff is operational complexity — you're running an additional process per pod, and the injector itself is a critical piece of infrastructure. If the injector webhook is unavailable, pod creation can be blocked. Make sure to configure the webhook's failurePolicy appropriately (Ignore for non-critical workloads, Fail if you want hard guarantees).
For a platform team, this is the right abstraction: developers annotate their deployments, and secrets appear. The Vault complexity lives in the platform layer, not in every service.