HashiCorp Vault: Secrets Management for Your Homelab and Enterprise
Hardcoded credentials in config files, .env files committed to Git, shared passwords stored in Notepad — if any of that sounds familiar, this project is for you. HashiCorp Vault is the industry-standard solution for centralizing secret storage, dynamic credential generation, and certificate management. By the end of this project you'll have a production-grade Vault cluster running in your lab, integrated with your applications, and generating short-lived credentials automatically.
Project Overview
What We're Building
┌─────────────────────────────────────────────────────────────────────┐
│ HashiCorp Vault Lab Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Clients Vault Cluster Backends │
│ │
│ ┌──────────┐ ┌────────────────┐ ┌──────────┐ │
│ │ Apps / │──AppRole──▶ │ vault-01 │────────▶│ KV Store │ │
│ │ Services │ │ (Active) │ └──────────┘ │
│ └──────────┘ │ :8200 │ │
│ └────────────────┘ ┌──────────┐ │
│ ┌──────────┐ │ HA │ PKI │ │
│ │ DevOps │──Token Auth──▶ │ │ Engine │ │
│ │ Engineers│ ┌────────────────┐ └──────────┘ │
│ └──────────┘ │ vault-02 │ │
│ │ (Standby) │ ┌──────────┐ │
│ ┌──────────┐ │ :8200 │────────▶│ Database │ │
│ │ K8s Pods │──SA Auth───▶└────────────────┘ │ Dynamic │ │
│ └──────────┘ │ │ Secrets │ │
│ ┌──────▼─────────┐ └──────────┘ │
│ ┌──────────┐ │ Consul │ │
│ │ LDAP │──LDAP Auth─▶│ (Storage │ ┌──────────┐ │
│ │ Users │ │ + Service │────────▶│ SSH CA │ │
│ └──────────┘ │ Discovery) │ │ Engine │ │
│ └────────────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘Prerequisites
- 2+ Linux VMs (Ubuntu 22.04 recommended) — 2 vCPU, 4GB RAM each
- Docker (for single-node dev mode option)
- Basic understanding of TLS and public key cryptography
- Familiarity with Linux CLI and systemd
- An application or database to integrate with (PostgreSQL used in examples)
Why Vault?
| Problem | Without Vault | With Vault |
|---|---|---|
| DB passwords | Hardcoded in .env | Dynamic, auto-rotated |
| TLS certificates | Manual renewal, often forgotten | Auto-issued, auto-renewed |
| API keys | Shared in Slack/email | Scoped, audited, short-lived |
| SSH access | Shared keys, no auditability | Signed certificates, full audit log |
| Secret rotation | Manual, painful | Automated, zero-downtime |
Part 1: Installation
Step 1: Install Vault on Both Nodes
On both vault-01 and vault-02:
# Add HashiCorp GPG key and repo
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [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 -y vault consul
# Verify
vault version
consul versionStep 2: Configure Consul Storage Backend
Vault needs a storage backend. We'll use Consul for HA and built-in service discovery.
On both nodes — Consul config:
# /etc/consul.d/consul.hcl
datacenter = "homelab"
data_dir = "/opt/consul"
log_level = "INFO"
node_name = "vault-01" # change per node
bind_addr = "0.0.0.0"
client_addr = "0.0.0.0"
server = true
bootstrap_expect = 2
retry_join = ["192.168.1.20", "192.168.1.21"]
ui_config {
enabled = true
}
performance {
raft_multiplier = 1
}# Start Consul
sudo systemctl enable consul
sudo systemctl start consul
# Verify cluster
consul membersExpected output:
Node Address Status Type Build Protocol
vault-01 192.168.1.20:8301 alive server 1.18.0 2
vault-02 192.168.1.21:8301 alive server 1.18.0 2Step 3: Generate TLS Certificates for Vault
Never run Vault without TLS in production.
# Generate a CA on vault-01
mkdir -p /etc/vault.d/tls && cd /etc/vault.d/tls
# Create CA key and cert
openssl genrsa -out vault-ca-key.pem 4096
openssl req -new -x509 -days 3650 \
-key vault-ca-key.pem \
-out vault-ca.pem \
-subj "/CN=Vault CA/O=HomeLab"
# Create Vault server certificate
openssl genrsa -out vault-key.pem 2048
cat > vault-cert.cnf <<EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = vault.homelab.local
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = vault.homelab.local
DNS.2 = vault-01.homelab.local
DNS.3 = vault-02.homelab.local
IP.1 = 192.168.1.20
IP.2 = 192.168.1.21
IP.3 = 127.0.0.1
EOF
openssl req -new -key vault-key.pem -out vault.csr -config vault-cert.cnf
openssl x509 -req -days 825 \
-in vault.csr \
-CA vault-ca.pem -CAkey vault-ca-key.pem -CAcreateserial \
-out vault-cert.pem \
-extensions v3_req -extfile vault-cert.cnf
# Distribute CA cert to vault-02
scp vault-ca.pem vault-cert.pem vault-key.pem user@192.168.1.21:/etc/vault.d/tls/
# Set permissions
chmod 640 /etc/vault.d/tls/*.pem
chown vault:vault /etc/vault.d/tls/*.pemStep 4: Configure Vault
# /etc/vault.d/vault.hcl
ui = true
log_level = "info"
storage "consul" {
address = "127.0.0.1:8500"
path = "vault/"
token = "" # Add Consul ACL token if enabled
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/etc/vault.d/tls/vault-cert.pem"
tls_key_file = "/etc/vault.d/tls/vault-key.pem"
tls_min_version = "tls12"
}
api_addr = "https://192.168.1.20:8200" # Change per node
cluster_addr = "https://192.168.1.20:8201"
seal "shamir" {}
telemetry {
prometheus_retention_time = "30s"
disable_hostname = true
}# Enable and start Vault
sudo systemctl enable vault
sudo systemctl start vault
# Export Vault address
export VAULT_ADDR="https://vault.homelab.local:8200"
export VAULT_CACERT="/etc/vault.d/tls/vault-ca.pem"
# Check status (should show uninitialized)
vault statusPart 2: Initialization and Unsealing
Step 5: Initialize Vault
Run only on vault-01, only once:
# Initialize with 5 key shares, threshold of 3
vault operator init \
-key-shares=5 \
-key-threshold=3
# Save the output SECURELY — you will NOT see these again
# Example output:
# Unseal Key 1: abc123...
# Unseal Key 2: def456...
# Unseal Key 3: ghi789...
# Unseal Key 4: jkl012...
# Unseal Key 5: mno345...
# Initial Root Token: s.xxxxxxxxxxxxxxxxxxxxCRITICAL: Store unseal keys in separate, secure locations. In production, distribute them to 5 different trusted people. Never store all keys in one place.
Step 6: Unseal Vault
# Provide 3 of 5 key shares (must run 3 times with different keys)
vault operator unseal # Enter key 1
vault operator unseal # Enter key 2
vault operator unseal # Enter key 3
# Verify sealed = false
vault statusStatus after unsealing:
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 5
Threshold 3
Version 1.18.0
Cluster Name vault-cluster-homelab
Cluster ID abc123...
HA Enabled true
HA Cluster https://192.168.1.20:8201
HA Mode active
Active Since 2026-03-13T12:00:00.000ZStep 7: Auto-Unseal with AWS KMS (Optional but Recommended)
For production, manual unsealing is tedious. Auto-unseal with a KMS key is safer.
# Replace the shamir seal block with:
seal "awskms" {
region = "ca-central-1"
kms_key_id = "alias/vault-auto-unseal"
}# Or use Azure Key Vault
seal "azurekeyvault" {
tenant_id = "YOUR_TENANT_ID"
client_id = "YOUR_CLIENT_ID"
client_secret = "YOUR_CLIENT_SECRET"
vault_name = "vault-unseal-kv"
key_name = "vault-unseal-key"
}Step 8: Create Admin Policy and Token
# Login with root token (first time only)
vault login
# Create admin policy
cat > admin-policy.hcl <<'EOF'
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOF
vault policy write admin admin-policy.hcl
# Create admin user (using userpass auth)
vault auth enable userpass
vault write auth/userpass/users/admin \
password="$(openssl rand -base64 20)" \
policies="admin"
# Revoke root token — don't leave it active
vault token revoke "$VAULT_TOKEN"Part 3: Secrets Engines
Step 9: KV Secrets Engine (v2)
The KV engine is the simplest — a versioned key-value store for static secrets.
# Enable KV v2 at the path "secret/"
vault secrets enable -path=secret -version=2 kv
# Store secrets
vault kv put secret/myapp/database \
username="dbuser" \
password="$(openssl rand -base64 20)" \
host="postgres.homelab.local" \
port="5432"
# Read secrets
vault kv get secret/myapp/database
# Read specific field
vault kv get -field=password secret/myapp/database
# Version history
vault kv metadata get secret/myapp/database
# Roll back to previous version
vault kv rollback -version=1 secret/myapp/databaseOutput:
====== Secret Path ======
secret/data/myapp/database
======= Metadata =======
Key Value
--- -----
created_time 2026-03-13T12:00:00.000Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
====== Data ======
Key Value
--- -----
host postgres.homelab.local
password Xk9mL2pQrT8vW5nZ6eYu
port 5432
username dbuserStep 10: Dynamic Database Secrets
This is where Vault gets powerful — generate temporary, unique database credentials per application.
# Enable the database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/myapp-postgres \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@postgres.homelab.local:5432/myapp?sslmode=disable" \
allowed_roles="myapp-readonly,myapp-readwrite" \
username="vault-admin" \
password="vault-admin-password"
# Create a role with TTL-limited credentials
vault write database/roles/myapp-readonly \
db_name="myapp-postgres" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
# Generate temporary credentials
vault read database/creds/myapp-readonlyOutput — ephemeral credentials valid for 1 hour:
Key Value
--- -----
lease_id database/creds/myapp-readonly/abc123xyz
lease_duration 1h
lease_renewable true
password A1a-x9KpZm2LqNvRoWs4
username v-userpass-myapp-readon-pX8mNbKtQr-1710331200Step 11: PKI Secrets Engine (Internal CA)
Replace your self-signed cert mess with a proper certificate authority.
# Enable PKI engine for the root CA
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki # 10 years for root
# Generate root CA
vault write -field=certificate pki/root/generate/internal \
common_name="HomeLab Root CA" \
issuer_name="root-2026" \
ttl="87600h" > /tmp/root_ca.crt
# Configure CRL and OCSP
vault write pki/config/cluster \
path="https://vault.homelab.local:8200/v1/pki"
vault write pki/config/urls \
issuing_certificates="https://vault.homelab.local:8200/v1/pki/ca" \
crl_distribution_points="https://vault.homelab.local:8200/v1/pki/crl"
# Enable intermediate CA for issuing certs
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int # 5 years max
# Generate intermediate CA CSR
vault write -format=json pki_int/intermediate/generate/internal \
common_name="HomeLab Intermediate CA 2026" \
issuer_name="homelab-intermediate" \
| jq -r '.data.csr' > /tmp/pki_intermediate.csr
# Sign with root CA
vault write -format=json pki/root/sign-intermediate \
issuer_ref="root-2026" \
csr=@/tmp/pki_intermediate.csr \
format=pem_bundle \
ttl="43800h" \
| jq -r '.data.certificate' > /tmp/intermediate.cert.pem
# Import signed cert back
vault write pki_int/intermediate/set-signed \
certificate=@/tmp/intermediate.cert.pem
# Create a role for issuing certs
vault write pki_int/roles/homelab-server \
allowed_domains="homelab.local" \
allow_subdomains=true \
max_ttl="720h" \
generate_lease=true
# Issue a certificate
vault write pki_int/issue/homelab-server \
common_name="grafana.homelab.local" \
alt_names="grafana.homelab.local" \
ttl="720h"Part 4: Authentication Methods
Step 12: AppRole Authentication (for Applications)
AppRole is designed for machine-to-machine authentication with no secrets in code.
# Enable AppRole
vault auth enable approle
# Create a policy for the application
cat > myapp-policy.hcl <<'EOF'
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "database/creds/myapp-readonly" {
capabilities = ["read"]
}
path "pki_int/issue/homelab-server" {
capabilities = ["create", "update"]
}
EOF
vault policy write myapp myapp-policy.hcl
# Create AppRole
vault write auth/approle/role/myapp \
secret_id_ttl=10m \
token_num_uses=10 \
token_ttl=20m \
token_max_ttl=30m \
secret_id_num_uses=40 \
policies="myapp"
# Get Role ID (not a secret — embed in app config)
vault read auth/approle/role/myapp/role-id
# Get Secret ID (treated as a secret — inject at runtime via CI/CD)
vault write -f auth/approle/role/myapp/secret-idApplication authentication flow:
# App authenticates and gets a token
ROLE_ID="<role-id>"
SECRET_ID="<secret-id>"
TOKEN=$(vault write -field=token auth/approle/login \
role_id="$ROLE_ID" \
secret_id="$SECRET_ID")
# Use token to read secrets
VAULT_TOKEN="$TOKEN" vault kv get secret/myapp/databaseStep 13: Kubernetes Authentication
For pods running in Kubernetes to authenticate without static secrets.
# Enable Kubernetes auth
vault auth enable kubernetes
# Configure the Kubernetes auth backend
vault write auth/kubernetes/config \
kubernetes_host="https://192.168.1.100:6443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
issuer="https://kubernetes.default.svc.cluster.local"
# Create a role for a specific service account
vault write auth/kubernetes/role/myapp-role \
bound_service_account_names="myapp-sa" \
bound_service_account_namespaces="production" \
policies="myapp" \
ttl="1h"Kubernetes deployment using Vault Agent Injector:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp-role"
vault.hashicorp.com/agent-inject-secret-config.env: "secret/data/myapp/database"
vault.hashicorp.com/agent-inject-template-config.env: |
{{- with secret "secret/data/myapp/database" -}}
export DB_USER="{{ .Data.data.username }}"
export DB_PASS="{{ .Data.data.password }}"
export DB_HOST="{{ .Data.data.host }}"
{{- end }}
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: myapp:latest
command: ["/bin/sh", "-c"]
args: ["source /vault/secrets/config.env && /app/start.sh"]Step 14: LDAP / Active Directory Authentication
# Enable LDAP auth
vault auth enable ldap
# Configure for Active Directory
vault write auth/ldap/config \
url="ldaps://dc01.homelab.local:636" \
starttls=false \
insecure_tls=false \
binddn="CN=vault-svc,OU=Service Accounts,DC=homelab,DC=local" \
bindpass="service-account-password" \
userdn="OU=Users,DC=homelab,DC=local" \
userattr="sAMAccountName" \
groupdn="OU=Groups,DC=homelab,DC=local" \
groupfilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))" \
groupattr="cn" \
certificate=@/etc/vault.d/tls/dc-ca.pem
# Map AD group to Vault policy
vault write auth/ldap/groups/VaultAdmins \
policies="admin"
vault write auth/ldap/groups/VaultUsers \
policies="default,read-only"
# Test login
vault login -method=ldap username="jsmith"Part 5: Audit Logging and Policy Management
Step 15: Enable Audit Logging
# Enable file audit log (always do this in production)
vault audit enable file file_path="/var/log/vault/audit.log"
# Optional: syslog backend for SIEM integration
vault audit enable syslog tag="vault" facility="AUTH"
# Verify audit is enabled
vault audit list -detailedSample audit log entry (JSON):
{
"time": "2026-03-13T12:00:00.000Z",
"type": "request",
"auth": {
"client_token": "hmac-sha256:abc...",
"accessor": "hmac-sha256:def...",
"policies": ["myapp"],
"entity_id": "...",
"display_name": "approle-myapp"
},
"request": {
"id": "...",
"operation": "read",
"mount_type": "kv",
"path": "secret/data/myapp/database",
"remote_address": "192.168.1.50"
},
"response": {
"mount_type": "kv"
}
}Step 16: Granular Policy Examples
# /etc/vault.d/policies/developer.hcl
# Developers can read dev secrets and generate their own DB creds
# Read dev environment secrets
path "secret/data/dev/*" {
capabilities = ["read", "list"]
}
# Generate database creds for dev DB
path "database/creds/dev-readwrite" {
capabilities = ["read"]
}
# Request their own TLS certs for dev
path "pki_int/issue/homelab-server" {
capabilities = ["create", "update"]
allowed_parameters = {
"common_name" = ["*.dev.homelab.local"]
"ttl" = ["1h", "4h", "8h", "24h"]
}
}
# View but not manage auth
path "auth/*" {
capabilities = ["list"]
}
# Cannot access prod
path "secret/data/prod/*" {
capabilities = []
}# /etc/vault.d/policies/ci-cd.hcl
# CI/CD pipeline — read-only, specific paths
path "secret/data/ci/+/credentials" {
capabilities = ["read"]
}
path "database/creds/app-migrate" {
capabilities = ["read"]
}
# Allow issuing deploy certs
path "pki_int/issue/deploy-server" {
capabilities = ["create", "update"]
max_wrapping_ttl = "5m"
}Part 6: Automated Secret Rotation
Step 17: Lease Renewal and Rotation Script
#!/bin/bash
# vault-rotate.sh — Rotate static secrets and notify apps
VAULT_ADDR="https://vault.homelab.local:8200"
VAULT_TOKEN="${VAULT_TOKEN:-$(cat /etc/vault-agent/token)}"
rotate_secret() {
local path="$1"
local key="$2"
local new_value
new_value=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Rotating $path/$key"
vault kv patch \
-address="$VAULT_ADDR" \
-header="X-Vault-Token: $VAULT_TOKEN" \
"$path" \
"$key=$new_value"
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] Rotated $path/$key successfully"
}
# Rotate application API keys
rotate_secret "secret/prod/myapp" "api_key"
rotate_secret "secret/prod/myapp" "webhook_secret"
# Force renew expiring leases
vault lease renew "$(vault list sys/leases/lookup/database/creds/myapp-readonly | head -1)"
echo "Rotation complete."Step 18: Vault Agent for Automatic Token Renewal
# /etc/vault-agent/config.hcl
vault {
address = "https://vault.homelab.local:8200"
ca_cert = "/etc/vault.d/tls/vault-ca.pem"
retry {
num_retries = 5
}
}
auto_auth {
method "approle" {
mount_path = "auth/approle"
config = {
role_id_file_path = "/etc/vault-agent/role_id"
secret_id_file_path = "/etc/vault-agent/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault-agent/token"
}
}
}
template {
source = "/etc/vault-agent/templates/database.env.tpl"
destination = "/etc/myapp/database.env"
command = "systemctl restart myapp"
}
template {
source = "/etc/vault-agent/templates/tls.tpl"
destination = "/etc/myapp/tls/server.crt"
command = "systemctl reload nginx"
}# /etc/vault-agent/templates/database.env.tpl
{{ with secret "database/creds/myapp-readonly" }}
DB_USER="{{ .Data.username }}"
DB_PASS="{{ .Data.password }}"
DB_HOST="postgres.homelab.local"
DB_NAME="myapp"
{{ end }}Verification Checklist
Cluster Health:
- Both Vault nodes active/standby
- Consul cluster healthy with 2+ members
- Vault status shows
Sealed: false - HA mode active with correct leader
Secrets Engines:
- KV v2 engine mounted at
secret/ - Database dynamic creds generate successfully
- PKI engine issuing certs signed by your CA
- Issued cert verifiable with
openssl verify
Authentication:
- AppRole login returns valid token
- Token has correct policy scope (test with
vault token capabilities) - LDAP/AD login works for AD users
- Kubernetes auth working from a test pod
Security Controls:
- Audit log receiving entries
- Root token revoked after initial setup
- Least-privilege policies — developer cannot read prod
- TLS enforced on all connections (no plaintext listener)
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
connection refused | Vault sealed or not started | vault status, check systemctl status vault |
permission denied | Token lacks policy capability | vault token capabilities <path> |
x509: certificate signed by unknown authority | CA not trusted | Export VAULT_CACERT to your CA cert |
| DB creds expiring too fast | TTL too short | Adjust default_ttl on database role |
unseal key invalid | Wrong key used | Use correct key shard, check Shamir threshold |
| AppRole secret ID expired | secret_id_ttl exceeded | Regenerate SecretID, consider secret_id_ttl=0 for non-expiring |
| Consul agent unhealthy | Network issue between nodes | Check firewall, ports 8300-8302, 8500, 8600 |
Next Steps
After building your Vault cluster:
- External Secrets Operator — Sync Vault secrets to Kubernetes Secrets automatically
- Vault as SSH CA — Sign SSH keys with Vault for zero-trust SSH access
- Transit Secrets Engine — Use Vault as an encryption-as-a-service for application data
- Sentinel Policies (Vault Enterprise) — Advanced policy enforcement with code
- Vault + Terraform — Manage Vault configuration as code
Resources
- HashiCorp Vault Documentation
- Vault Getting Started Tutorial
- AppRole Pull Authentication
- Vault Agent with Kubernetes
- Production Hardening Guide
Questions? Reach out in our community Discord!