Vault Agent Sidecar¶
The Vault Agent sidecar pattern injects secrets into application containers as environment variables from a tmpfs volume. No secrets are stored on disk or visible via
docker inspect.
How It Works¶
Vault Server
| AppRole auth (role-id + secret-id)
v
vault-agent container (sidecar)
| reads template /vault/templates/.env.tpl
| fetches secrets from Vault
| renders to /vault/secrets/.env (tmpfs, RAM only)
v
application containers
| mount /vault/secrets as read-only volume
| source .env at startup via shell command
The application container reads secrets from the shared tmpfs volume. The sidecar auto-refreshes every 5 minutes and renews the Vault token every 45 minutes.
Security Benefits¶
| Before (plain env vars) | After (Vault Agent) |
|---|---|
Secrets in group_vars/all.yml |
Only Vault address + AppRole config stored |
.env files on disk |
Secrets only in tmpfs (RAM, never on disk) |
Visible via docker inspect |
Not visible via docker inspect |
| Manual rotation | Automatic refresh with TTL |
| No audit trail | Full audit trail of every access |
Directory Structure on the Host¶
The vault_agent Ansible role deploys this structure:
/data/{app-name}/vault/
├── config/
│ ├── config.hcl # Vault Agent configuration
│ ├── role-id # AppRole Role ID (stable)
│ └── secret-id # AppRole Secret ID (one-time use)
└── templates/
└── .env.tpl # Jinja2-style template with Vault secret references
The secrets/ directory is a Docker tmpfs volume — it exists only in RAM and is never written to disk.
Docker Compose Integration¶
volumes:
vault-secrets:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
services:
vault-agent:
image: hashicorp/vault:latest
container_name: ${APP_NAME}-vault-agent
restart: unless-stopped
command: ["agent", "-config=/vault/config/config.hcl"]
volumes:
- /data/${APP_NAME}/vault/config:/vault/config:ro
- /data/${APP_NAME}/vault/templates:/vault/templates:ro
- vault-secrets:/vault/secrets
healthcheck:
test: ["CMD", "test", "-f", "/vault/secrets/.env"]
interval: 5s
timeout: 3s
retries: 12
app:
image: ${APP_IMAGE}
container_name: ${APP_NAME}
restart: unless-stopped
# Source the .env file at startup — do NOT use env_file directive
command: ["sh", "-c", "set -a && . /vault/secrets/.env && set +a && exec app-binary"]
volumes:
- vault-secrets:/vault/secrets:ro
depends_on:
vault-agent:
condition: service_healthy
Do not use Docker env_file with Vault Agent
Docker's env_file is evaluated at container creation time, before Vault Agent has rendered the secrets file. The file will not exist yet. Always source the .env file via a shell command at runtime.
Template Syntax¶
Templates use Go template syntax with Vault functions. The key pattern is:
# /vault/templates/.env.tpl
{{ with secret "secret/data/cwiq/shared/<app>/database" }}
DB_HOST={{ .Data.data.pg_host }}
DB_PASSWORD={{ .Data.data.pg_password }}
DB_PORT={{ or .Data.data.pg_port "5432" }}
{{ end }}
{{ with secret "secret/data/cwiq/shared/<app>/config" }}
SECRET_KEY={{ .Data.data.secret_key }}
{{ end }}
# Static values (not from Vault)
LOG_LEVEL=info
The .Data.data.<key> path reflects the KV v2 API structure. The outer .Data is the Vault response wrapper; the inner .data is the actual secret payload.
Ansible Role: vault_agent¶
Use the shared vault_agent role to deploy the sidecar configuration:
# In your application's setup.yml
- name: Deploy Vault Agent sidecar
include_role:
name: vault_agent
vars:
vault_agent_app_name: "<app-name>"
vault_role_id: "{{ vault_role_id }}" # From group_vars/all.yml
vault_secret_id: "{{ vault_secret_id }}" # From group_vars/all.yml
vault_agent_template_src: "{{ playbook_dir }}/templates/<app>-env.tpl.j2"
vault_agent_template_dest: ".env"
vault_agent_render_interval: "5m"
when: vault_enabled | default(false)
Role Variables Reference¶
| Variable | Required | Default | Description |
|---|---|---|---|
vault_addr |
No | https://vault.shared.cwiq.io |
Vault server URL |
vault_agent_app_name |
Yes | — | Application name (used for paths) |
vault_role_id |
Yes | — | AppRole Role ID |
vault_secret_id |
Yes | — | AppRole Secret ID |
vault_agent_template_src |
Yes | — | Path to template file |
vault_agent_template_dest |
No | .env |
Rendered output filename |
vault_agent_render_interval |
No | 5m |
Secret refresh interval |
Verifying the Sidecar is Working¶
# Check vault-agent container is healthy
ssh ec2-user@<hostname>-cwiq-io \
"docker ps --filter 'name=<app>-vault-agent' --format '{{.Status}}'"
# Expected: "healthy"
# Check secrets file exists and is non-empty (tmpfs)
ssh ec2-user@<hostname>-cwiq-io \
"docker exec <app>-vault-agent ls -la /vault/secrets/"
# Expected: .env file present
# Check vault-agent logs for errors
ssh ec2-user@<hostname>-cwiq-io \
"docker logs <app>-vault-agent --tail 20"
# Look for: "template rendered", no "error" lines
Related Documentation¶
- Vault Architecture
- AppRole & JWT Auth
- Secret Paths Reference
- Source:
ansible-playbooks/vault-server/docs/03-integration-guide.md