Skip to main content

Vault Secret Management

Resolve secrets from HashiCorp Vault directly in webhook and connection configurations using the {$vault:...} syntax.

Overview

Instead of storing secrets in plaintext in config files or environment variables, you can reference secrets stored in HashiCorp Vault. The Vault secret resolver fetches and caches secrets at runtime.

Config with environment variables (existing):

{
"my_webhook": {
"authorization": "Bearer {$WEBHOOK_TOKEN}"
}
}

Config with Vault references (new):

{
"my_webhook": {
"authorization": "Bearer {$vault:webhooks/my_webhook#bearer_token}"
}
}

Benefits

  • No secrets on disk - Config files contain references, not values
  • Centralized management - Single source of truth for all secrets
  • Audit logging - Vault tracks every secret access
  • Secret rotation - Update secrets in Vault without restarting the app
  • Cache with TTL - In-memory caching reduces Vault API calls

Reference Syntax

Vault references use the format:

{$vault:path/to/secret#field}
{$vault:path/to/secret#field:default_value}
PartDescription
path/to/secretVault KV secret path (max 512 chars, alphanumeric + _./-)
fieldField name within the secret (max 128 chars, alphanumeric + _.-)
default_valueOptional fallback if Vault is unreachable or field is missing

Examples

{
"github_webhook": {
"authorization": "Bearer {$vault:webhooks/github#token}",
"hmac": {
"secret": "{$vault:webhooks/github#hmac_secret}",
"header": "X-Hub-Signature-256",
"algorithm": "sha256"
}
},
"stripe_webhook": {
"authorization": "Bearer {$vault:webhooks/stripe#bearer_token:fallback_token}",
"module": "log"
}
}

Vault references work everywhere environment variable substitution works - in webhooks.json, connections.json, and etcd-stored configs.

Mixed Usage

You can mix {$VAR} environment variables and {$vault:...} references in the same config file. They are resolved independently.

Quick Start

1. Start Vault (Development)

docker run -d --cap-add=IPC_LOCK \
-e 'VAULT_DEV_ROOT_TOKEN_ID=myroot' \
-e 'VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200' \
-p 8200:8200 \
--name vault-dev \
vault:latest

2. Store a Secret

export VAULT_ADDR='http://localhost:8200'
export VAULT_TOKEN='myroot'

# Enable KV v2 engine (if not already enabled)
vault secrets enable -path=secret kv-v2

# Store webhook secrets
vault kv put secret/webhooks/github \
token="ghp_abc123..." \
hmac_secret="whsec_def456..."

3. Configure the Application

# Enable Vault secret resolution
export SECRETS_BACKEND=vault # or VAULT_ENABLED=true
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=myroot # or use VAULT_ROLE_ID + VAULT_SECRET_ID

make run

4. Use Vault References in Config

webhooks.json:

{
"github_events": {
"data_type": "json",
"module": "log",
"authorization": "Bearer {$vault:webhooks/github#token}",
"hmac": {
"secret": "{$vault:webhooks/github#hmac_secret}",
"header": "X-Hub-Signature-256",
"algorithm": "sha256"
}
}
}

5. Test

curl -X POST http://localhost:8000/webhook/github_events \
-H "Authorization: Bearer ghp_abc123..." \
-H "Content-Type: application/json" \
-d '{"action":"push"}'

Environment Variables

VariableDefaultDescription
SECRETS_BACKENDenvSet to vault to enable Vault resolution
VAULT_ENABLEDfalseAlternative: set to true to enable
VAULT_ADDR(required)Vault server URL (e.g., http://localhost:8200)
VAULT_TOKEN(none)Vault token for authentication
VAULT_ROLE_ID(none)AppRole role ID (alternative to token)
VAULT_SECRET_ID(none)AppRole secret ID (alternative to token)
VAULT_MOUNT_POINTsecretKV secrets engine mount point
VAULT_KV_VERSION2KV engine version (1 or 2)
VAULT_NAMESPACE(none)Vault enterprise namespace
VAULT_VERIFYtrueTLS verification (true, false, or CA cert path)
VAULT_CACHE_TTL_SECONDS300Secret cache TTL in seconds (0 to disable)

Authentication

Two authentication methods are supported:

Token Auth (Development)

export VAULT_TOKEN=myroot

AppRole Auth (Production)

export VAULT_ROLE_ID=<role-id>
export VAULT_SECRET_ID=<secret-id>

AppRole is recommended for production. See the full Vault guide for AppRole setup instructions.

Caching

Resolved secrets are cached in memory with a configurable TTL (default: 5 minutes). This avoids hitting Vault on every webhook request.

  • Cache hit: < 1ms
  • Cache miss (Vault API): 50-150ms
  • TTL configurable: via VAULT_CACHE_TTL_SECONDS
  • Cache cleared: on application restart

Set VAULT_CACHE_TTL_SECONDS=0 to disable caching entirely (every request hits Vault).

Works with etcd Backend

Vault references work with both config backends:

  • File backend: References in webhooks.json / connections.json are resolved at load time
  • etcd backend: References in etcd-stored JSON values are resolved at load time
# Store a webhook in etcd with Vault references
etcdctl put /cwm/production/webhooks/secure_hook \
'{"data_type":"json","module":"log","authorization":"Bearer {$vault:webhooks/secure#token}"}'

Docker Scenario

An end-to-end Docker Compose scenario is available at docker/scenario/06_vault_etcd_secrets/:

cd docker/scenario/06_vault_etcd_secrets
docker compose up -d
bash seed_vault.sh
bash seed_etcd.sh
bash run_test.sh
docker compose down

This tests Vault-backed authorization, HMAC secrets, legacy env placeholder compatibility, and secret rotation flows.

Further Reading

  • Full integration guide: docs/VAULT_INTEGRATION_GUIDE.md
  • Source: src/vault_secret_resolver.py
  • Variable substitution: src/utils.py (load_env_vars)
  • Tests: tests/unit/test_vault_secret_resolver.py