Privacy Tools Guide

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.