HashiCorp Vault — Agent Injector

2 minute read

The Vault Agent Injector is a Kubernetes mutating webhook that automatically injects Vault Agent sidecars into pods based on annotations — no manual sidecar configuration needed.


How It Works

1. Pod created with vault annotations
2. Kubernetes sends admission request to Injector webhook
3. Injector mutates the pod spec:
   - Adds init container (fetches initial secrets)
   - Adds sidecar container (keeps secrets updated)
   - Adds shared volume (tmpfs)
4. Pod starts with secrets available at /vault/secrets/

Install Agent Injector

# Add HashiCorp Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

# Install Vault with injector enabled (injector is on by default)
helm install vault hashicorp/vault \
    --set "injector.enabled=true" \
    --set "server.enabled=false" \
    --set "injector.externalVaultAddr=http://external-vault:8200"

If Vault is already running in the cluster:

helm install vault hashicorp/vault \
    --set "injector.enabled=true"

Basic Annotation Usage

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
      annotations:
        # Enable injection
        vault.hashicorp.com/agent-inject: "true"
        # Vault role for authentication
        vault.hashicorp.com/role: "myapp"
        # Secret to inject
        vault.hashicorp.com/agent-inject-secret-db-creds: "secret/data/myapp/db"
    spec:
      serviceAccountName: myapp
      containers:
        - name: myapp
          image: myapp:latest

This injects the raw JSON secret at /vault/secrets/db-creds.


Custom Templates via Annotations

metadata:
  annotations:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "myapp"

    # Secret file 1: database credentials as env file
    vault.hashicorp.com/agent-inject-secret-db.env: "secret/data/myapp/db"
    vault.hashicorp.com/agent-inject-template-db.env: |
      {{ with secret "secret/data/myapp/db" -}}
      DB_HOST={{ .Data.data.host }}
      DB_PORT={{ .Data.data.port }}
      DB_USER={{ .Data.data.username }}
      DB_PASS={{ .Data.data.password }}
      {{- end }}

    # Secret file 2: API key
    vault.hashicorp.com/agent-inject-secret-api-key: "secret/data/myapp/api"
    vault.hashicorp.com/agent-inject-template-api-key: |
      {{ with secret "secret/data/myapp/api" -}}
      {{ .Data.data.key }}
      {{- end }}

Annotation Reference

Annotation Description
vault.hashicorp.com/agent-inject Enable injection ("true")
vault.hashicorp.com/role Vault auth role
vault.hashicorp.com/agent-inject-secret-NAME Secret path to inject as NAME
vault.hashicorp.com/agent-inject-template-NAME Custom template for NAME
vault.hashicorp.com/agent-inject-status Set to "update" to re-inject
vault.hashicorp.com/agent-pre-populate Pre-populate secrets before app starts
vault.hashicorp.com/agent-pre-populate-only Only inject once (no sidecar)
vault.hashicorp.com/agent-inject-token Inject the Vault token file
vault.hashicorp.com/agent-run-as-user UID for the agent containers
vault.hashicorp.com/agent-limits-cpu Agent CPU limit
vault.hashicorp.com/agent-limits-mem Agent memory limit
vault.hashicorp.com/agent-requests-cpu Agent CPU request
vault.hashicorp.com/agent-requests-mem Agent memory request
vault.hashicorp.com/tls-skip-verify Skip TLS verification

Dynamic Secrets with Injector

annotations:
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/role: "myapp"

  # Dynamic PostgreSQL credentials
  vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/myapp-role"
  vault.hashicorp.com/agent-inject-template-db-creds: |
    {{ with secret "database/creds/myapp-role" -}}
    postgresql://{{ .Data.username }}:{{ .Data.password }}@db.internal:5432/mydb
    {{- end }}

The agent automatically renews the dynamic credential lease.


One-Time Injection (No Sidecar)

For secrets that don’t change:

annotations:
  vault.hashicorp.com/agent-inject: "true"
  vault.hashicorp.com/role: "myapp"
  vault.hashicorp.com/agent-pre-populate-only: "true"
  vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"

This runs only the init container — no sidecar stays running. Uses fewer resources but won’t auto-rotate secrets.


Vault Setup for Injector

# 1. Enable Kubernetes auth
vault auth enable kubernetes

# 2. Configure it
vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc"

# 3. Create a policy
vault policy write myapp - <<EOF
path "secret/data/myapp/*" {
  capabilities = ["read"]
}
path "database/creds/myapp-role" {
  capabilities = ["read"]
}
EOF

# 4. Create a role bound to the service account
vault write auth/kubernetes/role/myapp \
    bound_service_account_names=myapp \
    bound_service_account_namespaces=default \
    policies=myapp \
    ttl=1h

Quick Reference

Task Annotation
Enable injection vault.hashicorp.com/agent-inject: "true"
Set auth role vault.hashicorp.com/role: "myapp"
Inject a secret vault.hashicorp.com/agent-inject-secret-NAME: "PATH"
Custom template vault.hashicorp.com/agent-inject-template-NAME: "..."
One-shot (no sidecar) vault.hashicorp.com/agent-pre-populate-only: "true"

Next: Practice Exercises