Back to Distributed Systems & Kubernetes Series

Vault Track Part 3: Secret Injection

June 6, 2026 Wasil Zafar 38 min read

Two patterns for getting Vault secrets into Pods: (1) Vault Agent Injector — a mutating webhook that injects a Vault Agent sidecar, which mounts secrets as files on a shared volume. (2) Vault Secrets Operator — a Kubernetes operator that creates and manages Kubernetes Secrets from Vault, with automatic rotation.

Table of Contents

  1. Vault Agent Injector
  2. Vault Secrets Operator
  3. Injector vs VSO
  4. Exercises
  5. Key Takeaways & Next Steps

Vault Agent Injector

The Vault Agent Injector is a Kubernetes mutating admission webhook (installed as part of the Vault Helm chart with injector.enabled: true). When a Pod with special annotations is created, the webhook injects two containers into the Pod spec: an init container (fetches secrets before the app starts) and optionally a sidecar container (keeps secrets refreshed throughout the Pod's lifetime).

Basic Annotations

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grade-api
  namespace: grade-api
spec:
  template:
    metadata:
      annotations:
        # Required: enable the injector
        vault.hashicorp.com/agent-inject: "true"

        # Required: Vault role to authenticate with
        vault.hashicorp.com/role: "grade-api"

        # Required: the secret path to fetch
        vault.hashicorp.com/agent-inject-secret-db: "secret/data/grade-api/db"

        # Optional: control the output file format
        # This creates /vault/secrets/db with the raw Vault JSON response
    spec:
      serviceAccountName: grade-api-sa
      containers:
        - name: grade-api
          image: ghcr.io/your-org/grade-api:latest
          # After injection, secrets are available at:
          # /vault/secrets/db
          # /vault/secrets/

Secret Templates

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

    # Fetch the secret
    vault.hashicorp.com/agent-inject-secret-db: "secret/data/grade-api/db"

    # Template the output into a specific format
    # This creates /vault/secrets/db as a .env file
    vault.hashicorp.com/agent-inject-template-db: |
      {{- with secret "secret/data/grade-api/db" -}}
      export DB_HOST="{{ .Data.data.host }}"
      export DB_USERNAME="{{ .Data.data.username }}"
      export DB_PASSWORD="{{ .Data.data.password }}"
      {{- end }}

    # Multiple secrets — one annotation pair per secret
    vault.hashicorp.com/agent-inject-secret-redis: "secret/data/grade-api/redis"
    vault.hashicorp.com/agent-inject-template-redis: |
      {{- with secret "secret/data/grade-api/redis" -}}
      export REDIS_URL="{{ .Data.data.url }}"
      {{- end }}
    # Control when Vault Agent refreshes secrets
    # Refresh every 5 minutes for short-lived dynamic secrets
    vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/grade-api-role"
    vault.hashicorp.com/agent-inject-template-db-creds: |
      {{- with secret "database/creds/grade-api-role" -}}
      DB_USERNAME="{{ .Data.username }}"
      DB_PASSWORD="{{ .Data.password }}"
      DB_LEASE_DURATION="{{ .LeaseDuration }}"
      {{- end }}

    # POSIX signal to send to app when secrets are renewed
    vault.hashicorp.com/agent-inject-command-db-creds: "kill -HUP $(pidof grade-api)"

Init Container Mode

    # If you only need secrets at startup and don't need renewal:
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/role: "grade-api"
    vault.hashicorp.com/agent-pre-populate-only: "true"   # No sidecar, only init
    vault.hashicorp.com/agent-inject-secret-db: "secret/data/grade-api/db"

    # The init container exits after fetching secrets.
    # No sidecar running alongside your app.
    # Secrets are NOT refreshed automatically — use this for static secrets only.

Vault Secrets Operator

The Vault Secrets Operator (VSO) is an alternative to the Injector. Instead of injecting sidecars, VSO manages Kubernetes Secrets — it reads from Vault and creates/updates Kubernetes Secrets. Your Pods consume standard Kubernetes Secrets (env vars or volume mounts), with no Vault-specific annotations or sidecars.

Installing VSO

helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
  --namespace vault-secrets-operator-system \
  --create-namespace \
  --set defaultVaultConnection.enabled=true \
  --set defaultVaultConnection.address=http://vault.vault.svc.cluster.local:8200

VaultAuth CRD

# VaultAuth — defines how to authenticate to Vault for this namespace
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  name: grade-api-auth
  namespace: grade-api
spec:
  method: kubernetes
  mount: kubernetes
  kubernetes:
    role: grade-api
    serviceAccount: grade-api-sa
    audiences:
      - vault

VaultStaticSecret CRD

# VaultStaticSecret — creates/updates a Kubernetes Secret from a Vault path
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: grade-api-db-secret
  namespace: grade-api
spec:
  vaultAuthRef: grade-api-auth    # Reference the VaultAuth above

  type: kv-v2
  mount: secret                   # KV secrets engine mount path
  path: grade-api/db              # Path within the mount

  destination:
    name: grade-api-db-secret     # Name of the Kubernetes Secret to create
    create: true                  # Create if it doesn't exist
    transformation:
      excludeRaw: true            # Only include the KV data, not Vault metadata

  refreshAfter: 30s               # How often to sync from Vault

  rolloutRestartTargets:          # Restart Deployments when secret updates
    - kind: Deployment
      name: grade-api
# The resulting Kubernetes Secret looks like:
# apiVersion: v1
# kind: Secret
# metadata:
#   name: grade-api-db-secret
#   namespace: grade-api
# data:
#   host: cG9zdGdyZXMuZ3JhZGUtYXBpLnN2Yy5jbHVzdGVyLmxvY2Fs
#   username: Z3JhZGVfYXBpX3VzZXI=
#   password: c3VwZXItc2VjcmV0LWRiLXBhc3N3b3Jk

# Consume it normally in the Deployment:
spec:
  template:
    spec:
      containers:
        - name: grade-api
          envFrom:
            - secretRef:
                name: grade-api-db-secret

Injector vs VSO

Decision Guide

Use Vault Agent Injector when:

  • You need dynamic secrets (database credentials, AWS tokens) with in-Pod renewal
  • You want secrets as files (not Kubernetes Secrets) to minimize secret surface area
  • You need POSIX-signal-based secret refresh without Pod restart
  • Your app reads config from files naturally (12-factor apps)

Use Vault Secrets Operator when:

  • You have legacy apps that read from Kubernetes Secrets (env vars)
  • You want a standard Kubernetes interface — GitOps-friendly, no sidecar overhead
  • You need automatic Deployment rollouts when secrets change
  • You're using tools that already know how to consume Kubernetes Secrets (Helm values, etc.)

Exercises

Exercise 1 — Agent Injector: Write a Vault secret at secret/grade-api/config. Deploy a Pod with injector annotations referencing that path. Exec into the Pod and verify /vault/secrets/config contains the rendered values.
Exercise 2 — Template Rendering: Add a custom template annotation that renders the secret as a .properties file (key=value format). Verify the format in the mounted file.
Exercise 3 — VSO: Install the Vault Secrets Operator. Create a VaultAuth and VaultStaticSecret. Verify a Kubernetes Secret is created. Update the Vault secret — verify the K8s Secret is updated within the refresh interval.

Key Takeaways & Next Steps

Key Takeaways:
  • Vault Agent Injector uses annotations on the Pod spec — the mutating webhook injects the sidecar automatically
  • Templating lets you render secrets into any format your app expects (env files, JSON, TOML)
  • Vault Secrets Operator is the modern approach — it creates Kubernetes Secrets that any app can consume
  • VSO's rolloutRestartTargets automatically restarts Deployments when Vault secrets change
  • Both approaches use the Kubernetes auth method from Part 2 — same service account, same role

Next in This Track

In Part 4: Dynamic Secrets & PKI, we use Vault's database engine to generate on-demand PostgreSQL credentials with automatic expiry, and use the PKI engine to issue X.509 certificates for mTLS between services.