Setting Up Vault for Secrets Management
HashiCorp Vault centralizes secret storage, enforces access policies, and issues short-lived dynamic credentials. Instead of hardcoding a database password in your app, Vault issues a temporary credential that expires in 1 hour. When it’s gone, so is the attack surface.
Installation
# Debian/Ubuntu
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
# RHEL/CentOS
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install vault
# Verify
vault version
Development Mode (Testing Only)
# Start Vault in dev mode (in-memory, pre-unsealed, auto-root-token)
vault server -dev -dev-root-token-id="root"
# In another terminal:
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'
vault status
vault secrets list
Dev mode stores nothing permanently — data is lost on restart. Never use in production.
Production Installation with systemd
# /etc/vault.d/vault.hcl
ui = true
storage "raft" {
path = "/opt/vault/data"
node_id = "vault-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/opt/vault/tls/vault.crt"
tls_key_file = "/opt/vault/tls/vault.key"
}
api_addr = "https://vault.internal.example.com:8200"
cluster_addr = "https://vault.internal.example.com:8201"
# Create data directory and permissions
sudo mkdir -p /opt/vault/data /opt/vault/tls
sudo chown -R vault:vault /opt/vault
# Generate TLS cert (or use Let's Encrypt)
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout /opt/vault/tls/vault.key \
-out /opt/vault/tls/vault.crt \
-subj "/CN=vault.internal.example.com"
sudo systemctl enable --now vault
# Initialize Vault (first time only)
vault operator init -key-shares=5 -key-threshold=3
init outputs 5 unseal keys and 1 root token. Store each key in a different location (different person, different password manager). You need 3 of the 5 to unseal.
# Unseal (requires 3 keys)
vault operator unseal # paste key 1
vault operator unseal # paste key 2
vault operator unseal # paste key 3
vault status # Sealed: false
KV Secrets Engine
export VAULT_ADDR='https://vault.internal.example.com:8200'
export VAULT_TOKEN='hvs.your-root-token'
# Enable KV v2 secrets engine
vault secrets enable -path=secret kv-v2
# Write a secret
vault kv put secret/myapp/database \
username="appuser" \
password="$(openssl rand -base64 24)"
# Read a secret
vault kv get secret/myapp/database
# Read as JSON
vault kv get -format=json secret/myapp/database | jq '.data.data'
# Update a single field (KV v2 patches)
vault kv patch secret/myapp/database password="new-password"
# List versions
vault kv metadata get secret/myapp/database
# Roll back to a previous version
vault kv rollback -version=2 secret/myapp/database
Dynamic Database Credentials
This is Vault’s most powerful feature for databases. Vault creates a temporary database user, hands the credentials to your app, and deletes the user when the TTL expires.
# Enable the database secrets engine
vault secrets enable database
# Configure a PostgreSQL connection
vault write database/config/my-postgresql \
plugin_name=postgresql-database-plugin \
allowed_roles="app-role" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/appdb" \
username="vault_admin" \
password="$(cat /etc/vault/pg-admin-password)"
-- On the PostgreSQL server, create Vault's admin user with permission to create roles
CREATE ROLE vault_admin WITH LOGIN PASSWORD 'strong-password' CREATEROLE;
GRANT CONNECT ON DATABASE appdb TO vault_admin;
# Create a role that defines what the temporary credentials can do
vault write database/roles/app-role \
db_name=my-postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Request credentials
vault read database/creds/app-role
# Key Value
# --- -----
# lease_duration 1h
# username v-token-app-role-AbCdEfGhIj
# password A1B2C3D4E5F6...
Your application requests credentials at startup. When it crashes or restarts, it gets new credentials — the old ones are already deleted.
Policies
Policies define what a token can access.
# app-policy.hcl
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "database/creds/app-role" {
capabilities = ["read"]
}
# Deny everything else
path "*" {
capabilities = ["deny"]
}
vault policy write app-policy app-policy.hcl
# Create a token tied to this policy
vault token create -policy="app-policy" -ttl=24h
# Verify the policy
vault token lookup <token>
Kubernetes Integration (Vault Agent)
Vault Agent runs as a sidecar, authenticates to Vault, and writes secrets to files for your application.
# kubernetes-auth setup
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert="$(kubectl config view --raw --minify -o json | jq -r '.clusters[0].cluster["certificate-authority-data"]' | base64 -d)"
# Create a role binding the service account to the policy
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=production \
policies=app-policy \
ttl=1h
# pod-with-vault-agent.yaml
apiVersion: v1
kind: Pod
spec:
serviceAccountName: myapp
initContainers:
- name: vault-agent
image: hashicorp/vault:latest
command: ["vault", "agent", "-config=/vault/config/agent.hcl"]
volumeMounts:
- name: vault-config
mountPath: /vault/config
- name: secrets
mountPath: /vault/secrets
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets
mountPath: /var/secrets
readOnly: true
volumes:
- name: vault-config
configMap:
name: vault-agent-config
- name: secrets
emptyDir:
medium: Memory # tmpfs — secrets never written to disk
Vault Agent for CI/CD Pipelines
Hardcoding a Vault token in your CI/CD environment is a security smell — the token can be printed in logs, copied to test environments, or leaked through environment variable exposure. Vault’s AppRole auth method is designed for machine-to-machine authentication: your CI runner gets a role_id (not secret) and a short-lived secret_id (vended per job) to authenticate.
# Enable AppRole auth method
vault auth enable approle
# Create a role for CI — short TTL, limited policies
vault write auth/approle/role/ci-deployer \
token_policies="deploy-policy" \
token_ttl=10m \
token_max_ttl=30m \
secret_id_ttl=5m \ # secret_id expires in 5 minutes
secret_id_num_uses=1 # single-use secret_id (most secure)
# Get the role_id (not secret — safe to store in CI env var)
vault read auth/approle/role/ci-deployer/role-id
In your CI pipeline, generate a fresh secret_id per job using a trusted CI agent that has a long-lived token with minimal permissions (only auth/approle/role/ci-deployer/secret-id):
# GitHub Actions / CI step
# Step 1: Generate a one-time secret_id
SECRET_ID=$(vault write -f -field=secret_id \
auth/approle/role/ci-deployer/secret-id)
# Step 2: Exchange role_id + secret_id for a short-lived token
VAULT_TOKEN=$(vault write -field=token auth/approle/login \
role_id="$ROLE_ID" \
secret_id="$SECRET_ID")
# Step 3: Use the token to read deploy secrets
export DB_PASSWORD=$(VAULT_TOKEN="$VAULT_TOKEN" \
vault kv get -field=password secret/myapp/database)
The secret_id is consumed on first use and expires in 5 minutes regardless — even if it is leaked, it cannot be reused.
Audit Logging and Compliance
Vault logs every read and write operation. Enable the audit device to capture a structured log of all secret accesses:
# Enable file audit log
vault audit enable file file_path=/var/log/vault/audit.log
# Enable syslog audit (to ship to your SIEM)
vault audit enable syslog tag="vault" facility="AUTH"
# Verify audit is enabled
vault audit list
The audit log contains JSON entries for each operation:
{
"time": "2026-03-22T14:32:01.000Z",
"type": "response",
"auth": {
"client_token": "hmac-sha256:...",
"accessor": "auth/approle/...",
"display_name": "approle-ci-deployer",
"policies": ["deploy-policy"]
},
"request": {
"operation": "read",
"path": "secret/data/myapp/database"
},
"response": {
"data": {
"username": "hmac-sha256:..."
}
}
}
Secret values are HMAC-hashed in the audit log — you can verify whether a specific value was accessed without storing it in plaintext. Query who accessed production database credentials in the last hour:
# Parse audit log for reads of the database secret
jq 'select(.request.path == "secret/data/myapp/database" and .request.operation == "read")
| {time: .time, user: .auth.display_name}' /var/log/vault/audit.log
For compliance reporting, these logs satisfy SOC 2, PCI DSS, and ISO 27001 secret access logging requirements when retained for 90+ days.
Related Articles
- How to Audit Your Password Manager Vault: A Practical Guide
- Audit Password Vault for Weak, Duplicate, and Reused
- Bitwarden Web Vault vs Desktop App Comparison
- 1Password Secrets Automation for DevOps: A Practical Guide
- Bitwarden Vault Export Backup Guide
- AI Tools for Automated Secrets Rotation and Vault Management Built by theluckystrike — More at zovo.one