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.