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


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

  1. Central storage β€” one encrypted, access-controlled location for all secrets.
  2. Dynamic secrets β€” generate short-lived DB credentials on demand instead of shared static passwords (covered in vault-secrets).
  3. Fine-grained ACLs β€” which identity can read which path (see rbac-basics).
  4. Full audit log β€” every read/write is recorded and tamper-evident.
  5. 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:

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:

  1. IPC_LOCK lets Vault lock its memory β€” the master key never swaps to disk where an attacker could scavenge it.
  2. Port binds to 127.0.0.1, so only local users or your reverse proxy reach Vault; it's never directly internet-exposed.
  3. ./file holds 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.17 for stability or hashicorp/vault:1.17.4 for 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:

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

  1. Never run dev mode outside throwaway experiments.
  2. TLS everywhere β€” terminate at Vault itself in production, not just the proxy.
  3. Distribute key shares physically separated; treat the root token as radioactive.
  4. Don't automate Shamir unsealing on the same host β€” it defeats the model. If you need auto-unseal, use KMS/HSM.
  5. Back up /vault/file (it's ciphertext) and test restore into a scratch container.
  6. Upgrade path: pin the image tag, read Vault release notes, snapshot raft data before upgrading.
  7. Enable audit logging early β€” covered alongside RBAC in rbac-basics.

πŸ”— Related


πŸ“ Change Log

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse