Skip to content

HashiCorp Vault

Run a secrets management platform on Kubernetes and learn how applications authenticate to it.

Time: ~20 minutes Difficulty: Intermediate

  • Installing Vault on minikube via Helm (dev mode)
  • The KV v2 secrets engine for key-value storage
  • Vault policies for access control
  • Kubernetes auth method: pods authenticate using their ServiceAccount token
  • Reading secrets from inside a pod
  • The Vault web UI

The previous demo showed that Kubernetes Secrets are base64 encoded, not encrypted. Anyone with namespace access can read them. Vault solves this with:

  • Encryption at rest and in transit
  • Fine-grained access policies
  • Audit logging (who read what, when)
  • Dynamic secrets (database credentials generated on demand)
  • Automatic rotation and lease management

Navigate to the demo directory:

Terminal window
cd demos/vault
Terminal window
kubectl apply -f manifests/namespace.yaml
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault \
--namespace vault-demo \
--set "server.dev.enabled=true" \
--set "server.dev.devRootToken=root" \
--set "injector.enabled=false"

Dev mode runs Vault unsealed with an in-memory backend. Never use this in production.

Wait for Vault to be ready:

Terminal window
kubectl get pods -n vault-demo -w
Terminal window
kubectl port-forward svc/vault 8200:8200 -n vault-demo

Open http://localhost:8200. Log in with token: root.

Terminal window
# Exec into the Vault pod
kubectl exec -it vault-0 -n vault-demo -- /bin/sh
# Inside the pod:
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=root
# Store database credentials
vault kv put secret/demo/database \
username=appuser \
password=s3cret-passw0rd \
host=postgres.example.com \
port=5432
# Store an API key
vault kv put secret/demo/api \
key=sk-live-abc123def456 \
provider=stripe
# Read them back
vault kv get secret/demo/database
vault kv get -field=password secret/demo/database
# List all secrets under demo/
vault kv list secret/demo/
exit

Policies control who can access which secrets.

Terminal window
kubectl exec -it vault-0 -n vault-demo -- /bin/sh
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=root
# Create a read-only policy
vault policy write app-readonly - <<'EOF'
path "secret/data/demo/*" {
capabilities = ["read", "list"]
}
EOF
# Create an admin policy
vault policy write app-admin - <<'EOF'
path "secret/data/demo/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/demo/*" {
capabilities = ["read", "list", "delete"]
}
EOF
# Verify
vault policy list
vault policy read app-readonly
exit

This lets pods authenticate to Vault using their ServiceAccount token.

Terminal window
kubectl exec -it vault-0 -n vault-demo -- /bin/sh
export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=root
# Enable the Kubernetes auth method
vault auth enable kubernetes
# Configure it to talk to the K8s API
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# Create a role that maps ServiceAccount "demo-app" to the "app-readonly" policy
vault write auth/kubernetes/role/demo-app \
bound_service_account_names=demo-app \
bound_service_account_namespaces=vault-demo \
policies=app-readonly \
ttl=1h
exit
Terminal window
kubectl apply -f manifests/test-app.yaml
Terminal window
kubectl exec -it deploy/demo-app -n vault-demo -- /bin/sh
# Install curl (for Vault API calls)
wget -qO /tmp/vault.zip https://releases.hashicorp.com/vault/1.15.4/vault_1.15.4_linux_amd64.zip 2>/dev/null
# Alternative: use the Vault API directly with wget
# Authenticate using the pod's ServiceAccount token
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
# Login to Vault
VAULT_TOKEN=$(wget -qO- --post-data "{\"jwt\": \"$SA_TOKEN\", \"role\": \"demo-app\"}" \
http://vault.vault-demo.svc:8200/v1/auth/kubernetes/login 2>/dev/null | \
sed 's/.*"client_token":"\([^"]*\)".*/\1/')
echo "Got Vault token: ${VAULT_TOKEN:0:10}..."
# Read the database secret
wget -qO- --header "X-Vault-Token: $VAULT_TOKEN" \
http://vault.vault-demo.svc:8200/v1/secret/data/demo/database 2>/dev/null
exit

The pod authenticates to Vault using its ServiceAccount token and receives a Vault token scoped to the app-readonly policy. It can read secrets but not modify them.

manifests/
namespace.yaml # vault-demo namespace
vault-policy.yaml # ConfigMap with policy HCL files (reference)
test-app.yaml # ServiceAccount + Deployment for testing
Installed via Helm:
vault-0 # Vault server pod (dev mode)

Authentication flow:

Pod (ServiceAccount: demo-app)
|
v sends SA JWT token
Vault K8s Auth Method
|
v validates with K8s API
Role mapping (demo-app -> app-readonly policy)
|
v returns Vault token
Pod reads secret/demo/* with Vault token
  1. Try writing a secret with the read-only token (should fail):

    Terminal window
    # From inside the demo-app pod, after authenticating:
    wget -qO- --method=POST \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --body-data '{"data":{"key":"value"}}' \
    http://vault.vault-demo.svc:8200/v1/secret/data/demo/hack
    # 403 Forbidden
  2. Check secret versioning in the Vault UI. Update a secret and see previous versions.

  3. Create a second role with app-admin policy and test write access.

Terminal window
helm uninstall vault -n vault-demo
kubectl delete namespace vault-demo

See docs/deep-dive.md for a detailed explanation of Vault architecture, unsealing, transit secrets engine, dynamic database credentials, PKI engine, and production deployment patterns.

Move on to External Secrets to learn how ESO syncs Vault secrets into native Kubernetes Secrets automatically.