HashiCorp Vault — Sidecar Pattern

2 minute read

The sidecar pattern deploys Vault Agent alongside your application container, handling authentication and secret delivery through shared volumes.


Why Sidecar?

Approach Pros Cons
App-native (SDK) Fine-grained control Code changes, vendor lock-in
Init container Simple, one-time fetch No renewal or rotation
Sidecar (Agent) Auto-renewal, no code changes Extra container resource usage

Architecture

┌─────────────────────────────────────────┐
│                   Pod                    │
│                                          │
│  ┌──────────────┐   ┌───────────────┐   │
│  │  Vault Agent  │   │  Application  │   │
│  │  (sidecar)    │   │               │   │
│  │               │   │  Reads:       │   │
│  │  Renders:     │──▶│  /vault/      │   │
│  │  secrets to   │   │  secrets/     │   │
│  │  shared vol   │   │  config.env   │   │
│  └──────────────┘   └───────────────┘   │
│         │                                │
│    Shared Volume                         │
│    (emptyDir or tmpfs)                   │
└─────────────────────────────────────────┘

Kubernetes Sidecar Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      serviceAccountName: myapp

      # Init container: authenticate and render initial secrets
      initContainers:
        - name: vault-agent-init
          image: hashicorp/vault:1.15
          args:
            - "agent"
            - "-config=/etc/vault/vault-agent.hcl"
            - "-exit-after-auth"
          volumeMounts:
            - name: vault-config
              mountPath: /etc/vault
            - name: vault-secrets
              mountPath: /vault/secrets

      containers:
        # Sidecar: keep secrets refreshed
        - name: vault-agent
          image: hashicorp/vault:1.15
          args:
            - "agent"
            - "-config=/etc/vault/vault-agent.hcl"
          volumeMounts:
            - name: vault-config
              mountPath: /etc/vault
            - name: vault-secrets
              mountPath: /vault/secrets
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "128Mi"
              cpu: "100m"

        # Application container
        - name: myapp
          image: myapp:latest
          volumeMounts:
            - name: vault-secrets
              mountPath: /vault/secrets
              readOnly: true
          env:
            - name: DB_PASSWORD_FILE
              value: /vault/secrets/db-password

      volumes:
        - name: vault-config
          configMap:
            name: vault-agent-config
        - name: vault-secrets
          emptyDir:
            medium: Memory  # tmpfs — secrets never hit disk

Agent ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: vault-agent-config
data:
  vault-agent.hcl: |
    vault {
      address = "http://vault.vault.svc.cluster.local:8200"
    }

    auto_auth {
      method "kubernetes" {
        mount_path = "auth/kubernetes"
        config = {
          role = "myapp"
        }
      }

      sink "file" {
        config = {
          path = "/vault/secrets/.vault-token"
        }
      }
    }

    template {
      contents = "{{ with secret \"secret/data/myapp/db\" }}{{ .Data.data.password }}{{ end }}"
      destination = "/vault/secrets/db-password"
    }

    template {
      contents = "{{ with secret \"secret/data/myapp/config\" }}DB_HOST={{ .Data.data.host }}\nDB_PORT={{ .Data.data.port }}\nDB_USER={{ .Data.data.username }}\nDB_PASS={{ .Data.data.password }}{{ end }}"
      destination = "/vault/secrets/app.env"
    }

    exit_after_auth = false

Docker Compose Sidecar

version: "3.8"
services:
  vault-agent:
    image: hashicorp/vault:1.15
    command: ["vault", "agent", "-config=/etc/vault/agent.hcl"]
    volumes:
      - ./vault-agent.hcl:/etc/vault/agent.hcl:ro
      - ./role-id:/etc/vault/role-id:ro
      - ./secret-id:/etc/vault/secret-id:ro
      - shared-secrets:/vault/secrets
    environment:
      - VAULT_ADDR=http://vault:8200

  myapp:
    image: myapp:latest
    depends_on:
      - vault-agent
    volumes:
      - shared-secrets:/vault/secrets:ro
    environment:
      - DB_PASSWORD_FILE=/vault/secrets/db-password

volumes:
  shared-secrets:
    driver_opts:
      type: tmpfs
      device: tmpfs

Application Reading Secrets

# Python — read secret from file
import os

def read_secret(path):
    """Read a secret from a file rendered by Vault Agent."""
    with open(path, 'r') as f:
        return f.read().strip()

db_password = read_secret('/vault/secrets/db-password')
// Go — read secret from file
package main

import (
    "os"
    "strings"
)

func readSecret(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(data)), nil
}

Quick Reference

Pattern Use Case
Init container only One-time secret fetch, no renewal
Sidecar only Continuous renewal and rotation
Init + Sidecar Initial secrets ready before app starts, then continuous

Next: Agent Injector