This is part of the Vault + Kubernetes Integration Guide. Return to the main guide for the full architecture overview.
The Vault Agent Injector uses a Kubernetes mutating admission webhook to automatically inject a Vault Agent sidecar into your pods. The sidecar handles authentication, secret retrieval, templating, and token renewal — your application just reads files from a shared volume.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─ Pod ──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┐ shared ┌──────────────────────┐ │ │
│ │ │ App │ tmpfs │ Vault Agent │ │ │
│ │ │ Container │◄─────────►│ Sidecar │ │ │
│ │ │ │ volume │ │ │ │
│ │ │ Reads from │ │ - Auto-auth │ │ │
│ │ │ /vault/ │ │ - Secret fetching │ │ │
│ │ │ secrets/ │ │ - Template rendering│ │ │
│ │ └──────────────┘ │ - Token renewal │ │ │
│ │ └──────────┬───────────┘ │ │
│ └─────────────────────────────────────────┼─────────────┘ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ Injector │ Webhook │ Auth + Read │
│ │ Controller │ intercepts │ │
│ │ (Deployment) │ pod creation ▼ │
│ └──────────────────┘ ┌──────────────┐ │
│ │ Vault Server │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
Installation
The injector is installed alongside Vault via the Helm chart:
helm install vault hashicorp/vault \
--namespace vault \
--create-namespace \
--set injector.enabled=true \
--set injector.replicas=2 \
--set "injector.externalVaultAddr=https://vault.corp.com:8200"
# Use externalVaultAddr if Vault is outside the cluster
For injector-only installations (external Vault, no in-cluster Vault server):
helm install vault hashicorp/vault \
--namespace vault \
--create-namespace \
--set server.enabled=false \
--set injector.enabled=true \
--set "injector.externalVaultAddr=https://vault.corp.com:8200"
Basic Usage: Annotation Reference
All injection behavior is controlled via pod annotations:
metadata:
annotations:
# Required: Enable injection
vault.hashicorp.com/agent-inject: "true"
# Required: Vault role to authenticate with
vault.hashicorp.com/role: "my-app-role"
# Inject a secret at /vault/secrets/<name>
vault.hashicorp.com/agent-inject-secret-<name>: "path/to/secret"
# Optional: Custom template for the secret
vault.hashicorp.com/agent-inject-template-<name>: |
{{ with secret "path/to/secret" }}
{{ .Data.data.key }}
{{ end }}
Complete Annotation Reference
| Annotation | Default | Description |
|---|---|---|
agent-inject |
false |
Enable injection |
role |
— | Vault auth role name |
agent-inject-secret-NAME |
— | Secret path → renders to /vault/secrets/NAME |
agent-inject-template-NAME |
— | Go template for secret rendering |
agent-inject-status |
— | Set to update to re-render on restart |
agent-pre-populate-only |
false |
Use init container only (no sidecar) |
agent-init-first |
false |
Run init container before app starts |
agent-inject-token |
false |
Also write raw Vault token to disk |
agent-image |
Latest | Override agent container image |
agent-requests-cpu |
250m |
Agent CPU request |
agent-limits-mem |
128Mi |
Agent memory limit |
secret-volume-path-NAME |
/vault/secrets |
Custom mount path for a specific secret |
preserve-secret-case |
false |
Don’t lowercase secret keys |
Practical Examples
Example 1: Environment File for Your App
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "api-server"
vault.hashicorp.com/agent-inject-secret-env: "secret/data/production/api-server"
vault.hashicorp.com/agent-inject-template-env: |
{{ with secret "secret/data/production/api-server" -}}
DATABASE_URL=postgresql://{{ .Data.data.db_user }}:{{ .Data.data.db_pass }}@{{ .Data.data.db_host }}:5432/appdb
REDIS_URL=redis://:{{ .Data.data.redis_pass }}@redis.production.svc:6379/0
JWT_SECRET={{ .Data.data.jwt_secret }}
{{- end }}
spec:
serviceAccountName: api-server-sa
containers:
- name: api-server
image: api-server:v2.1
command: ["/bin/sh", "-c"]
args: ["source /vault/secrets/env && exec ./api-server"]
Example 2: JSON Config File
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "api-server"
vault.hashicorp.com/agent-inject-secret-config.json: "secret/data/production/api-config"
vault.hashicorp.com/agent-inject-template-config.json: |
{{ with secret "secret/data/production/api-config" -}}
{
"database": {
"host": "{{ .Data.data.db_host }}",
"port": {{ .Data.data.db_port }},
"username": "{{ .Data.data.db_user }}",
"password": "{{ .Data.data.db_pass }}"
},
"cache": {
"redis_url": "{{ .Data.data.redis_url }}"
}
}
{{- end }}
Example 3: Dynamic Database Credentials
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "api-server"
vault.hashicorp.com/agent-inject-secret-dbcreds: "database/creds/api-readonly"
vault.hashicorp.com/agent-inject-template-dbcreds: |
{{ with secret "database/creds/api-readonly" -}}
DB_USER={{ .Data.username }}
DB_PASS={{ .Data.password }}
{{- end }}
The Agent automatically renews the database lease and re-renders the template when new credentials are generated.
Example 4: Multiple Secrets from Different Paths
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "api-server"
# Secret 1: App config
vault.hashicorp.com/agent-inject-secret-app: "secret/data/prod/app-config"
# Secret 2: TLS certificate
vault.hashicorp.com/agent-inject-secret-tls.crt: "pki/issue/api-cert"
vault.hashicorp.com/secret-volume-path-tls.crt: "/etc/tls"
vault.hashicorp.com/agent-inject-template-tls.crt: |
{{ with secret "pki/issue/api-cert" "common_name=api.corp.com" -}}
{{ .Data.certificate }}
{{ .Data.issuing_ca }}
{{- end }}
# Secret 3: TLS key
vault.hashicorp.com/agent-inject-secret-tls.key: "pki/issue/api-cert"
vault.hashicorp.com/secret-volume-path-tls.key: "/etc/tls"
vault.hashicorp.com/agent-inject-template-tls.key: |
{{ with secret "pki/issue/api-cert" "common_name=api.corp.com" -}}
{{ .Data.private_key }}
{{- end }}
Example 5: Init Container Only (No Sidecar)
For jobs or pods that only need secrets at startup:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/role: "batch-job"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/batch/config"
This uses an init container to fetch secrets before the app starts, then exits — no running sidecar consuming resources.
Resource Tuning
The sidecar runs in every pod, so resource requests matter at scale:
annotations:
vault.hashicorp.com/agent-requests-cpu: "50m"
vault.hashicorp.com/agent-limits-cpu: "100m"
vault.hashicorp.com/agent-requests-mem: "32Mi"
vault.hashicorp.com/agent-limits-mem: "64Mi"
Rule of thumb: If you have 100 pods with agent sidecars at default settings, that’s an extra 25 CPU cores requested and 12.8 GB memory. Tune aggressively.
Troubleshooting
Sidecar not being injected
# Check the webhook exists
kubectl get mutatingwebhookconfigurations | grep vault
# Check injector logs
kubectl logs -n vault -l app.kubernetes.io/name=vault-agent-injector
# Verify the namespace isn't excluded
kubectl get namespace <ns> -o yaml | grep -i vault
Secrets file is empty
# Check the agent sidecar logs inside the pod
kubectl logs <pod> -c vault-agent
# Common cause: wrong secret path or missing policy
Pod stuck in Init
# Agent can't authenticate — check connectivity to Vault
kubectl logs <pod> -c vault-agent-init
Next: Compare with the Vault CSI Provider or the modern Vault Secrets Operator.