Vault Introduction - HashiCorp Vault Basics
Status: Active
Last Updated: 2026-08-15
Category: Security - Phase 4: Secret & Access Management
Prerequisites: docker-basics, docker-compose-intro, secrets
Time: 2-3 hours
Tags: vault, secrets, hashicorp, docker-compose, shamir, unseal, encryption
Summary
Hardcoded API keys in .env files and plaintext passwords in config files are the single most common self-inflicted security wound. HashiCorp Vault solves this by becoming the single, audited, encrypted source of truth for every secret your infrastructure uses. This lesson explains why Vault exists, how its architecture works (storage backend, barrier, unseal keys), and walks you through a production-shaped docker-compose deployment including initialization, unsealing, and your first secret. Updated for Vault 1.16+/1.17+ with current best practices.
π― What You'll Learn
- β
Explain the problems Vault solves vs dotenv/
.envfiles - β Describe Vault's architecture: storage backend, security barrier, seal/unseal
- β Deploy Vault with docker-compose using a file or integrated Raft storage backend
- β Initialize Vault and securely distribute Shamir unseal key shares
- β Unseal Vault after a restart and understand why it seals on reboot
- β Create your first KV secret and read it back via CLI and HTTP API
- β Understand when to use dev mode vs a real deployment
Why Vault?
The Problem With Files
Most homelabs and small teams start with secrets in files:
.env # DB password, API keys
docker-compose.yml # inline environment vars
~/.aws/credentials # cloud keys
nginx.conf # TLS key paths
Each file is a separate risk:
| Problem | Consequence |
|---|---|
| Secrets copied to every host | More breach surface; no central revocation |
| No access control granularity | Anyone with server access reads everything |
| No audit trail | "Who read the prod DB password?" β nobody knows |
| Rotation = manual edits everywhere | Rotation never happens |
| Secrets leak into backups, git, images | Permanent compromise |
What Vault Gives You
- Central storage β one encrypted, access-controlled location for all secrets.
- Dynamic secrets β generate short-lived DB credentials on demand instead of shared static passwords (covered in vault-secrets).
- Fine-grained ACLs β which identity can read which path (see rbac-basics).
- Full audit log β every read/write is recorded and tamper-evident.
- Programmatic access β apps fetch secrets at runtime via API/token instead of files baked into images.
Rule: Secrets never live in code, never live in Git, never live in container images. They live in Vault, and things authenticate to Vault to get them.
When NOT to Use Vault Yet
If you have three containers and one admin, dotenvx + encrypted files (sysadmin/dotenvx) is fine. Reach for Vault when you have: multiple services needing shared secrets, multiple humans/admins, automation that needs credentials, or any compliance requirement.
Vault Architecture
The Core Mental Model
βββββββββββββββββββββββββββββββββββββββββββββββ
β Vault Server β
β β
β βββββββββββββββββββββββββββββββββββββββ β
β β Security Barrier β β
β β (AES-256-GCM encryption) β β
β β β β ββββββββββββββββ
β β βββββββββββββ βββββββββββββββββ β ββββΆβ Storage β
β β β Secret β β Auth Methods β β β β Backend β
β β β Engines β β (token,LDAPβ¦) β β β β (Raft file / β
β β βββββββββββββ βββββββββββββββββ β β β Consul etc.)β
β β βββββββββββββ βββββββββββββββββ β β ββββββββββββββββ
β β β Audit β β Policy β β β
β β β Devices β β Engine β β β
β β βββββββββββββ βββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββ β
β β² only exists in memory β² β
βββββββββββ΄ββββββββββββββββββββββββ΄ββββββββββββ
Unseal Keys (Shamir)
Key concepts:
- Storage backend: durable persistence for Vault's data. Critically, everything written there is encrypted by the barrier first β the backend stores only ciphertext.
- Security barrier: Vault encrypts all data at rest with a master key. The master key itself is encrypted by the unseal keys.
- Sealed state: at startup, Vault knows nothing. Data is inaccessible until enough unseal key shares are provided to reconstruct the master key.
- Unsealing: entering the threshold of key shares decrypts the master key into memory only. A restart wipes it β you must unseal again. This is deliberate: a stolen disk alone is useless.
Shamir's Secret Sharing
Vault doesn't store one master key copy. At init, it splits the master key into N shares (default 5) requiring K of them (default 3) to reconstruct:
Master Key ββsplitβββΆ Share1 (person A)
Share2 (person B)
Share3 (safe deposit box)
Share4 (encrypted USB, offsite)
Share5 (sealed envelope, office safe)
Unseal needs ANY 3 of the 5.
Why: no single person (or single compromised machine) can unseal Vault alone. For a homelab you can lower this (-key-shares=1 -key-threshold=1) for convenience, but understand what you're trading away.
Secret Engines
Vault is not just a big encrypted map β it mounts multiple engines at paths:
| Path | Engine | Purpose |
|---|---|---|
secret/ |
KV v2 | Static key/value secrets |
database/ |
Database | Dynamic, short-lived DB creds |
pki/ |
PKI | Issue X.509 certificates (see certificate-fundamentals) |
transit/ |
Transit | Encryption-as-a-service |
ssh/ |
SSH | Signed SSH certificates |
kv/ |
KV v1/v2 | Versioned key/value storage |
identity/ |
Identity | Entity and group management |
This course covers KV in vault-secrets; auth methods get their own treatment in vault-authentication.
Vault 1.16+: Improved secret engine performance and new plugin support. Check HashiCorp Vault releases for current version features.
Installation with Docker Compose
Dev Mode (Never Production!)
docker run --rm -it -p 8200:8200 --cap-add=IPC_LOCK \
-e VAULT_DEV_ROOT_TOKEN_ID=myroot \
-e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
hashicorp/vault:1.17 dev
Dev mode: runs unsealed, in-memory storage, one static root token. Perfect for learning the CLI, useless for anything real β everything is lost on restart.
Real Single-Node Deployment
Create the directory layout:
mkdir -p ~/vault/{config,file} && cd ~/vault
~/vault/config/config.hcl β the Vault server configuration:
ui = true
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = true # acceptable ONLY behind a private network/TLS proxy;
# production must set tls_cert_file/tls_key_file here
}
storage "raft" {
path = "/vault/file"
node_id = "vault-1"
}
api_addr = "http://127.0.0.1:8200"
cluster_addr = "https://127.0.0.1:8201"
What Happens: We use the integrated Raft storage backend so Vault persists its own (encrypted) state in /vault/file β no external Consul needed for a single node, and Raft gives you a clean upgrade path to HA clustering later. tls_disable=true assumes a reverse proxy terminates TLS; if exposing Vault directly, configure TLS here instead (see tls-configuration).
~/vault/docker-compose.yml:
services:
vault:
image: hashicorp/vault:1.17
container_name: vault
cap_add:
- IPC_LOCK # allows mlock() to prevent memory being swapped to disk
ports:
- "127.0.0.1:8200:8200" # bind loopback only; proxy in front for remote access
environment:
VAULT_ADDR: http://127.0.0.1:8200
volumes:
- ./config:/vault/config:ro
- ./file:/vault/file
restart: unless-stopped
What Happens:
IPC_LOCKlets Vault lock its memory β the master key never swaps to disk where an attacker could scavenge it.- Port binds to
127.0.0.1, so only local users or your reverse proxy reach Vault; it's never directly internet-exposed. ./fileholds the sealed ciphertext. Back it up, but note: backups are unreadable while sealed AND without your unseal keys.
Start and verify:
docker compose up -d
docker logs vault # expect: "Vault is sealed" ... core: sealed, not initialized
curl -s http://127.0.0.1:8200/v1/sys/health | jq
Expected health output:
{
"initialized": false,
"sealed": true,
"standby": false,
"version": "1.17.x"
}
Version Note: As of 2025, Vault 1.17.x is the current stable release. Vault 1.15 reached end of standard maintenance. Pin to
hashicorp/vault:1.17for stability orhashicorp/vault:1.17.4for a specific patch. Check HashiCorp Vault Docker Hub for latest tags.
Initialization
Initialize exactly once, ever:
export VAULT_ADDR=http://127.0.0.1:8200
docker exec -e VAULT_ADDR=$VAULT_ADDR vault vault operator init \
-key-shares=5 -key-threshold=3 \
> vault-init.json
chmod 600 vault-init.json
cat vault-init.json
Output (abbreviated):
Unseal Key 1: aBc123...xyz
Unseal Key 2: dEf456...uvw
Unseal Key 3: ghi789...rst
Unseal Key 4: jkl012...opq
Unseal Key 5: mno345...lmn
Initial Root Token: hvs.XXXXXXXXXXXXXXXXXXXXXX
β οΈ These values will NEVER be shown again. Distribution checklist:
- Print the 5 unseal keys; store each share separately (two different safes/offsite locations, trusted co-founder, bank box). Never all five in the same place, never in a password manager synced to one account.
- Store
vault-init.jsonoffline (encrypted USB), then delete it from the server:
shred -u vault-init.json # wipe after copying offline
The root token is your break-glass credential: store it like the keys, and don't use it day-to-day (create scoped tokens/policies later).
Unsealing
Every Vault restart starts sealed. Unseal with any 3 distinct shares:
docker exec -it vault vault operator unseal
# paste Unseal Key 1 β "Sealed: true" still
docker exec -it vault vault operator unseal
# paste Unseal Key 2 β progress 2/3
docker exec -it vault vault operator unseal
# paste Unseal Key 3 β "Sealed: false"
What Happens: each share partially reconstructs the master key in memory. At the threshold (3), the barrier unlocks and Vault can read/write its storage. Shares are never persisted server-side.
For automation-friendly unsealing in a trusted single-admin lab, some use -key-shares=1 -key-threshold=1; enterprises instead use auto-unseal via cloud KMS (AWS/GCP/Azure) or HSM β out of scope here but know it exists before scaling out.
First Secret
Enable the KV v2 engine and write a secret:
export VAULT_TOKEN=hvs.XXXX # root token from init
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault secrets enable -path=secret kv-v2
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault kv put secret/demo/postgres username=fogadmin password='S3cur3-Passw0rd!'
What Happens: kv put secret/demo/postgres writes a versioned secret at secret/data/demo/postgres (KV v2 inserts a data/ segment automatically and keeps history β old versions remain readable, which is great for rollback, terrible if someone once wrote a real password they now want gone; use vault kv destroy for that).
Read it back:
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault kv get secret/demo/postgres
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault kv get -field=password secret/demo/postgres
Same thing via raw HTTP API (this is what applications do):
curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
http://127.0.0.1:8200/v1/secret/data/demo/postgres | jq '.data.data'
Then delete the demo secret and move on to real auth patterns:
docker exec -e VAULT_ADDR=$VAULT_ADDR -e VAULT_TOKEN=$VAULT_TOKEN vault \
vault kv delete secret/demo/postgres
Troubleshooting & Common Issues
| Symptom | Cause | Fix |
|---|---|---|
Vault is sealed on every request |
Restarted container | Unseal again (3 shares) |
permission denied writing secrets |
Root policy restrictions / wrong token | Confirm VAULT_TOKEN is valid; check vault token lookup |
Container exits immediately, mlock error |
Missing IPC_LOCK capability |
Add cap_add: [IPC_LOCK] to compose |
connection refused from another host |
Port bound to loopback | Intentional β go through reverse proxy with TLS |
| Lost unseal keys AND sealed | Barrier permanently locked | Nothing recovers data; restore from backup + re-init. This is why share distribution matters |
| UI shows blank page behind proxy | Websocket/path rewriting issue | Proxy / to :8200 untouched; set correct Host header |
Check state quickly:
docker exec -e VAULT_ADDR=http://127.0.0.1:8200 vault vault status
Security Best Practices
- Never run dev mode outside throwaway experiments.
- TLS everywhere β terminate at Vault itself in production, not just the proxy.
- Distribute key shares physically separated; treat the root token as radioactive.
- Don't automate Shamir unsealing on the same host β it defeats the model. If you need auto-unseal, use KMS/HSM.
- Back up
/vault/file(it's ciphertext) and test restore into a scratch container. - Upgrade path: pin the image tag, read Vault release notes, snapshot raft data before upgrading.
- Enable audit logging early β covered alongside RBAC in rbac-basics.
π Related
- Next: vault-authentication β tokens, AppRole, LDAP, Kubernetes auth
- Then: vault-secrets β KV engines, dynamic database credentials
- rbac-basics β policies and least privilege inside Vault
- certificate-fundamentals β TLS for the listener
- ../infrastructure/secrets-in-iac β consuming Vault from Terraform/Ansible
- ../containers/docker-volumes β backing up the raft directory safely
π Change Log
- 2026-08-14 β Initial lesson created as part of KB course build-out (security Phase 4).
- 2026-08-15 β Updated for Vault 1.17: updated Docker image tags, version references, health check output, and added version note with Docker Hub link.