Every application needs secrets: database passwords, API keys, TLS certificates, cloud credentials. The default approach — hardcoding them in config files, storing them in environment variables baked into Docker images, or passing them through CI/CD environment variables — works until it doesn’t. Secrets leak into git history, get committed in .env files, show up in build logs, and outlive their usefulness by years.
HashiCorp Vault solves this systematically. It’s a secrets engine, a certificate authority, an encryption service, and an identity broker — all in one. This guide covers running Vault in production, the secrets engines you’ll use most, authenticating your workloads, and integrating with Kubernetes.
Core Concepts
Before diving into configuration, understand Vault’s model:
Secrets Engines are plugins mounted at paths. Each engine manages a different type of secret. The KV engine stores static key-value pairs. The database engine generates dynamic credentials. The PKI engine issues TLS certificates. The AWS engine creates ephemeral IAM credentials. You mount engines at paths like /secret, /database, /pki and interact with them through those paths.
Auth Methods are how clients prove their identity to Vault. AppRole uses a role ID + secret ID pair (for automated systems). Kubernetes auth uses the pod’s service account JWT. GitHub auth uses personal access tokens. Once authenticated, Vault issues a token with attached policies.
Policies are HCL or JSON documents that define what a token can do. They’re path-based ACLs: path "secret/data/myapp/*" { capabilities = ["read"] } gives read access to everything under that path.
Leases are the mechanism behind dynamic secrets. Every secret Vault issues has a lease — a TTL after which the secret is revoked. Dynamic database credentials exist only for their lease duration. If your application is compromised and the credentials stolen, they expire on their own.
The Seal protects Vault’s encryption keys. On startup, Vault is sealed (encrypted, unreadable). Unsealing requires key shares (Shamir’s Secret Sharing by default) or auto-unseal via a cloud KMS. A sealed Vault serves no requests.
Deploying Vault
Development Mode (Testing Only)
1
2
3
4
5
6
7
8
|
# Start Vault in dev mode — in-memory, auto-unsealed, root token printed
vault server -dev
# Export the address and root token (printed on startup)
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxxxxxx'
vault status
|
Dev mode is for learning. It’s in-memory (all data lost on restart), uses HTTP, and starts with a root token. Never use it for anything real.
Production Deployment with Docker Compose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
# docker-compose.yml
services:
vault:
image: hashicorp/vault:1.17
ports:
- "8200:8200"
volumes:
- ./vault/config:/vault/config
- ./vault/data:/vault/data
- ./vault/logs:/vault/logs
cap_add:
- IPC_LOCK # allows Vault to lock memory (prevents secrets swapping to disk)
command: vault server -config=/vault/config/vault.hcl
environment:
VAULT_ADDR: http://127.0.0.1:8200
volumes: {}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
# vault/config/vault.hcl
ui = true
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = false
tls_cert_file = "/vault/config/tls/vault.crt"
tls_key_file = "/vault/config/tls/vault.key"
}
storage "file" {
path = "/vault/data"
}
# For production: use Raft integrated storage instead
# storage "raft" {
# path = "/vault/data"
# node_id = "vault-1"
# }
api_addr = "https://vault.example.com:8200"
cluster_addr = "https://vault.example.com:8201"
# Auto-unseal with AWS KMS (production recommendation)
# seal "awskms" {
# region = "us-east-1"
# kms_key_id = "arn:aws:kms:us-east-1:123456789:key/xxx"
# }
|
Initializing and Unsealing
On first start, Vault must be initialized — this generates the master key and splits it into shares:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
export VAULT_ADDR='https://vault.example.com:8200'
# Initialize with 5 key shares, requiring 3 to unseal (default)
vault operator init -key-shares=5 -key-threshold=3
# Output (SAVE THESE SECURELY):
# Unseal Key 1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Unseal Key 2: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Unseal Key 3: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Unseal Key 4: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Unseal Key 5: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Initial Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx
# Unseal (run 3 times with 3 different keys)
vault operator unseal # paste key 1
vault operator unseal # paste key 2
vault operator unseal # paste key 3
vault status
# Sealed: false
|
Treat the root token and unseal keys like the master keys to your infrastructure. Store them in separate secure locations (password managers, printed copies in safes, HSMs). The root token should be used only for initial setup, then revoked:
1
2
3
4
5
|
# After initial setup, revoke the root token
vault token revoke "$VAULT_TOKEN"
# Generate a new root token only when needed for emergency admin
vault operator generate-root -init
|
The KV Secrets Engine
The KV (Key-Value) engine is Vault’s static secret store. Version 2 adds versioning and soft-delete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
# Enable KV v2 at the "secret/" path
vault secrets enable -path=secret kv-v2
# Write a secret
vault kv put secret/myapp/database \
username=myapp \
password=s3cr3t123 \
host=db.internal
# Read it back
vault kv get secret/myapp/database
# ====== Secret Path ======
# secret/data/myapp/database
# ======= Metadata =======
# created_time 2026-03-26T10:00:00Z
# version 1
# ======== Data ========
# Key Value
# host db.internal
# password s3cr3t123
# username myapp
# Get just one field
vault kv get -field=password secret/myapp/database
# Update (creates version 2)
vault kv put secret/myapp/database \
username=myapp \
password=n3wp4ssw0rd \
host=db.internal
# List versions
vault kv metadata get secret/myapp/database
# Get a specific version
vault kv get -version=1 secret/myapp/database
# Delete current version (soft delete — can be undeleted)
vault kv delete secret/myapp/database
# Permanently destroy a version
vault kv destroy -versions=1 secret/myapp/database
|
Dynamic Secrets: The Key Differentiator
Static secrets (KV) are better than a .env file, but dynamic secrets are where Vault shines. Instead of storing a long-lived database password, Vault generates a unique credential for each requester, with a short TTL. When the lease expires, Vault revokes the credentials.
PostgreSQL Dynamic Credentials
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# Enable the database secrets engine
vault secrets enable database
# Configure the PostgreSQL connection
vault write database/config/myapp-postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="myapp-readonly,myapp-readwrite" \
connection_url="postgresql://{{username}}:{{password}}@db.internal:5432/myapp" \
username="vault_admin" \
password="vaultadminpassword"
# Create a role — defines the SQL to run when creating 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}}\";
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";
" \
default_ttl="1h" \
max_ttl="24h"
vault write database/roles/myapp-readwrite \
db_name=myapp-postgres \
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}}\";
" \
default_ttl="1h" \
max_ttl="8h"
|
Now generate credentials on demand:
1
2
3
4
5
6
7
|
vault read database/creds/myapp-readonly
# Key Value
# lease_id database/creds/myapp-readonly/AbCdEf123456
# lease_duration 1h
# lease_renewable true
# password A1b2C3d4E5f6G7h8
# username v-token-myapp-ro-AbCdEf123456
|
A unique user v-token-myapp-ro-AbCdEf123456 was created in PostgreSQL, valid for 1 hour. When the lease expires, Vault drops the role. If your app server is compromised and the credentials extracted, they stop working in at most one hour.
1
2
3
4
5
6
7
8
|
# Renew a lease (extend TTL within max_ttl)
vault lease renew database/creds/myapp-readonly/AbCdEf123456
# Revoke immediately (e.g., on incident response)
vault lease revoke database/creds/myapp-readonly/AbCdEf123456
# Revoke ALL leases for a role
vault lease revoke -prefix database/creds/myapp-readonly
|
AWS Dynamic Credentials
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
vault secrets enable aws
vault write aws/config/root \
access_key=AKIAIOSFODNN7EXAMPLE \
secret_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
region=us-east-1
vault write aws/roles/myapp-s3 \
credential_type=iam_user \
policy_document=-<<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::myapp-bucket/*"
}]
}
EOF
# Generate credentials
vault read aws/creds/myapp-s3
# access_key AKIAIOSFODNN7EXAMPLE
# secret_key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# security_token <nil>
# lease_duration 768h
|
The PKI Secrets Engine
Vault can act as a Certificate Authority — issuing TLS certificates with short TTLs on demand. No more manually managing certificate renewals; every service requests a cert when it starts and Vault issues one valid for 24 hours.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
# Enable the PKI engine for the root CA
vault secrets enable -path=pki pki
vault secrets tune -max-lease-ttl=87600h pki # 10 years for root CA
# Generate the root CA certificate
vault write -field=certificate pki/root/generate/internal \
common_name="LunarOps Root CA" \
issuer_name="root-2026" \
ttl=87600h > /tmp/root_ca.crt
# Configure CRL and issuer URLs
vault write pki/config/urls \
issuing_certificates="https://vault.internal:8200/v1/pki/ca" \
crl_distribution_points="https://vault.internal:8200/v1/pki/crl"
# Enable an intermediate CA (best practice — don't issue end-entity certs from root)
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int # 5 years for intermediate
# Generate a CSR for the intermediate CA
vault write -format=json pki_int/intermediate/generate/internal \
common_name="LunarOps Intermediate CA" \
issuer_name="lunarops-intermediate" \
| jq -r '.data.csr' > /tmp/pki_int.csr
# Sign the intermediate CSR with the root CA
vault write -format=json pki/root/sign-intermediate \
issuer_ref="root-2026" \
csr=@/tmp/pki_int.csr \
format=pem_bundle \
ttl=43800h \
| jq -r '.data.certificate' > /tmp/intermediate.cert.pem
# Import the signed certificate back into the intermediate CA
vault write pki_int/intermediate/set-signed \
certificate=@/tmp/intermediate.cert.pem
vault write pki_int/config/urls \
issuing_certificates="https://vault.internal:8200/v1/pki_int/ca" \
crl_distribution_points="https://vault.internal:8200/v1/pki_int/crl"
# Create a role — defines what certificates can be issued
vault write pki_int/roles/lunarops-dot-internal \
issuer_ref="lunarops-intermediate" \
allowed_domains="internal,lunarops.internal" \
allow_subdomains=true \
allow_bare_domains=false \
max_ttl="720h" # 30 days max
# Issue a certificate
vault write pki_int/issue/lunarops-dot-internal \
common_name="myapp.lunarops.internal" \
ttl="24h"
# certificate -----BEGIN CERTIFICATE-----...
# issuing_ca -----BEGIN CERTIFICATE-----...
# private_key -----BEGIN RSA PRIVATE KEY-----...
# serial_number xx:xx:xx:xx:xx
|
Applications can now request a fresh cert on startup — no cert rotation cronjobs, no expiry surprises.
Auth Methods
AppRole: Machine-to-Machine Authentication
AppRole is designed for automated systems (CI/CD pipelines, VM startup scripts, containers):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# Enable AppRole auth
vault auth enable approle
# Create a policy for myapp
vault policy write myapp-policy - <<EOF
path "secret/data/myapp/*" {
capabilities = ["read"]
}
path "database/creds/myapp-readonly" {
capabilities = ["read"]
}
path "pki_int/issue/lunarops-dot-internal" {
capabilities = ["create", "update"]
}
EOF
# Create an AppRole bound to that policy
vault write auth/approle/role/myapp \
token_policies="myapp-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=24h \
secret_id_num_uses=10 # each secret_id can be used 10 times
# Get the role ID (not secret — can be stored in config)
vault read auth/approle/role/myapp/role-id
# role_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Generate a secret ID (treat as a password — deliver securely to the app)
vault write -f auth/approle/role/myapp/secret-id
# secret_id: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
# secret_id_accessor: zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz
|
In your application:
1
2
3
4
5
6
|
# Authenticate and get a token
vault write auth/approle/login \
role_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
secret_id="yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
# token: hvs.CAESXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# token_duration: 1h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import hvac # pip install hvac
client = hvac.Client(url='https://vault.internal:8200')
# Authenticate with AppRole
client.auth.approle.login(
role_id='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
secret_id='yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy',
)
# Read a static secret
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/database',
mount_point='secret',
)
db_password = secret['data']['data']['password']
# Get dynamic database credentials
creds = client.read('database/creds/myapp-readonly')
username = creds['data']['username']
password = creds['data']['password']
lease_id = creds['lease_id']
# Renew the token before it expires
client.auth.token.renew_self()
|
Kubernetes Auth: Pod-Native Authentication
When your workloads run in Kubernetes, each pod already has a service account JWT. Vault’s Kubernetes auth method validates these JWTs directly — no secrets to distribute:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Enable Kubernetes auth
vault auth enable kubernetes
# Configure it (run from inside a pod, or use the cluster's API server URL)
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
# Create a role binding a Kubernetes service account to a Vault policy
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=production \
policies=myapp-policy \
ttl=1h
|
In your pod, authenticate using the service account token:
1
2
3
4
5
6
7
|
# From inside a pod
JWT=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
vault write auth/kubernetes/login \
role=myapp \
jwt="$JWT"
# token: hvs.CAESXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
1
2
3
4
5
6
7
8
9
10
11
12
|
import hvac
with open('/var/run/secrets/kubernetes.io/serviceaccount/token') as f:
jwt = f.read()
client = hvac.Client(url='https://vault.internal:8200')
client.auth.kubernetes.login(role='myapp', jwt=jwt)
# Now read secrets
secret = client.secrets.kv.v2.read_secret_version(
path='myapp/config', mount_point='secret'
)
|
Vault Agent: Sidecar for Secret Injection
Vault Agent runs alongside your application, handles authentication and token renewal automatically, and renders secrets into files or environment variables. Your application just reads a file — no Vault SDK needed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# /etc/vault-agent/config.hcl
vault {
address = "https://vault.internal:8200"
}
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "myapp"
}
}
sink "file" {
config = {
path = "/tmp/.vault-token"
}
}
}
# Render a template to a file
template {
source = "/etc/vault-agent/templates/database.ctmpl"
destination = "/secrets/database.env"
perms = "0640"
command = "pkill -HUP myapp" # signal app to reload config on change
}
template {
source = "/etc/vault-agent/templates/app-config.ctmpl"
destination = "/secrets/config.yaml"
}
|
{{/* /etc/vault-agent/templates/database.ctmpl */}}
{{ with secret "database/creds/myapp-readonly" }}
DB_USERNAME={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{ end }}
{{/* /etc/vault-agent/templates/app-config.ctmpl */}}
{{ with secret "secret/data/myapp/config" }}
api_key: {{ .Data.data.api_key }}
webhook_secret: {{ .Data.data.webhook_secret }}
{{ end }}
In Kubernetes, run Vault Agent as a sidecar:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
template:
spec:
serviceAccountName: myapp
initContainers:
# Init container renders secrets before the main app starts
- name: vault-agent-init
image: hashicorp/vault:1.17
args: ["agent", "-config=/etc/vault-agent/config.hcl", "-exit-after-auth"]
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault-agent
- name: secrets
mountPath: /secrets
containers:
- name: myapp
image: myapp:latest
env:
- name: SECRET_DIR
value: /secrets
volumeMounts:
- name: secrets
mountPath: /secrets
# Sidecar keeps secrets fresh (renews leases, re-renders templates)
- name: vault-agent
image: hashicorp/vault:1.17
args: ["agent", "-config=/etc/vault-agent/config.hcl"]
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault-agent
- name: secrets
mountPath: /secrets
volumes:
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: secrets
emptyDir:
medium: Memory # in-memory — not persisted to disk
|
Vault Secrets Operator for Kubernetes
The Vault Secrets Operator (VSO) is the modern Kubernetes-native approach. It syncs Vault secrets into native Kubernetes Secrets, which your pods consume normally:
1
2
3
4
5
|
# Install via Helm
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
# VaultAuth — how the operator authenticates to Vault
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: default
namespace: production
spec:
method: kubernetes
mount: kubernetes
kubernetes:
role: myapp
serviceAccount: myapp
---
# VaultStaticSecret — syncs a KV secret to a Kubernetes Secret
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: myapp-config
namespace: production
spec:
type: kv-v2
mount: secret
path: myapp/config
destination:
name: myapp-config # creates/updates this Kubernetes Secret
create: true
refreshAfter: 30s # re-sync every 30 seconds
vaultAuthRef: default
---
# VaultDynamicSecret — generates dynamic DB creds and keeps them fresh
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: myapp-db-creds
namespace: production
spec:
mount: database
path: creds/myapp-readonly
destination:
name: myapp-db-creds
create: true
renewalPercent: 67 # renew when 67% of TTL has elapsed
vaultAuthRef: default
|
Your pod then uses these as standard Kubernetes Secrets — no Vault SDK, no sidecar:
1
2
3
4
5
6
7
|
containers:
- name: myapp
envFrom:
- secretRef:
name: myapp-config
- secretRef:
name: myapp-db-creds
|
Audit Logging
Every request to Vault is auditable. Enable an audit log:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# Log to a file
vault audit enable file file_path=/vault/logs/audit.log
# Log to syslog
vault audit enable syslog
# List enabled audit devices
vault audit list
# The audit log records every request and response (with secrets HMAC'd)
tail -f /vault/logs/audit.log | jq .
# {
# "time": "2026-03-26T10:00:00Z",
# "type": "request",
# "auth": { "token_type": "service", "display_name": "approle-myapp" },
# "request": {
# "operation": "read",
# "path": "database/creds/myapp-readonly"
# }
# }
|
Policies: Least Privilege in Practice
A well-structured policy grants the minimum necessary access:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# /etc/vault/policies/myapp.hcl
# Read application config
path "secret/data/myapp/config" {
capabilities = ["read"]
}
# Read all secrets under myapp/ namespace
path "secret/data/myapp/*" {
capabilities = ["read"]
}
# Read metadata (to check versions, not values)
path "secret/metadata/myapp/*" {
capabilities = ["list", "read"]
}
# Generate database credentials (read = generate new creds)
path "database/creds/myapp-readonly" {
capabilities = ["read"]
}
# Issue TLS certificates for the app's domain
path "pki_int/issue/lunarops-dot-internal" {
capabilities = ["create", "update"]
}
# Allow the token to renew itself
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Allow the token to look up its own properties
path "auth/token/lookup-self" {
capabilities = ["read"]
}
|
1
2
3
4
5
6
7
8
|
# Apply the policy
vault policy write myapp /etc/vault/policies/myapp.hcl
# Test what a token with this policy can do
vault token create -policy=myapp -ttl=1h
# Then test with that token:
VAULT_TOKEN=hvs.test vault kv get secret/myapp/config # should work
VAULT_TOKEN=hvs.test vault kv get secret/otherapp/config # should fail
|
Common Operational Tasks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# Seal Vault (emergency — stops all secret serving)
vault operator seal
# Check replication status (Enterprise)
vault operator replication status
# Rotate the encryption key (does not require new unseal keys)
vault operator rotate
# Generate a root token for emergency admin
vault operator generate-root -init
# Follow the prompts, provide 3 unseal keys
# Snapshot (Raft integrated storage)
vault operator raft snapshot save /backup/vault-snapshot-$(date +%Y%m%d).snap
# Restore from snapshot
vault operator raft snapshot restore /backup/vault-snapshot-20260326.snap
# List all mounts
vault secrets list
# List all auth methods
vault auth list
# Tune a mount's default TTL
vault secrets tune -default-lease-ttl=24h database/
|
Quick Reference
| Task |
Command |
| Write a secret |
vault kv put secret/myapp key=value |
| Read a secret |
vault kv get secret/myapp |
| Read one field |
vault kv get -field=key secret/myapp |
| List secrets |
vault kv list secret/myapp |
| Generate DB creds |
vault read database/creds/myrole |
| Issue a certificate |
vault write pki_int/issue/myrole common_name=host.internal |
| Create a policy |
vault policy write name policy.hcl |
| Create an AppRole |
vault write auth/approle/role/name token_policies=policy |
| Get role-id |
vault read auth/approle/role/name/role-id |
| Get secret-id |
vault write -f auth/approle/role/name/secret-id |
| AppRole login |
vault write auth/approle/login role_id=x secret_id=y |
| Revoke a lease |
vault lease revoke lease/id |
| Revoke all for path |
vault lease revoke -prefix database/creds/myrole |
| Seal Vault |
vault operator seal |
| Unseal Vault |
vault operator unseal (×3) |
| Vault status |
vault status |
Vault turns secrets management from “everyone has the database password in their .env file” into a controlled, audited, time-limited system. Dynamic secrets eliminate the class of incident where a credential leaked months ago gets used today — because those credentials don’t exist anymore. The PKI engine eliminates manual certificate management. And Kubernetes auth means your pods authenticate with their existing identity, with no secrets to inject at deployment time.
Start with the KV engine and AppRole auth — that alone is a massive improvement over environment variables. Then add dynamic database credentials, and you’ve eliminated your most sensitive long-lived secrets. The investment in setting up Vault pays back every time you don’t have to rotate a password after an incident.
Comments