HashiCorp Vault
Deploy and operate HashiCorp Vault for centralized secrets management
HashiCorp Vault
HashiCorp Vault is the industry standard for secrets management. It provides a unified interface to any secret while providing tight access control and recording a detailed audit log.
Architecture Overview
Vault uses a client-server architecture with pluggable backends:
+------------------+
| Clients |
| (CLI/API/UI) |
+--------+---------+
|
v
+--------+---------+
| Vault Server |
| (API Layer) |
+--------+---------+
|
+-------------------+-------------------+
| | |
v v v
+-----+-----+ +-----+-----+ +-----+-----+
| Secrets | | Auth | | Audit |
| Engines | | Methods | | Devices |
+-----------+ +-----------+ +-----------+
| | |
v v v
+-----+-----+ +-----+-----+ +-----+-----+
| Storage | | LDAP/ | | File/ |
| Backend | | OIDC/etc | | Syslog |
+-----------+ +-----------+ +-----------+
Installation and Setup
Development Mode
For learning and testing (never use in production):
# Start Vault in dev mode
vault server -dev
# Dev mode automatically:
# - Unseals the vault
# - Sets root token to 'root'
# - Enables in-memory storage
# - Listens on localhost:8200
Production Deployment with Docker
# docker-compose.yml
version: '3.8'
services:
vault:
image: hashicorp/vault:1.15
container_name: vault
ports:
- "8200:8200"
environment:
VAULT_ADDR: 'http://127.0.0.1:8200'
cap_add:
- IPC_LOCK
volumes:
- ./vault/config:/vault/config
- ./vault/data:/vault/data
- ./vault/logs:/vault/logs
command: vault server -config=/vault/config/config.hcl
# vault/config/config.hcl
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = false
tls_cert_file = "/vault/config/tls/vault.crt"
tls_key_file = "/vault/config/tls/vault.key"
}
api_addr = "https://vault.example.com:8200"
cluster_addr = "https://vault.example.com:8201"
ui = true
# Telemetry for monitoring
telemetry {
prometheus_retention_time = "30s"
disable_hostname = true
}
Initialization and Unsealing
# Initialize Vault (only once, on first setup)
vault operator init -key-shares=5 -key-threshold=3
# Output:
# Unseal Key 1: abc123...
# Unseal Key 2: def456...
# Unseal Key 3: ghi789...
# Unseal Key 4: jkl012...
# Unseal Key 5: mno345...
# Initial Root Token: hvs.xxxxx
# CRITICAL: Store these keys securely and separately!
# Use Shamir's Secret Sharing - no single person should have all keys
# Unseal Vault (requires threshold number of keys)
vault operator unseal # Enter key 1
vault operator unseal # Enter key 2
vault operator unseal # Enter key 3
# Login with root token
vault login hvs.xxxxx
Secrets Engines
KV (Key-Value) Secrets Engine
The most common secrets engine for storing arbitrary secrets:
# Enable KV v2 (versioned)
vault secrets enable -path=secret kv-v2
# Store a secret
vault kv put secret/myapp/database \
username="dbuser" \
password="s3cr3t" \
host="db.example.com"
# Read a secret
vault kv get secret/myapp/database
# Read specific field
vault kv get -field=password secret/myapp/database
# Read specific version
vault kv get -version=1 secret/myapp/database
# Delete a secret (soft delete, can be recovered)
vault kv delete secret/myapp/database
# Permanently destroy
vault kv destroy -versions=1,2 secret/myapp/database
Database Secrets Engine (Dynamic Secrets)
Generate on-demand, short-lived database credentials:
# Enable the database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/mydb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb" \
allowed_roles="readonly,readwrite" \
username="vault_admin" \
password="vault_admin_password"
# Create a role that generates credentials
vault write database/roles/readonly \
db_name=mydb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Generate credentials
vault read database/creds/readonly
# Output:
# Key Value
# lease_id database/creds/readonly/abc123
# lease_duration 1h
# username v-root-readonly-xyz789
# password A1B2C3D4E5...
AWS Secrets Engine
Generate dynamic AWS IAM credentials:
# Enable AWS secrets engine
vault secrets enable aws
# Configure root credentials
vault write aws/config/root \
access_key=AKIAIOSFODNN7EXAMPLE \
secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
region=us-east-1
# Create role for EC2 admin
vault write aws/roles/ec2-admin \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ec2:*",
"Resource": "*"
}
]
}
EOF
# Generate credentials
vault read aws/creds/ec2-admin
Authentication Methods
AppRole (for Applications)
Most common method for automated workloads:
# Enable AppRole auth
vault auth enable approle
# Create a role for your application
vault write auth/approle/role/myapp \
token_policies="myapp-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=10m \
secret_id_num_uses=1
# Get role ID (can be embedded in app config)
vault read auth/approle/role/myapp/role-id
# Generate secret ID (should be delivered securely)
vault write -f auth/approle/role/myapp/secret-id
# Application authenticates with both
vault write auth/approle/login \
role_id="abc123" \
secret_id="def456"
Kubernetes Authentication
For applications running in Kubernetes:
# Enable Kubernetes auth
vault auth enable kubernetes
# Configure with cluster info
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Create a role bound to a service account
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp-sa \
bound_service_account_namespaces=production \
policies=myapp-policy \
ttl=1h
# Kubernetes pod spec using Vault Agent Injector
apiVersion: v1
kind: Pod
metadata:
name: myapp
annotations:
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/role: 'myapp'
vault.hashicorp.com/agent-inject-secret-db-creds: 'secret/data/myapp/database'
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "secret/data/myapp/database" -}}
export DB_USER="{{ .Data.data.username }}"
export DB_PASS="{{ .Data.data.password }}"
{{- end -}}
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: myapp:latest
Policies
Vault policies define what secrets a token can access:
# myapp-policy.hcl
# Read application secrets
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
# Generate database credentials
path "database/creds/myapp-readonly" {
capabilities = ["read"]
}
# No access to other apps
path "secret/data/otherapp/*" {
capabilities = ["deny"]
}
# Allow token renewal
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Create policy
vault policy write myapp-policy myapp-policy.hcl
# List policies
vault policy list
# Read policy
vault policy read myapp-policy
Audit Logging
Enable audit logging for compliance:
# Enable file audit device
vault audit enable file file_path=/vault/logs/audit.log
# Enable syslog audit
vault audit enable syslog tag="vault" facility="AUTH"
# Audit log entry (JSON)
# {
# "time": "2024-01-15T10:30:00Z",
# "type": "response",
# "auth": {
# "client_token": "hmac-sha256:abc123",
# "policies": ["myapp-policy"]
# },
# "request": {
# "operation": "read",
# "path": "secret/data/myapp/database"
# }
# }
High Availability
Raft Storage (Integrated)
# vault-node-1.hcl
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
retry_join {
leader_api_addr = "https://vault-2.example.com:8200"
}
retry_join {
leader_api_addr = "https://vault-3.example.com:8200"
}
}
# Check raft peers
vault operator raft list-peers
# Add a new node
vault operator raft join https://vault-1.example.com:8200
Application Integration Example
Python Application
import hvac
import os
class VaultClient:
def __init__(self):
self.client = hvac.Client(
url=os.environ.get('VAULT_ADDR', 'http://localhost:8200')
)
self._authenticate()
def _authenticate(self):
# AppRole authentication
role_id = os.environ['VAULT_ROLE_ID']
secret_id = os.environ['VAULT_SECRET_ID']
response = self.client.auth.approle.login(
role_id=role_id,
secret_id=secret_id
)
self.client.token = response['auth']['client_token']
def get_secret(self, path, key=None):
secret = self.client.secrets.kv.v2.read_secret_version(
path=path
)
data = secret['data']['data']
return data.get(key) if key else data
def get_database_credentials(self, role='readonly'):
creds = self.client.secrets.database.generate_credentials(
name=role
)
return {
'username': creds['data']['username'],
'password': creds['data']['password'],
'lease_id': creds['lease_id'],
'lease_duration': creds['lease_duration']
}
# Usage
vault = VaultClient()
db_config = vault.get_secret('myapp/database')
dynamic_creds = vault.get_database_credentials()
Best Practices
- Never store root token - Generate and revoke after initial setup
- Use short TTLs - Minimize blast radius of compromised credentials
- Enable audit logging - Required for compliance and forensics
- Rotate credentials regularly - Use dynamic secrets where possible
- Principle of least privilege - Grant minimum required permissions
- Encrypt in transit and at rest - Always use TLS
- Separate unseal keys - Use Shamir's Secret Sharing properly
Part of: Secrets Management
Updated: 1/24/2025
Found an issue?