JIT Privileged Access & Dynamic Secrets
Understanding Vault and why it matters
Vault manages all your passwords, API keys, and credentials. No more secrets in config files!
Without Vault:
With Vault: Credentials are generated on-demand, auto-expire, and every access is logged!
| Phase | Time | Difficulty |
|---|---|---|
| Understanding Concepts | 20-30 min | Reading |
| Vault Installation | 30-45 min | Easy |
| Basic Configuration | 30-45 min | Easy |
| Secrets Engines | 45-60 min | Medium |
| Authentik Integration | 30-45 min | Medium |
| JIT Access Setup | 45-60 min | Medium |
Total: 4-5 hours (can be split across multiple sessions)
Understanding Vault before we build
Vault has specific terminology. Understanding it makes everything easier!
| Static Secrets (KV) | Dynamic Secrets |
|---|---|
| You store the secret in Vault | Vault generates the secret on-demand |
| Example: API keys, certificates | Example: Database credentials, SSH keys |
| Doesn't expire automatically | Auto-expires after TTL (time-to-live) |
| Manual rotation needed | New credentials each request |
Instead of having permanent access to systems, you request access only when needed:
Traditional
Permanent admin password
Never expires
Shared by team
JIT Access
Request access when needed
Auto-expires in 1 hour
Unique per person
Instead of knowing the database password, you ask Vault: "I need database access for 1 hour." Vault creates a temporary user just for you, and deletes it when time expires!
What you need before starting
Make sure Project A is complete and working
Vault will authenticate users through Authentik. If Authentik isn't working, Vault SSO won't work either.
| Resource | Additional Required | Total with Project A |
|---|---|---|
| RAM | +1 GB | ~9 GB minimum |
| Storage | +5 GB | ~105 GB minimum |
| CPU | Same | 4 cores minimum |
You don't need these to complete the guide, but they help understand what's happening:
Adding Vault to your Docker stack
SSH into your server first, then run these commands in the ~/homelab-iam directory.
# Navigate to project directory
cd ~/homelab-iam
# Create Vault directories
mkdir -p vault/{config,data,logs,policies}
# Set permissions (Vault runs as user 100)
sudo chown -R 100:100 vault/data vault/logs
# Verify structure
ls -la vault/
cat > ~/homelab-iam/vault/config/vault-config.hcl << 'EOF'
# ============================================
# HASHICORP VAULT CONFIGURATION
# ============================================
# UI Settings
ui = true
# Listener (how clients connect)
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = true # TLS handled by Traefik
}
# Storage Backend (where secrets are stored)
storage "file" {
path = "/vault/data"
}
# API Address (for internal communication)
api_addr = "http://127.0.0.1:8200"
# Cluster Address
cluster_addr = "http://127.0.0.1:8201"
# Disable memory lock (for Docker)
disable_mlock = true
# Logging
log_level = "info"
log_file = "/vault/logs/vault.log"
EOF
We're using file storage for simplicity. For production, you'd use Consul, PostgreSQL, or integrated storage (Raft).
Add this service to your docker-compose.yml file:
# Add this to your docker-compose.yml services section
# Edit with: nano ~/homelab-iam/docker-compose.yml
# ==========================================
# HASHICORP VAULT - Secrets Management
# ==========================================
vault:
image: hashicorp/vault:1.15
container_name: vault
restart: unless-stopped
cap_add:
- IPC_LOCK
environment:
VAULT_ADDR: "http://127.0.0.1:8200"
VAULT_API_ADDR: "http://127.0.0.1:8200"
volumes:
- ./vault/config:/vault/config:ro
- ./vault/data:/vault/data
- ./vault/logs:/vault/logs
- ./vault/policies:/vault/policies:ro
command: server -config=/vault/config/vault-config.hcl
networks:
- traefik-public
- authentik-internal
labels:
- "traefik.enable=true"
- "traefik.http.routers.vault.rule=Host(\`vault.${DOMAIN}\`)"
- "traefik.http.routers.vault.entrypoints=websecure"
- "traefik.http.routers.vault.tls.certresolver=letsencrypt"
- "traefik.http.services.vault.loadbalancer.server.port=8200"
Add this in the services: section, after the other services. Make sure indentation matches (2 spaces).
cd ~/homelab-iam
# Pull the Vault image
docker compose pull vault
# Start Vault
docker compose up -d vault
# Check it's running
docker compose ps vault
# Check logs
docker logs vault
Vault starts "sealed" and needs to be initialized. This generates your master keys.
# Initialize Vault (generates keys)
docker exec vault vault operator init -key-shares=1 -key-threshold=1
# ⚠️ SAVE THE OUTPUT! You'll see:
# Unseal Key 1: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Initial Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Copy those values, then unseal with:
docker exec vault vault operator unseal YOUR_UNSEAL_KEY_HERE
# Check status (should show "Sealed: false")
docker exec vault vault status
Write down both the Unseal Key and Root Token immediately!
If you lose these, you lose access to ALL your secrets forever. Store them securely (password manager, safe, etc.).
You should see the Vault dashboard. Next, we'll configure secrets engines and authentication.
Setting up secrets engines and policies
Enable the secrets engines we'll use
# Login with root token
docker exec -it vault vault login
# When prompted, paste your Root Token
# You should see: "Success! You are now authenticated."
KV (Key-Value) engine stores static secrets like API keys and passwords.
# Enable KV secrets engine version 2
docker exec vault vault secrets enable -path=secret kv-v2
# Verify it's enabled
docker exec vault vault secrets list
# Create a test secret
docker exec vault vault kv put secret/test password="mysecretpassword" username="testuser"
# Read the secret back
docker exec vault vault kv get secret/test
Version 2 includes versioning (keep history of changes) and soft-delete (recover accidentally deleted secrets).
Policies control who can access what secrets. Let's create some basic ones.
# Create admin policy file
cat > ~/homelab-iam/vault/policies/admin-policy.hcl << 'EOF'
# Admin Policy - Full access to secrets
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
EOF
# Load the policy into Vault
docker exec vault vault policy write admin /vault/policies/admin-policy.hcl
# Create developer policy file
cat > ~/homelab-iam/vault/policies/developer-policy.hcl << 'EOF'
# Developer Policy - Read access to app secrets
path "secret/data/apps/*" {
capabilities = ["read", "list"]
}
path "secret/metadata/apps/*" {
capabilities = ["list"]
}
EOF
# Load the policy
docker exec vault vault policy write developer /vault/policies/developer-policy.hcl
# List all policies
docker exec vault vault policy list
Let's organize secrets by application:
# Store Grafana OAuth secret (from Project A)
docker exec vault vault kv put secret/apps/grafana \
oauth_client_id="grafana" \
oauth_client_secret="YOUR_GRAFANA_SECRET"
# Store a generic API key
docker exec vault vault kv put secret/apps/myapp \
api_key="sk-xxxxxxxxxxxxxxxxxxxx" \
api_url="https://api.example.com"
# Store database credentials
docker exec vault vault kv put secret/databases/postgres \
username="app_user" \
password="supersecretpassword" \
host="postgres" \
port="5432"
# List all secrets
docker exec vault vault kv list secret/apps
Vault is now storing secrets. Next, let's set up dynamic secrets and Authentik integration.
Auto-generated credentials that expire
Vault creates unique credentials on-demand that automatically expire!
After TTL expires → Vault automatically deletes the user from the database!
# Enable the database secrets engine
docker exec vault vault secrets enable database
# Verify it's enabled
docker exec vault vault secrets list
Tell Vault how to connect to your PostgreSQL database (from Project A):
# First, get your PostgreSQL password from .env
grep POSTGRES_PASSWORD ~/homelab-iam/.env
# Configure Vault to connect to PostgreSQL
docker exec vault vault write database/config/authentik-db \
plugin_name="postgresql-database-plugin" \
allowed_roles="readonly","readwrite" \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/authentik?sslmode=disable" \
username="authentik" \
password="YOUR_POSTGRES_PASSWORD"
Roles define what permissions the generated credentials have:
# Create a read-only database role
docker exec vault vault write database/roles/readonly \
db_name="authentik-db" \
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"
# Create a read-write database role
docker exec vault vault write database/roles/readwrite \
db_name="authentik-db" \
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}}\";" \
revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
default_ttl="1h" \
max_ttl="4h"
Generate temporary database credentials:
# Request read-only credentials
docker exec vault vault read database/creds/readonly
# You'll see output like:
# Key Value
# --- -----
# lease_id database/creds/readonly/xxxxx
# lease_duration 1h
# username v-root-readonly-xxxxx
# password xxxxxxxxxxxxxxxx
# Test the credentials work
docker exec postgres psql -U v-root-readonly-xxxxx -d authentik -c "SELECT 1;"
Every time you request credentials, you get a NEW unique username/password that auto-expires. No more shared passwords!
Request access only when you need it
No permanent admin access. Request it, use it, it expires automatically.
Policy that allows requesting database credentials with time limits:
# Create JIT policy
cat > ~/homelab-iam/vault/policies/jit-db-policy.hcl << 'EOF'
# JIT Database Access Policy
# Users can request temporary database credentials
# Allow reading database credentials
path "database/creds/readonly" {
capabilities = ["read"]
}
path "database/creds/readwrite" {
capabilities = ["read"]
}
# Allow users to check their own leases
path "sys/leases/lookup" {
capabilities = ["update"]
}
# Allow users to renew their leases
path "sys/leases/renew" {
capabilities = ["update"]
}
EOF
# Load the policy
docker exec vault vault policy write jit-db-access /vault/policies/jit-db-policy.hcl
# Create a token with JIT policy (expires in 8 hours)
docker exec vault vault token create \
-policy="jit-db-access" \
-ttl="8h" \
-display-name="jit-user-token"
# Save the token that's output!
Here's how a user would request JIT database access:
# Step 1: Login with the limited token
docker exec -it vault vault login YOUR_JIT_TOKEN_HERE
# Step 2: Request database credentials (expires in 1 hour)
docker exec vault vault read database/creds/readonly
# Step 3: Use the credentials for your work...
# Step 4: When done, you can manually revoke
docker exec vault vault lease revoke database/creds/readonly/LEASE_ID
# Or just wait - it expires automatically!
Traditional: "Here's the database password, don't share it" (everyone shares it)
JIT: "Request access when needed, get unique credentials that expire"
# Log back in as root first
docker exec -it vault vault login
# List all active database leases
docker exec vault vault list sys/leases/lookup/database/creds/readonly
# Get details on a specific lease
docker exec vault vault lease lookup database/creds/readonly/LEASE_ID
# Revoke all database credentials (emergency)
docker exec vault vault lease revoke -prefix database/creds/
Login to Vault using your Authentik account
Use your existing Authentik account to login to Vault!
| Field | Value |
|---|---|
| Name | Vault OIDC Provider |
| Authorization flow | default-provider-authorization-implicit-consent |
| Client type | Confidential |
| Client ID | vault |
| Redirect URIs | https://vault.yourdomain.com/ui/vault/auth/oidc/oidc/callback https://vault.yourdomain.com/oidc/callback |
| Scopes | openid, profile, email, groups |
| Field | Value |
|---|---|
| Name | Vault |
| Slug | vault |
| Provider | Vault OIDC Provider |
| Launch URL | https://vault.yourdomain.com |
# Login as root first
docker exec -it vault vault login
# Enable OIDC auth method
docker exec vault vault auth enable oidc
# Configure OIDC with Authentik
docker exec vault vault write auth/oidc/config \
oidc_discovery_url="https://authentik.yourdomain.com/application/o/vault/" \
oidc_client_id="vault" \
oidc_client_secret="YOUR_CLIENT_SECRET" \
default_role="default"
# Create default OIDC role
docker exec vault vault write auth/oidc/role/default \
bound_audiences="vault" \
allowed_redirect_uris="https://vault.yourdomain.com/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="https://vault.yourdomain.com/oidc/callback" \
user_claim="preferred_username" \
groups_claim="groups" \
policies="default,developer"
# Create admin OIDC role (for admins group)
docker exec vault vault write auth/oidc/role/admin \
bound_audiences="vault" \
allowed_redirect_uris="https://vault.yourdomain.com/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="https://vault.yourdomain.com/oidc/callback" \
user_claim="preferred_username" \
groups_claim="groups" \
bound_claims='{"groups": "admins"}' \
policies="default,admin"
Make sure to replace yourdomain.com and YOUR_CLIENT_SECRET with your actual values.
You can now login to Vault using your Authentik credentials. No more separate password!
Make sure everything works
Verify all components are working correctly
# Check Vault status
docker exec vault vault status
# List enabled secrets engines
docker exec vault vault secrets list
# List enabled auth methods
docker exec vault vault auth list
# List policies
docker exec vault vault policy list
# Check Vault logs
docker logs vault --tail=50
Cause: Vault seals itself after restart
Solution: Run the unseal command:
docker exec vault vault operator unseal YOUR_UNSEAL_KEY
Cause: Redirect URI mismatch or wrong client secret
Solution:
• Check redirect URIs match exactly in both Authentik and Vault
• Verify client secret is correct
• Check Authentik provider scopes include: openid, profile, email, groups
Cause: Policy doesn't allow the action
Solution:
• Check which policies are attached to your token
• Use root token for admin tasks
• Update policy to include needed paths
Cause: Database connection issue or wrong credentials
Solution:
• Verify PostgreSQL is running: docker compose ps postgres
• Check connection string has correct password
• Ensure database user has permission to create roles
Your Vault setup is complete!
Enterprise-grade secrets management with JIT access in your home lab!