Everything for Vault in one place — pick a section below. 30 reviewed items across 4 content types, plus a hands-on tutorial.
Quick Answer
Vault is a secrets management platform that stores passwords, API keys, certificates, and encryption keys in one secure place. It controls who can access what, generates short-lived credentials on demand, and logs every access for auditing.
Detailed Answer
Think of Vault as a bank vault for your digital secrets. A physical bank vault has a thick door, access logs, time locks, and safe deposit boxes that only authorized people can open. Vault provides the same controls for passwords, API keys, certificates, and encryption keys in your software systems. Without it, secrets end up scattered across config files, environment variables, CI/CD pipelines, and developer laptops, like cash left on random desks around an office building.
At its core, Vault is a centralized platform for managing secrets, built by HashiCorp. It gives you a single interface to manage sensitive data across any type of infrastructure. Instead of hardcoding database passwords in application configs or storing AWS keys in plain-text files, teams store everything in Vault and fetch it through its API. Vault handles two types of secrets: static secrets (key-value pairs you store yourself) and dynamic secrets (credentials it creates on the fly with automatic expiration). Dynamic secrets are a game-changer because each one is unique and short-lived.
Under the hood, Vault uses a seal/unseal system based on Shamir's Secret Sharing to protect its master encryption key. When Vault starts, it is sealed and cannot read any stored data. Operators must provide a minimum number of unseal keys (say 3 out of 5) to reconstruct the master key and unlock Vault. All secrets are encrypted with AES-256-GCM before being written to the storage backend, which can be Consul, Raft integrated storage, PostgreSQL, or other supported systems. Vault also keeps a full audit log of every request and response, giving you a complete record of who accessed what and when.
In production, Vault solves several big problems at once. First, it eliminates secret sprawl by being the single source of truth for all credentials. Second, dynamic secrets limit the damage from a breach because each credential is unique and expires quickly. Third, the lease and renewal system means credentials automatically expire, enforcing least-privilege access without manual work. Fourth, encryption as a service lets applications encrypt data without managing their own keys. Companies like Stripe, Adobe, and Shopify use Vault to manage millions of secrets across thousands of microservices.
The most common mistake teams make is treating Vault as just another key-value store. If you simply move your static passwords into Vault without adopting dynamic secrets, automated rotation, and proper policies, you have only centralized the problem without actually fixing it. Another big pitfall is not planning for the unsealing process during disaster recovery. Teams must distribute unseal keys securely and practice recovery regularly, because a sealed Vault means every application that depends on it loses access to its credentials at the same time.
Code Example
# Install Vault on Linux curl -fsSL https://releases.hashicorp.com/vault/1.15.4/vault_1.15.4_linux_amd64.zip -o vault.zip # Download Vault binary unzip vault.zip && sudo mv vault /usr/local/bin/ # Extract and install to PATH vault version # Verify installation # Start Vault in dev mode for local testing (NOT for production) vault server -dev -dev-root-token-id="dev-token-123" # Start dev server with known root token # Set environment variables for CLI access export VAULT_ADDR='http://127.0.0.1:8200' # Point CLI to Vault server address export VAULT_TOKEN='dev-token-123' # Authenticate CLI with root token # Store a secret for the payments-api service vault kv put secret/payments-api/db \ username="payments_svc" \ password="Pr0d$ecure!2024" \ host="prod-db-cluster.internal" # Write database credentials to KV store # Retrieve the secret programmatically vault kv get -format=json secret/payments-api/db # Fetch secret in JSON format for automation # Enable audit logging to track all secret access vault audit enable file file_path=/var/log/vault/audit.log # Enable file-based audit logging # Check Vault server status vault status # Show seal status, HA mode, and storage backend info
Interview Tip
A junior engineer typically describes Vault as just a place to store passwords, missing the bigger picture. In your interview, hit three main points: centralized secrets management, dynamic secret generation, and encryption as a service. Make it clear that Vault is not just a key-value store but an identity-based platform where access depends on trusted identities like Kubernetes service accounts, LDAP users, or cloud IAM roles, not static credentials. Bring up dynamic secrets as the standout feature because each application instance gets unique, short-lived credentials that are automatically revoked, which dramatically limits the damage from a breach. Give a real-world example, like database credential rotation that requires zero application changes when you use Vault's dynamic secrets engine.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ HashiCorp Vault │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ KV Secrets │ │ Dynamic │ │ Encryption │ │
│ │ Engine │ │ Secrets │ │ as a Svc │ │
│ └──────┬──────┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴────────────────┴──────┐ │
│ │ Policy Engine (ACL/Sentinel) │ │
│ └──────┬────────────────┬────────────────┬──────┘ │
│ │ │ │ │
│ ┌──────┴──────┐ ┌──────┴───────┐ ┌────┴───────┐ │
│ │ Token │ │ Kubernetes │ │ LDAP │ │
│ │ Auth │ │ Auth │ │ Auth │ │
│ └─────────────┘ └──────────────┘ └────────────┘ │
│ ↑ ↑ ↑ │
└─────────┼────────────────┼────────────────┼──────────┘
│ │ │
┌──────┴──────┐ ┌──────┴───────┐ ┌────┴───────┐
│ payments-api│ │ prod-cluster│ │ Engineers │
│ (service) │ │ (K8s pods) │ │ (humans) │
└─────────────┘ └──────────────┘ └────────────┘💬 Comments
Quick Answer
Secrets engines are pluggable modules in Vault, each mounted at a specific path. KV stores static secrets you manage yourself. Database, AWS, and PKI engines generate unique, short-lived credentials on demand, each tailored to a specific use case.
Detailed Answer
Think of secrets engines as specialized departments in a bank. The KV engine is the safe deposit box department where you store and retrieve items. The Database engine is a temporary badge office that prints access cards expiring after a shift. The AWS engine is a valet service creating temporary parking passes. The PKI engine is a notary office issuing certified documents. Each department has its own procedures, but they all follow the same bank's security policies.
Secrets engines in Vault are components that store, generate, or encrypt data. You enable them at specific paths (like secret/, database/, aws/, pki/) and each path is completely isolated from the others. The KV (Key-Value) engine comes in two versions: v1 offers simple storage, while v2 adds versioning, soft delete, and metadata tracking. The Database engine connects to real databases like PostgreSQL, MySQL, or MongoDB and creates unique credentials per request, each with a configurable time-to-live. The AWS engine generates IAM users, STS tokens, or assumed-role credentials on the fly. The PKI engine works as a full certificate authority, issuing X.509 certificates with parameters you define.
Under the hood, each secrets engine works like a virtual filesystem. When you request credentials from database/creds/payments-readonly, Vault routes the request to the Database engine, which connects to the configured database, creates a new user with the right permissions, and hands back the credentials along with a lease ID. This lease ID is important because Vault tracks all active leases and automatically revokes credentials when the lease expires. For a database credential, revocation means running a DROP USER or similar statement against the database. For PKI, requesting a certificate from pki/issue/web-server triggers certificate generation using the configured CA, and the certificate's validity period becomes its lease. Each engine keeps its own configuration, connections, and state independently.
In production, organizations typically mount multiple instances of the same engine for different environments and teams. For example, you might have database/prod/ and database/staging/ as separate mounts with different connection strings and policies. The Database engine is especially powerful for microservices because each pod gets unique credentials. If a pod is compromised, you revoke only that specific lease without touching other services. The AWS engine is invaluable for CI/CD pipelines where build jobs need temporary AWS access without storing long-lived IAM keys. The PKI engine eliminates the headache of managing certificates by hand, automating issuance, renewal, and revocation, often cutting certificate provisioning time from days to milliseconds.
The biggest mistake with secrets engines is getting lease TTLs wrong. Setting database credential TTLs too short causes connection storms as hundreds of pods simultaneously request new credentials. Setting them too long defeats the whole point of dynamic secrets. A common PKI mistake is exposing the root CA through Vault instead of keeping it offline and only mounting an intermediate CA. For KV v2, teams often forget that deleting a secret only soft-deletes it, and the data stays recoverable until you explicitly destroy the version. This matters for compliance when handling sensitive data like personally identifiable information.
Code Example
# Enable KV v2 secrets engine for application secrets
vault secrets enable -path=secret -version=2 kv # Mount KV v2 at secret/ path
# Store secrets for the payments-api service
vault kv put secret/payments-api/config \
stripe_key="sk_live_abc123" \
webhook_secret="whsec_xyz789" # Write multiple key-value pairs atomically
# Enable Database secrets engine for PostgreSQL
vault secrets enable -path=database database # Mount database engine at database/ path
# Configure PostgreSQL connection for prod-cluster
vault write database/config/prod-payments-db \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@prod-db.internal:5432/payments" \
allowed_roles="payments-readonly,payments-readwrite" \
username="vault_admin" \
password="VaultAdm!n2024" # Configure database connection with template credentials
# Create a role that generates read-only credentials
vault write database/roles/payments-readonly \
db_name=prod-payments-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h" # Define role with SQL template and 1-hour default lease
# Generate dynamic database credentials
vault read database/creds/payments-readonly # Creates a new PostgreSQL user with SELECT-only permissions
# Enable AWS secrets engine for CI/CD pipelines
vault secrets enable -path=aws aws # Mount AWS engine at aws/ path
# Configure AWS root credentials
vault write aws/config/root \
access_key="AKIA_VAULT_ROOT_KEY" \
secret_key="vault-root-secret-key" \
region="us-east-1" # Configure AWS IAM credentials Vault uses to create dynamic users
# Create an AWS role for deploy pipelines
vault write aws/roles/deploy-pipeline \
credential_type=iam_user \
policy_document=-<<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject","ecr:PushImage"],"Resource":"*"}]}
EOF # Define IAM policy for dynamic AWS users
# Enable PKI secrets engine for TLS certificates
vault secrets enable -path=pki pki # Mount PKI engine at pki/ path
# Generate internal root CA with 10-year validity
vault write pki/root/generate/internal \
common_name="prod-cluster-ca" \
ttl=87600h # Create root CA certificate valid for 10 years
# Create a role for issuing web server certificates
vault write pki/roles/web-server \
allowed_domains="internal.payments.com" \
allow_subdomains=true \
max_ttl="720h" # Allow issuing certs for *.internal.payments.com with 30-day max TTL
# Issue a TLS certificate for the payments API
vault write pki/issue/web-server \
common_name="api.internal.payments.com" \
ttl="72h" # Generate certificate and private key valid for 72 hoursInterview Tip
A junior engineer typically lists secrets engines by name without explaining the real difference between static and dynamic secrets. In your answer, draw a clear line: KV stores secrets you manage yourself, while Database, AWS, and PKI engines create unique, time-bound credentials on every request. Focus on the lease mechanism as the concept that ties all dynamic engines together. Every dynamically generated secret has a lease ID, a TTL, and an automatic revocation process. Walk through a concrete example like the Database engine creating a unique PostgreSQL user per pod, so if one pod is compromised, you revoke only that credential. Interviewers love hearing about the practical impact: dynamic secrets kill the shared-password problem and turn credential rotation from a stressful fire drill into a non-event.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐
│ Vault Server │
│ │
│ ┌────────────┐ ┌────────────┐ ┌─────────┐ ┌─────────┐ │
│ │ KV v2 │ │ Database │ │ AWS │ │ PKI │ │
│ │ Engine │ │ Engine │ │ Engine │ │ Engine │ │
│ │ secret/ │ │ database/ │ │ aws/ │ │ pki/ │ │
│ └─────┬──────┘ └─────┬──────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │ │
└────────┼──────────────┼─────────────┼───────────┼────────┘
│ │ │ │
↓ ↓ ↓ ↓
┌──────────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐
│ Static │ │PostgreSQL│ │ AWS IAM │ │ X.509 │
│ Secrets │ │ MySQL │ │ STS │ │ Certs │
│ (stored) │ │ MongoDB │ │ AssumeRol│ │ (gen) │
└──────────────┘ └────┬─────┘ └──────────┘ └─────────┘
│
↓
┌───────────────────┐
│ Dynamic Creds │
│ ┌─────────────┐ │
│ │ Lease: 1h │ │
│ │ Auto-Revoke │ │
│ └─────────────┘ │
└───────────────────┘💬 Comments
Quick Answer
Vault uses pluggable auth methods to verify who you are. Every method ultimately produces a Vault token. Kubernetes auth checks pod service account JWTs, LDAP checks against directory services, and OIDC connects to identity providers like Okta or Azure AD.
Detailed Answer
Think of Vault authentication like airport security with multiple valid forms of ID. You can show a passport (token), a boarding pass linked to your airline account (Kubernetes service account), a government-issued driver's license (LDAP), or a trusted traveler card like Global Entry (OIDC). No matter which ID you present, once verified, you get the same type of boarding pass (a Vault token) that determines which gates and lounges you can access. The key point is that all auth methods end up producing a Vault token.
Vault supports over a dozen auth methods, each designed for a different identity source. Token auth is the most basic because every other auth method eventually creates a token. Tokens come in several types: root tokens with unlimited access, service tokens with TTLs (expiration times), and batch tokens that are lightweight and non-renewable for high-throughput workloads. Kubernetes auth is built for containerized workloads where pods prove their identity using their service account JWT. LDAP auth connects Vault to enterprise directory services like Active Directory or OpenLDAP, mapping directory groups to Vault policies. OIDC auth integrates with modern identity providers like Okta, Azure AD, Google Workspace, or Keycloak, enabling browser-based single sign-on.
Under the hood, every authentication follows the same pattern. The client presents a credential, and the auth method checks it against a backend. For Kubernetes auth, Vault calls the Kubernetes TokenReview API to verify the pod's service account JWT is real and not expired. For LDAP, Vault does an LDAP bind operation against the configured directory server. For OIDC, Vault goes through the OAuth2 authorization code flow, checking the ID token's signature against the provider's public keys (JWKS endpoint). Once the credential checks out, Vault creates a token and attaches policies based on configured role mappings. The token carries metadata like the entity ID, creation time, TTL, and attached policies. Vault's identity system creates a single canonical entity for each unique client, so the same person logging in through both LDAP and OIDC is tracked as one identity.
In production, the right auth method depends on what is connecting. Machine-to-machine communication in Kubernetes should use Kubernetes auth because it needs no static credentials and uses the existing service account setup. Human operators should use OIDC connected to the organization's identity provider, which lets you enforce multi-factor authentication and manage user access from one place. LDAP auth works well for organizations with existing Active Directory infrastructure who want to reuse their group structure for Vault permissions. Token auth should be reserved for automation, initial setup, and emergency break-glass situations. Most production deployments run multiple auth methods at the same time, each serving different types of clients.
The most dangerous mistake is not understanding token hierarchy and orphan tokens. By default, when a parent token is revoked, all child tokens are also revoked. So if an operator's token expires and they had created tokens for automation, those automation tokens die too, potentially causing outages. Another common error with Kubernetes auth is using the default service account instead of creating dedicated ones per application, which breaks least-privilege principles. For OIDC, teams often forget to set bound claims properly, accidentally letting any user from the identity provider authenticate to Vault regardless of their actual role or group membership.
Code Example
# Enable Kubernetes auth method
vault auth enable kubernetes # Mount Kubernetes auth at auth/kubernetes/ path
# Configure Kubernetes auth with cluster details
vault write auth/kubernetes/config \
kubernetes_host="https://prod-cluster.internal:6443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token # Point Vault to the K8s API server for JWT validation
# Create a role mapping K8s service account to Vault policies
vault write auth/kubernetes/role/payments-api \
bound_service_account_names="payments-api-sa" \
bound_service_account_namespaces="payments-prod" \
policies="payments-readonly,payments-db-creds" \
ttl="1h" \
max_ttl="4h" # Only pods with payments-api-sa in payments-prod namespace get these policies
# Enable LDAP auth method
vault auth enable ldap # Mount LDAP auth at auth/ldap/ path
# Configure LDAP connection to Active Directory
vault write auth/ldap/config \
url="ldaps://ad.payments-corp.com:636" \
userdn="OU=Engineers,DC=payments-corp,DC=com" \
groupdn="OU=Groups,DC=payments-corp,DC=com" \
groupfilter="(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}}))" \
userattr="sAMAccountName" \
certificate=@/etc/vault/ldap-ca.crt # Configure LDAP with recursive group lookup
# Map LDAP group to Vault policy
vault write auth/ldap/groups/sre-team \
policies="infrastructure-admin,database-admin" # SRE team members get admin policies
# Enable OIDC auth method for Okta SSO
vault auth enable oidc # Mount OIDC auth at auth/oidc/ path
# Configure OIDC with Okta as identity provider
vault write auth/oidc/config \
oidc_discovery_url="https://payments-corp.okta.com" \
oidc_client_id="0oa1b2c3d4VaultApp" \
oidc_client_secret="okta-client-secret-here" \
default_role="default" # Configure Okta OIDC discovery endpoint and client credentials
# Create OIDC role with group-based policy mapping
vault write auth/oidc/role/default \
bound_audiences="0oa1b2c3d4VaultApp" \
allowed_redirect_uris="https://vault.payments-corp.com/ui/vault/auth/oidc/oidc/callback" \
user_claim="email" \
groups_claim="groups" \
oidc_scopes="openid,profile,groups" \
token_policies="default" \
ttl="8h" # Map OIDC email to Vault identity, pull groups from token claims
# Login via Kubernetes from a pod
vault write auth/kubernetes/login \
role="payments-api" \
jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token # Authenticate using pod service account JWT
# Login via LDAP
vault login -method=ldap username="jdoe" # Authenticate with LDAP credentials (prompts for password)Interview Tip
A junior engineer typically picks one auth method and talks about it in isolation without connecting it to Vault's unified token system. In your interview, make the point that every auth method is just a different front door that produces the same thing: a Vault token with policies attached. Separate human auth methods (OIDC, LDAP) from machine auth methods (Kubernetes, AppRole, AWS IAM). Point out that Kubernetes auth is zero-secret-bootstrapping because the pod's service account JWT is automatically there and Vault validates it against the Kubernetes API. For OIDC, mention that it lets you enforce MFA through your identity provider without Vault having to handle MFA itself. Show you understand the identity system by explaining that Vault creates entity aliases, so one person logging in through multiple methods is still tracked as a single identity for audit purposes.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────┐
│ Vault Server │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Identity System │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Entity A│ │Entity B│ │Entity C│ │ │
│ │ └───┬────┘ └───┬────┘ └───┬────┘ │ │
│ └───────┼───────────┼───────────┼───────────────┘ │
│ ↑ ↑ ↑ │
│ ┌───────┴──┐ ┌─────┴────┐ ┌──┴──────────┐ │
│ │ Token │ │ LDAP │ │ OIDC │ │
│ │ Auth │ │ Auth │ │ Auth │ │
│ └───┬──────┘ └────┬─────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌───┴──────────────┴───────────────┴──────────┐ │
│ │ Kubernetes Auth │ │
│ └──────────────────┬──────────────────────────┘ │
└─────────────────────┼───────────────────────────────┘
↑
┌────────────┼────────────┐
│ │ │
┌──────┴─────┐ ┌────┴─────┐ ┌───┴──────┐
│ payments │ │ orders │ │ shipping │
│ -api pod │ │ -svc pod │ │ -svc pod │
│ (JWT) │ │ (JWT) │ │ (JWT) │
└────────────┘ └──────────┘ └──────────┘💬 Comments
Quick Answer
Vault policies are rules written in HCL or JSON that control what a token can do. Each rule maps a path to allowed actions (create, read, update, delete, list). Everything is denied by default unless a policy explicitly allows it.
Detailed Answer
Think of Vault policies like the badge system in a large office building. Your badge determines which floors you can visit, which doors you can open, and what you can do in each room. Some badges let you enter a room and read documents but not change them. Others give full access to certain floors while blocking others entirely. Just as a building security system keeps all doors locked by default, Vault denies all access unless a policy explicitly allows it.
Vault policies are written in HCL (HashiCorp Configuration Language) or JSON and define rules that map API paths to allowed actions. The five main actions are create, read, update, delete, and list. Two additional actions exist: sudo (for protected admin paths) and deny (an explicit override that blocks access no matter what). Policies follow a deny-by-default model, meaning a token with no policies can access nothing. Two built-in policies exist: the default policy grants basic self-management like token lookup and renewal, and the root policy grants unrestricted access to everything. Custom policies get attached to tokens during authentication, and a token's effective permissions are the combination of all its attached policies.
Under the hood, Vault evaluates policies using a longest-prefix-match approach. When a client makes a request to secret/data/payments-api/db, Vault looks at all policies on the token and finds the most specific matching path rule. Path patterns support wildcards: the asterisk matches any single path segment, and the plus sign matches one directory level. For example, secret/data/payments-api/* matches any key under payments-api but not nested paths, while secret/data/+ matches any direct child of data. Vault also supports templated policies using identity metadata, so you can write rules like secret/data/{{identity.entity.name}}/* to let each authenticated entity access only secrets under their own namespace. Policy changes take effect right away for new requests without needing to regenerate tokens.
In production, policy design follows least-privilege principles organized around your team structure. Teams typically create policies at three levels: infrastructure policies for platform teams (managing auth methods, secrets engines, and audit devices), application policies for service identities (reading specific secrets and generating dynamic credentials), and operator policies for human users (managing secrets within their team's area). Sentinel policies, available in Vault Enterprise, add a second enforcement layer with governance rules that can require MFA for certain paths or restrict secret creation to specific time windows. Many organizations use Terraform to manage Vault policies as code, which enables peer review and version control for access changes.
The most common mistake is overly broad wildcard policies that accidentally grant more access than intended. A policy like path "secret/*" { capabilities = ["read"] } looks reasonable but gives read access to every secret in the entire KV store, including other teams' secrets. Another frequent error is forgetting that the list action is separate from read. A service might be able to read a specific secret but cannot discover what secrets exist without list permission. Teams also trip over the difference between secret/data/ and secret/metadata/ paths in KV v2. The actual secret values live under data/ while version history and metadata live under metadata/, so you need separate policy rules for each.
Code Example
# Create a read-only policy for the payments-api service
# File: payments-api-policy.hcl
vault policy write payments-api - <<'EOF'
# Allow reading database credentials for payments service
path "database/creds/payments-readonly" {
capabilities = ["read"] # Generate dynamic DB credentials
}
# Allow reading KV secrets under payments-api namespace
path "secret/data/payments-api/*" {
capabilities = ["read", "list"] # Read any secret under payments-api/
}
# Allow reading secret metadata for version management
path "secret/metadata/payments-api/*" {
capabilities = ["read", "list"] # List and read version history
}
# Allow the service to renew its own token
path "auth/token/renew-self" {
capabilities = ["update"] # Keep token alive without re-authentication
}
# Explicitly deny access to other teams' secrets
path "secret/data/orders-api/*" {
capabilities = ["deny"] # Hard deny even if another policy grants access
}
EOF # Write the policy to Vault
# Create an admin policy for the SRE team
vault policy write sre-admin - <<'EOF'
# Full access to secrets engine management
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"] # Manage secrets engines
}
# Full access to auth method management
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"] # Manage auth methods
}
# Templated policy: each user accesses their own namespace
path "secret/data/users/{{identity.entity.name}}/*" {
capabilities = ["create", "read", "update", "delete", "list"] # Per-user secret namespace
}
EOF # Write admin policy to Vault
# List all configured policies
vault policy list # Show all policy names
# Read the contents of a specific policy
vault policy read payments-api # Display the HCL policy rules
# Create a token with specific policies attached
vault token create \
-policy="payments-api" \
-policy="default" \
-ttl="4h" \
-display-name="payments-api-prod" # Create token with payments-api and default policies
# Test a policy with a specific token (check capabilities)
vault token capabilities s.AbCdEf123456 secret/data/payments-api/db # Check what a token can do at a pathInterview Tip
A junior engineer typically writes overly broad wildcard policies without really understanding the path hierarchy or how capabilities work. In your interview, stress the deny-by-default model and how policies are additive across a token: if a token has three policies, the effective permissions are the union of all three, except that an explicit deny in any policy overrides all grants. Walk through a concrete example showing how path "secret/data/payments-api/*" differs from path "secret/data/payments-api/+" in what they match. Bring up templated policies with identity metadata as an advanced technique that avoids creating separate policies for every user. Highlight the KV v2 gotcha where you need separate rules for secret/data/ and secret/metadata/ paths, since this is the most common cause of "access denied" errors in production. Mention Sentinel policies for enterprise governance needs.
◈ Architecture Diagram
┌─────────────────────────────────────────────────────────┐ │ Vault Policy Engine │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ Path Matching (Longest Prefix) │ │ │ └──────────────────────┬───────────────────────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │ │ │ payments │ │ sre-admin │ │ default │ │ │ │ -api policy│ │ policy │ │ policy │ │ │ ├────────────┤ ├─────────────┤ ├──────────────┤ │ │ │secret/data/│ │sys/mounts/* │ │auth/token/ │ │ │ │payments-api│ │ C,R,U,D,L,S │ │renew-self │ │ │ │ R,L │ │ │ │ U │ │ │ ├────────────┤ ├─────────────┤ └──────────────┘ │ │ │database/ │ │sys/auth/* │ │ │ │creds/ │ │ C,R,U,D,L,S │ │ │ │ R │ └─────────────┘ │ │ └────────────┘ │ │ │ │ Token: policy=[payments-api, default] │ │ Effective: UNION of all attached policies │ │ Deny: overrides any grant from other policies │ └─────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
Vault integrates with Kubernetes through two main approaches. The Vault Agent Injector uses a webhook to add a sidecar container that fetches, renders, and auto-renews secrets. The CSI Provider uses a DaemonSet to mount secrets as files at pod startup. Each has different tradeoffs around flexibility, resource usage, and secret rotation.
Detailed Answer
Think of two ways to get mail delivered to your apartment. The Vault Agent Injector is like a personal assistant who lives with you (sidecar container), picks up your mail from the post office (Vault), sorts it for you (renders templates), and keeps checking for new mail throughout the day (auto-renewal). The Vault CSI Provider is like a mail slot in your door (volume mount) where the building's mailroom staff (DaemonSet) delivers letters from the post office. Both get mail to you, but the assistant can do more while the mail slot is simpler and cheaper.
The Vault Agent Injector works through a Kubernetes mutating admission webhook. When a pod is created with certain annotations (vault.hashicorp.com/agent-inject: "true"), the webhook intercepts the pod creation and adds Vault Agent sidecar containers. An init container runs first to authenticate with Vault and fetch initial secrets before the app starts, so secrets are ready from the beginning. The sidecar container keeps running alongside the application, automatically renewing tokens and re-fetching secrets when they change or are about to expire. Secrets are written as files in a shared in-memory volume (tmpfs) mounted at /vault/secrets, keeping sensitive data off disk.
Under the hood, the Agent Injector runs as a Kubernetes Deployment in the vault namespace with a MutatingWebhookConfiguration that intercepts CREATE and UPDATE operations on pods. When triggered, it adds two containers: an init container that runs vault agent with -exit-after-auth for the initial secret fetch, and a sidecar that runs vault agent in daemon mode with auto-auth set up for Kubernetes authentication. The agent uses consul-template-style templates to format secrets however you need (plain text, JSON, env file, etc.). The CSI Provider takes a different path. It runs as a DaemonSet that implements the CSI (Container Storage Interface) provider spec. When a pod mounts a SecretProviderClass volume, the kubelet calls the CSI driver, which talks to the Vault CSI provider through a Unix socket to authenticate and fetch secrets, then mounts them as files in the pod.
In production, the choice between the two depends on your needs. Agent Injector is more popular because it handles secret rotation without restarting pods, supports template rendering for custom formats, and works with any Kubernetes distribution. But each pod gets a sidecar using about 25-75 MB of memory, which adds up across hundreds of pods. CSI Provider is more efficient since one DaemonSet serves all pods on a node, but it requires installing the Secrets Store CSI Driver separately and does not natively support automatic secret rotation without enabling the rotation feature flag. Many teams use both: Agent Injector for pods that need dynamic database credentials with auto-renewal, and CSI Provider for simpler cases like TLS certificates or static API keys.
The biggest gotcha with the Agent Injector is annotation sprawl and init container race conditions. If the init container fails to fetch secrets because Vault is briefly unavailable, the app container will start without secrets, which can cause crashes. Teams need proper readiness probes and retry logic. With CSI Provider, the key pitfall is that secrets are only fetched at pod startup by default. If a secret rotates in Vault, existing pods keep the stale value until they restart, unless you enable the rotation reconciler that polls Vault at set intervals. Another common mistake is not setting the right service account for the pod, which causes auth failures that show up as confusing mount errors instead of clear permission-denied messages.
Code Example
# Install Vault Helm chart with Agent Injector enabled
helm install vault hashicorp/vault \
--namespace vault \
--set "injector.enabled=true" \
--set "server.ha.enabled=true" \
--set "server.ha.replicas=3" # Deploy Vault server with HA and injector webhook
# Create a Kubernetes service account for the payments-api
kubectl create serviceaccount payments-api-sa \
-n payments-prod # Dedicated service account for Vault auth
# Configure Vault Kubernetes auth role (run against Vault)
vault write auth/kubernetes/role/payments-api \
bound_service_account_names="payments-api-sa" \
bound_service_account_namespaces="payments-prod" \
policies="payments-api" \
ttl="1h" # Bind K8s service account to Vault policy
# Deploy payments-api with Vault Agent Injector annotations
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
namespace: payments-prod
spec:
replicas: 3
selector:
matchLabels:
app: payments-api
template:
metadata:
labels:
app: payments-api
annotations:
vault.hashicorp.com/agent-inject: "true" # Enable sidecar injection
vault.hashicorp.com/role: "payments-api" # Vault auth role to use
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/payments-readonly" # Secret path to fetch
vault.hashicorp.com/agent-inject-template-db-creds: | # Custom template to render credentials
{{- with secret "database/creds/payments-readonly" -}}
export DB_USER="{{ .Data.username }}"
export DB_PASS="{{ .Data.password }}"
{{- end }}
spec:
serviceAccountName: payments-api-sa # Use dedicated service account
containers:
- name: payments-api
image: payments-corp/payments-api:v2.4.1
command: ["/bin/sh", "-c", "source /vault/secrets/db-creds && exec ./payments-api"] # Source secrets then start app
volumeMounts: [] # Vault agent manages the volume automatically
EOF # Deploy with Vault sidecar injection
# Install Secrets Store CSI Driver (prerequisite for CSI Provider)
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set syncSecret.enabled=true \
--set enableSecretRotation=true # Install CSI driver with rotation support
# Install Vault CSI Provider
helm install vault-csi hashicorp/vault \
--namespace vault \
--set "csi.enabled=true" \
--set "server.enabled=false" \
--set "injector.enabled=false" # Deploy only the CSI provider DaemonSet
# Create SecretProviderClass for Vault CSI
kubectl apply -f - <<'EOF'
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-payments-secrets
namespace: payments-prod
spec:
provider: vault
parameters:
vaultAddress: "http://vault.vault.svc:8200" # Vault server address
roleName: "payments-api" # Vault auth role
objects: | # List of secrets to mount
- objectName: "db-password"
secretPath: "secret/data/payments-api/db"
secretKey: "password"
EOF # Define which Vault secrets to mount via CSIInterview Tip
A junior engineer typically describes only one injection method without comparing tradeoffs or explaining when to pick each one. In your interview, present both the Agent Injector and CSI Provider as complementary tools. Explain that the Agent Injector uses a mutating admission webhook to add sidecar containers that handle authentication, secret fetching, template rendering, and automatic renewal. Contrast that with the CSI Provider's approach of using a DaemonSet and the Secrets Store CSI Driver to mount secrets as volumes. Highlight the key difference: Agent Injector handles dynamic secret rotation naturally because the sidecar keeps running, while CSI Provider needs explicit rotation configuration. Mention the memory tradeoff where a sidecar per pod adds up versus one DaemonSet per node. Point out that the annotation-based configuration is both a strength for simplicity and a weakness for discoverability in large deployments.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─── Agent Injector Flow ────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Mutating │ │ Pod (payments-api) │ │ │
│ │ │ Webhook │───→│ ┌──────────┐ ┌───────────┐ │ │ │
│ │ └──────────┘ │ │ App │ │ Vault │ │ │ │
│ │ │ │Container │ │ Agent │ │ │ │
│ │ │ │ │ │ Sidecar │ │ │ │
│ │ │ └─────┬────┘ └─────┬─────┘ │ │ │
│ │ │ │ tmpfs │ │ │ │
│ │ │ ←────────────→ │ │ │
│ │ │ /vault/secrets │ │ │
│ │ └────────────────┬─────────────┘ │ │
│ └───────────────────────────────────┼────────────────┘ │
│ │ │
│ ┌─── CSI Provider Flow ─────────────┼────────────────┐ │
│ │ │ │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │ │
│ │ │ CSI │ │ Vault CSI │ │ │ │
│ │ │ Driver │→ │ Provider │ │ │ │
│ │ │ │ │ (DaemonSet) │ │ │ │
│ │ └──────────┘ └──────┬───────┘ │ │ │
│ └───────────────────────┼───────────┼────────────────┘ │
│ │ │ │
└──────────────────────────┼───────────┼───────────────────┘
│ │
↓ ↓
┌──────────────────────┐
│ Vault Server │
│ ┌────────────────┐ │
│ │ K8s Auth │ │
│ │ Secrets Engine │ │
│ └────────────────┘ │
└──────────────────────┘💬 Comments
Quick Answer
Vault HA uses active/standby nodes sharing storage (Consul or Raft) where one active node handles requests and standbys take over if it fails. DR involves cross-datacenter replication (Enterprise), automated snapshots, auto-unseal, and tested recovery runbooks.
Detailed Answer
Think of Vault HA like a hospital emergency room. There is always one attending physician on duty (active node) making decisions and treating patients (handling requests). Several residents (standby nodes) are in the building, fully equipped and ready to step in if the attending is unavailable (leader election). For disaster recovery, the hospital has partnerships with hospitals in other cities (replication clusters) and keeps complete copies of all patient records (snapshots) in secure off-site storage, so even if the entire building goes down, operations can resume elsewhere.
Vault achieves high availability through leader election where only one node is active at any time and multiple standbys are ready to take over. With integrated Raft storage (the recommended approach since Vault 1.4), Vault nodes form a consensus cluster where the leader handles all writes and copies data to followers. If the leader fails, Raft automatically elects a new leader, usually within seconds. When using Consul as the storage backend, Vault uses Consul's session and locking mechanism for leader election. Standby nodes can either forward requests to the active node (the default) or send HTTP 307 redirects pointing the client to the active node's address.
Under the hood, the Raft consensus protocol needs a quorum (majority) of nodes to be healthy for the cluster to work. In a 3-node cluster, 2 must be up; in a 5-node cluster, 3 must be up. Writes only commit when the leader replicates the log entry to a quorum of followers, which ensures data durability. The unsealing process matters during recovery because each Vault node starts sealed and must be individually unsealed. Auto-unseal using cloud KMS services (AWS KMS, Azure Key Vault, GCP Cloud KMS) removes the manual bottleneck by letting the cloud provider's hardware security modules handle the unseal key. Vault Enterprise extends HA with performance replication (read replicas that serve reads locally) and disaster recovery replication (full cluster replication across regions for failover).
In production, a solid Vault deployment needs multiple layers of protection. The Raft cluster should have 3 or 5 nodes spread across availability zones within a region. Auto-unseal should be configured so node restarts and scaling events do not need manual intervention. Automated snapshots using vault operator raft snapshot save should run on a schedule (every 1-4 hours) and be stored in a separate system like S3 with versioning and cross-region replication. Monitoring should track seal status, leader elections, storage backend latency, and token expiration rates. Load balancers should use Vault's /v1/sys/health endpoint for health checks, with the right response codes configured (200 for active, 429 for standby, 472 for DR secondary). For multi-region setups, Vault Enterprise's DR replication provides near-zero RPO (Recovery Point Objective) and RTO (Recovery Time Objective) of minutes.
The most dangerous mistake is losing unseal keys and the recovery key at the same time. Without auto-unseal, losing enough unseal keys means your Vault data is permanently encrypted and unrecoverable. Teams must set up secure key distribution, such as storing each key share with a different trusted person or in separate secure storage systems. Another critical pitfall is never testing disaster recovery. Having snapshots is worthless if you have never practiced restoring them to a new cluster under pressure. A subtler Raft issue is split-brain during network partitions, where an isolated leader keeps accepting writes that will be lost when the partition heals and a new leader has already been elected. Setting appropriate Raft timeouts and monitoring for leadership changes helps catch this.
Code Example
# Vault server configuration for HA with integrated Raft storage
# File: /etc/vault.d/vault.hcl
# Configure Raft integrated storage for HA
# storage "raft" {
# path = "/opt/vault/data" # Local data directory for Raft logs and snapshots
# node_id = "vault-prod-1" # Unique identifier for this node in the cluster
# }
# Initialize a new Vault cluster with key shares
vault operator init \
-key-shares=5 \
-key-threshold=3 \
-format=json > /secure/vault-init-keys.json # Create 5 unseal key shares, require 3 to unseal
# Unseal the Vault (must be done on each node)
vault operator unseal <key-share-1> # Provide first unseal key share
vault operator unseal <key-share-2> # Provide second unseal key share
vault operator unseal <key-share-3> # Provide third key share to reach threshold and unseal
# Join additional nodes to the Raft cluster
vault operator raft join https://vault-prod-1.internal:8200 # Join second node to cluster leader
vault operator raft join https://vault-prod-1.internal:8200 # Join third node to cluster leader
# Verify Raft cluster membership
vault operator raft list-peers # Show all nodes in the Raft cluster with leader status
# Configure auto-unseal with AWS KMS (in vault.hcl)
# seal "awskms" {
# region = "us-east-1" # AWS region for KMS key
# kms_key_id = "arn:aws:kms:us-east-1:123456:key/abc-def-123" # KMS key ARN for auto-unseal
# }
# Take a manual Raft snapshot for backup
vault operator raft snapshot save /backup/vault-snapshot-$(date +%Y%m%d-%H%M%S).snap # Save point-in-time Raft snapshot
# Automated snapshot script for cron (runs every 4 hours)
# 0 */4 * * * /usr/local/bin/vault-backup.sh
# --- vault-backup.sh ---
export VAULT_ADDR='https://vault.internal:8200' # Set Vault address for CLI
export VAULT_TOKEN=$(cat /secure/backup-token) # Read backup service token
SNAPSHOT_FILE="vault-snap-$(date +%Y%m%d-%H%M%S).snap" # Generate timestamped filename
vault operator raft snapshot save /tmp/${SNAPSHOT_FILE} # Take Raft snapshot
aws s3 cp /tmp/${SNAPSHOT_FILE} s3://prod-vault-backups/snapshots/ --sse aws:kms # Upload to S3 with KMS encryption
rm -f /tmp/${SNAPSHOT_FILE} # Clean up local snapshot file
# Restore from a Raft snapshot (disaster recovery)
vault operator raft snapshot restore /backup/vault-snapshot-20240115-120000.snap # Restore Vault state from snapshot
# Check Vault health for load balancer configuration
curl -s https://vault.internal:8200/v1/sys/health | python3 -m json.tool # Check node health, seal status, and HA mode
# Monitor Raft cluster health
vault operator raft list-peers # Verify all nodes are healthy and identify the current leader
vault status # Check seal status, HA enabled, and active node address
# Enable DR replication on primary cluster (Enterprise only)
vault write -f sys/replication/dr/primary/enable # Enable DR primary replication
vault write sys/replication/dr/primary/secondary-token id="prod-dr-secondary" # Generate activation token for DR secondaryInterview Tip
A junior engineer typically mentions running multiple Vault instances but cannot explain leader election, quorum requirements, or the key difference between HA and DR. In your interview, clearly separate these: HA protects against node failures within a datacenter using active/standby with shared storage and automatic failover, while DR protects against entire datacenter outages using cross-region replication. Explain that integrated Raft storage is the modern standard because it removes the need for a separate Consul cluster while still giving you strong consistency. Stress that auto-unseal is a production must-have since manual unsealing creates an operational bottleneck and a security risk when you need to gather unseal keys during an outage. Talk about your snapshot strategy, covering frequency, retention, encrypted off-site storage, and most importantly, regular restoration testing to prove your backups actually work.
◈ Architecture Diagram
┌──────────┐
│ Vault │
│ Startup │
└────┬─────┘
↓
┌──────────┐
│ Wrapped │
│ Root Key │
└────┬─────┘
↓
┌──────────┐
│ AWS KMS │
│ Decrypt │
└────┬─────┘
↓
┌──────────┐
│ Unsealed │
│ Vault │
└──────────┘
┌──────────┐
│ Recovery │→ Root Token
│ Keys │→ Rekey Only
└──────────┘💬 Comments
Quick Answer
Vault's database engine creates unique, short-lived credentials per request, each tracked by a lease with a TTL. At scale, mass credential requests during rollouts can overwhelm both Vault and the database's connection limit, requiring connection pooling, TTL tuning, and rate limiting.
Detailed Answer
Think of a hotel that issues temporary room keys instead of permanent ones. Each guest gets a unique key that expires at checkout. If the guest wants to stay longer, they visit the front desk to extend it. If every guest checks in at 3 PM at the same time, the front desk becomes a bottleneck. Vault's dynamic secrets work the same way: each application instance gets unique database credentials that expire, but mass simultaneous requests stress the system.
Dynamic secrets exist because static, shared database passwords are a security risk. When every microservice shares one password and it leaks, every service is compromised and rotating the credential means coordinating all consumers at once. Vault's database secrets engine creates a unique username and password (or certificate) per request, with each one tracked by a lease. The lease has a TTL (time-to-live) and a max TTL. Services can renew the lease before it expires to keep using the same credentials, or let it expire and request fresh ones.
Under the hood, when a service reads from the creds endpoint (e.g., vault read database/creds/payments-readonly), Vault connects to the target database using a pre-configured admin connection, runs the creation SQL defined in the role (CREATE USER with specific privileges), generates a random password, stores a lease record in its storage backend, and returns the credentials with the lease ID and TTL. When the lease expires or gets revoked, Vault runs the revocation SQL (DROP USER or ALTER USER) to remove access. A background process in Vault scans for expired leases and revokes them in batches.
At production scale with 500 pods rolling out at once, each requesting database credentials, Vault faces three bottlenecks: Vault's own request handling capacity, the database's ability to handle rapid CREATE USER statements, and the connection limit on the database. If each of 500 pods gets unique credentials and opens a connection pool of 10, that is 5,000 database connections, which can blow past PostgreSQL's max_connections. You need to tune max_open_connections on the Vault database config to limit how many credential-creation connections Vault opens at once, set reasonable default_ttl and max_ttl to balance security with renewal overhead, and put connection poolers like PgBouncer between the application and database.
The sneaky gotcha is lease revocation storms. When Vault's lease tracker falls behind due to high load or a brief outage, expired leases pile up. When the tracker catches up, it tries to mass-revoke, sending hundreds of DROP USER statements to the database at once. This can spike database CPU and cause lock contention, hurting application queries. Watch the vault.expire.num_leases metric, set appropriate max_open_connections, and stagger pod rollouts to avoid credential request thundering herds.
Code Example
# Enable the database secrets engine at a specific path
vault secrets enable -path=database database
# Configure PostgreSQL connection for the payments database
vault write database/config/payments-db \
plugin_name=postgresql-database-plugin \
allowed_roles="payments-readonly,payments-readwrite" \
connection_url="postgresql://{{username}}:{{password}}@payments-db.prod.company.com:5432/payments?sslmode=require" \
max_open_connections=5 \
max_idle_connections=3 \
max_connection_lifetime=30s \
username="vault_admin" \
password="REDACTED"
# Create a read-only role with 1-hour TTL and 24-hour max TTL
vault write database/roles/payments-readonly \
db_name=payments-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
# Application requests credentials (each call gets unique creds)
vault read database/creds/payments-readonly
# Returns: username=v-payments-readonly-abc123, password=<random>, lease_id=database/creds/payments-readonly/xyz789, lease_duration=3600
# Renew a lease before it expires to extend credential lifetime
vault lease renew -increment=1h database/creds/payments-readonly/xyz789
# Revoke a specific credential when a pod shuts down
vault lease revoke database/creds/payments-readonly/xyz789
# Revoke all credentials for the payments-readonly role during incident response
vault lease revoke -prefix database/creds/payments-readonly
# Monitor active lease count to detect accumulation
vault read sys/metrics | grep expire.num_leasesInterview Tip
A junior engineer typically says Vault creates temporary database passwords and stops there. For a senior or architect role, the interviewer wants to hear about lease lifecycle and production scaling concerns. Explain how the lease tracker works, what happens when leases expire in bulk (revocation storms), how max_open_connections prevents Vault from overwhelming the database during credential creation, and why rollout strategies matter for credential request thundering herds. Strong answers also cover the difference between lease renewal (extending existing credentials) and new credential requests (creating new database users), and why connection poolers like PgBouncer become essential when dynamic secrets multiply your connection count.
◈ Architecture Diagram
┌──────────┐
│ Pod A │──→ vault read
└────┬─────┘ creds/role
↓
┌──────────┐
│ Vault │──→ CREATE USER
│ Server │ on database
└────┬─────┘
↓
┌──────────┐
│ Lease │──→ TTL: 1h
│ Tracker │ Revoke on
└────┬─────┘ expiry
↓
┌──────────┐
│PaymentsDB│
│ Postgres │
└──────────┘💬 Comments
Quick Answer
Auto-unseal lets an external KMS or Vault Transit engine decrypt the root encryption key automatically on startup. This replaces Shamir's unseal keys with recovery keys, which can only generate new root tokens or trigger rekey operations but cannot unseal Vault. The trust shifts from human key holders to the KMS itself.
Detailed Answer
Think of a bank vault with two possible lock systems. The traditional lock requires three bank officers to each insert their key at the same time (Shamir's unseal keys). The modern lock uses an electronic reader connected to a central security office that opens automatically when the vault door is closed (auto-unseal via KMS). If the security office network goes down, the vault stays locked even though the vault itself is fine. Recovery keys in the modern system let officers generate a new electronic badge, but they cannot manually turn the old mechanical lock.
Vault stores all data encrypted with a root encryption key, which is itself encrypted (wrapped) by the unseal mechanism. In the default Shamir mode, the root key is split into shares, and you need a threshold of shares to put the key back together and unseal Vault. This is secure but operationally painful: every Vault restart, upgrade, or node replacement needs someone to provide unseal shares. Auto-unseal fixes this by having an external system do the key unwrapping automatically.
With AWS KMS auto-unseal, you configure Vault with a KMS key ARN. During initialization, Vault generates its root encryption key and wraps it using an AWS KMS Encrypt API call. The wrapped key is saved in Vault's storage backend. On startup, Vault calls KMS Decrypt to unwrap the root key, completing the unseal without anyone having to type anything. The IAM role on the Vault server must have kms:Encrypt and kms:Decrypt permissions for the specified KMS key. Transit auto-unseal works the same way but uses another Vault cluster's Transit secrets engine as the key wrapping service, creating a trust chain between two Vault clusters. When you initialize Vault with auto-unseal enabled, it generates recovery keys (using Shamir splitting) instead of unseal keys. These recovery keys can generate root tokens and start rekey operations, but they cannot perform the unseal operation itself.
At production scale, auto-unseal is essentially required for any automated deployment, Kubernetes-based Vault setup, or auto-scaling group. Without it, a pod restart or node replacement creates a sealed Vault that blocks all secret access until operators show up with unseal shares. You need to make sure the KMS is highly available, consider cross-region KMS replication for DR, lock down IAM policies to least privilege, and monitor CloudTrail for KMS Decrypt calls. For Transit auto-unseal, the upstream Vault cluster becomes a hard dependency: if it goes down, the downstream cluster cannot unseal.
The gotcha that catches people is the migration path between seal types. Moving from Shamir to auto-unseal, or between two different KMS providers, requires a seal migration that rewraps the root key. During migration, Vault must restart with both old and new seal configurations, and the old unseal or recovery keys must be provided. If the old keys are lost or the old KMS key is deleted before migration finishes, the Vault data is gone forever. Document your seal configurations, keep old KMS keys until migration is confirmed, and always test the migration in staging before touching production.
Code Example
# Vault server configuration with AWS KMS auto-unseal
# /etc/vault.d/vault.hcl
storage "raft" { # Uses integrated Raft storage for HA clustering
path = "/opt/vault/data" # Local data directory for Raft log and snapshots
node_id = "vault-prod-1" # Unique identifier for this Raft cluster member
}
listener "tcp" { # TCP listener for API and cluster communication
address = "0.0.0.0:8200" # Listens on all interfaces on port 8200
tls_cert_file = "/opt/vault/tls/vault.crt" # TLS certificate for HTTPS
tls_key_file = "/opt/vault/tls/vault.key" # TLS private key
}
seal "awskms" { # Delegates unseal to AWS KMS
region = "us-east-1" # AWS region where the KMS key exists
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234-efgh-5678" # KMS key ARN for wrapping
# IAM role on the EC2 instance or EKS pod must have kms:Encrypt and kms:Decrypt
}
api_addr = "https://vault.prod.company.com:8200" # Advertised API address for client redirection
cluster_addr = "https://vault-prod-1.internal:8201" # Raft peer communication address
# Initialize Vault with auto-unseal (creates recovery keys, not unseal keys)
vault operator init -recovery-shares=5 -recovery-threshold=3
# Output: Recovery Key 1: xxxx, Recovery Key 2: yyyy, ...
# Note: These are RECOVERY keys, NOT unseal keys
# Verify auto-unseal is active after a restart
vault status
# Seal Type: awskms
# Sealed: false (auto-unsealed via KMS)
# Transit auto-unseal configuration (alternative to KMS)
seal "transit" { # Uses another Vault cluster's Transit engine
address = "https://vault-upstream.prod.company.com:8200" # Upstream Vault address
token = "s.UPSTREAM_TOKEN" # Token with transit encrypt/decrypt permissions
disable_renewal = "false" # Renews the token automatically
key_name = "vault-downstream-autounseal" # Transit key name for wrapping
mount_path = "transit/" # Path where Transit engine is mounted
}
# Generate a new root token using recovery keys (emergency procedure)
vault operator generate-root -init
vault operator generate-root -nonce=<nonce> <recovery-key-1>
vault operator generate-root -nonce=<nonce> <recovery-key-2>
vault operator generate-root -nonce=<nonce> <recovery-key-3>Interview Tip
A junior engineer typically says auto-unseal means Vault starts without needing keys, but for a senior or architect role, the interviewer wants to hear about the trust model shift and failure modes. Explain how the root encryption key wrapping changes from Shamir shares to KMS Decrypt calls, why recovery keys serve a different purpose and cannot unseal, what happens when KMS is unavailable (Vault stays sealed), and the critical risk during seal migration. Strong answers also cover Transit auto-unseal dependency chains, IAM least-privilege for KMS access, CloudTrail auditing of unseal operations, and why losing a KMS key before migration finishes means permanent data loss.
◈ Architecture Diagram
┌──────────┐
│ Vault │
│ Startup │
└────┬─────┘
↓
┌──────────┐
│ Wrapped │
│ Root Key │
└────┬─────┘
↓
┌──────────┐
│ AWS KMS │
│ Decrypt │
└────┬─────┘
↓
┌──────────┐
│ Unsealed │
│ Vault │
└──────────┘
┌──────────┐
│ Recovery │→ Root Token
│ Keys │→ Rekey Only
└──────────┘💬 Comments
Quick Answer
Vault Agent runs as an init container and sidecar that authenticates, fetches secrets, renders them to files, and handles lease renewal. The CSI Provider uses a CSI volume to mount secrets as files at startup. Agent is more flexible but adds a sidecar per pod; CSI is simpler but cannot rotate dynamic secrets without a pod restart.
Detailed Answer
Think of two ways to get mail delivered to your apartment. A personal assistant (Vault Agent) picks up your mail, sorts it, puts specific letters in specific folders, and checks for new mail all day. A building mailroom slot (CSI Provider) delivers mail when you first open your mailbox but does not actively refresh it. Both save you from going to the post office yourself (hardcoding Vault API calls in your app), but they differ in how they handle ongoing updates.
The core problem both solve is getting secrets to applications without changing application code. Without them, every app needs Vault SDK integration, authentication logic, lease renewal code, and error handling for Vault being down. Many legacy or third-party applications cannot be modified at all. Both Vault Agent and CSI Provider authenticate to Vault on behalf of the application, fetch the requested secrets, and make them available as ordinary files the application can read like any config file.
Vault Agent works by injecting an init container that authenticates to Vault using Kubernetes auth (validating the pod's service account JWT), fetches the initial secrets, and writes them to a shared emptyDir volume using Go templates. Then the sidecar container keeps running to handle lease renewal and re-rendering when secrets change. The Agent template system supports custom formatting, so secrets can be written as .env files, JSON, YAML, or whatever the application expects. When a dynamic secret's lease is about to expire, Agent automatically renews it or fetches new credentials and updates the file. The Vault Agent Injector automates all of this through a mutating webhook that reads pod annotations.
The CSI Provider takes a different route. The Secrets Store CSI Driver creates a CSI volume that mounts secrets as files. The Vault CSI Provider plugin authenticates to Vault and fetches the specified secrets during the volume mount phase. Secrets show up as files in the mounted path. But since no sidecar keeps running, it does not handle lease renewal or dynamic secret rotation by default. The enableSecretRotation feature polls Vault at intervals, but the application needs to watch for file changes or pods need to restart for updated secrets to take effect.
The hidden cost of Vault Agent is resource overhead at scale. Each pod with the Agent sidecar uses 50-100 MB of memory and some CPU. In a cluster with 2,000 pods, that is 100-200 GB of memory just for secret injection sidecars. The gotcha with CSI Provider is the rotation gap: if a database credential is revoked in Vault and the CSI Provider has not refreshed yet, the application uses stale credentials and gets authentication errors. Pick Agent when dynamic secret rotation matters, and CSI Provider when simplicity and lower resource usage are the priority.
Code Example
# Vault Agent Injector: Pod annotations for automatic sidecar injection
apiVersion: apps/v1 # Deployment API for managing replicated pods
kind: Deployment # Long-running service with Agent sidecar
metadata:
name: checkout-worker # Payment processing worker service
namespace: payments # Application namespace
spec:
replicas: 4 # Four replicas processing checkout transactions
selector:
matchLabels:
app: checkout-worker # Connects Deployment to Pods
template:
metadata:
labels:
app: checkout-worker # Pod label for service discovery
annotations:
vault.hashicorp.com/agent-inject: "true" # Enables Agent sidecar injection
vault.hashicorp.com/role: "payments-checkout" # Vault Kubernetes auth role
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/payments-readwrite" # Dynamic DB creds path
vault.hashicorp.com/agent-inject-template-db-creds: | # Custom template for rendering
{{- with secret "database/creds/payments-readwrite" -}}
PGUSER={{ .Data.username }}
PGPASSWORD={{ .Data.password }}
PGHOST=payments-db.prod.company.com
PGPORT=5432
PGDATABASE=payments
{{- end }}
vault.hashicorp.com/agent-inject-secret-api-key: "secret/data/payments/stripe-key" # Static KV secret
vault.hashicorp.com/agent-limits-cpu: "100m" # Limits Agent sidecar CPU usage
vault.hashicorp.com/agent-limits-mem: "64Mi" # Limits Agent sidecar memory
spec:
serviceAccountName: checkout-worker # SA bound to Vault Kubernetes auth role
containers:
- name: worker # Main application container
image: registry.company.com/checkout-worker:5.3.1 # Production worker image
command: ['./checkout-worker', '--config=/vault/secrets/db-creds'] # Reads secrets from Agent-rendered files
---
# CSI Provider: SecretProviderClass mounting Vault secrets as a CSI volume
apiVersion: secrets-store.csi.x-k8s.io/v1 # Secrets Store CSI Driver API
kind: SecretProviderClass # Defines which secrets to mount and from where
metadata:
name: checkout-vault-secrets # Name referenced by the pod volume
namespace: payments # Same namespace as the consuming pod
spec:
provider: vault # Uses the Vault CSI provider plugin
parameters:
vaultAddress: "https://vault.prod.company.com:8200" # Vault server URL
roleName: "payments-checkout" # Vault Kubernetes auth role
objects: | # List of secrets to mount as files
- objectName: "db-password" # Filename in the mounted volume
secretPath: "secret/data/payments/db-password" # Vault KV path
secretKey: "password" # Specific key within the KV secret
- objectName: "stripe-api-key" # Another file in the mount
secretPath: "secret/data/payments/stripe-key" # Vault path for Stripe key
secretKey: "key" # Key field within the secret
# Vault Kubernetes auth role binding the service account
vault write auth/kubernetes/role/payments-checkout \
bound_service_account_names=checkout-worker \
bound_service_account_namespaces=payments \
policies=payments-db-readwrite,payments-kv-read \
ttl=1hInterview Tip
A junior engineer typically says Vault Agent injects secrets as files into pods and stops there. For a senior or architect role, the interviewer wants a tradeoff analysis between Agent and CSI Provider. Explain how Agent handles lease renewal continuously through the sidecar while CSI Provider only fetches secrets at mount time, how Agent's template rendering lets you format secrets however the app needs them, the memory cost of running sidecars across thousands of pods, and why CSI Provider's polling-based rotation creates windows where stale credentials cause application failures. Strong answers also cover the mutating webhook mechanism, Kubernetes auth role binding, and strategies for monitoring secret injection failures at scale.
◈ Architecture Diagram
┌──────────────────────────┐ │ Vault Agent Model │ │ ┌────────┐ ┌──────────┐ │ │ │Init │→│Sidecar │ │ │ │Auth │ │Renew+ │ │ │ └────────┘ │Render │ │ │ └────┬─────┘ │ │ ↓ │ │ ┌──────────┐ │ │ │ App Pod │ │ │ │ /vault/ │ │ │ │ secrets │ │ │ └──────────┘ │ └──────────────────────────┘ ┌──────────────────────────┐ │ CSI Provider Model │ │ ┌────────┐ │ │ │CSI Vol │→ Mount once │ │ │Mount │ at pod start │ │ └────┬───┘ │ │ ↓ │ │ ┌──────────┐ │ │ │ App Pod │ │ │ │ /mnt/sec │ │ │ └──────────┘ │ └──────────────────────────┘
💬 Comments
Quick Answer
Performance replication creates read-only secondary clusters that serve local reads while forwarding writes to the primary, reducing latency. DR replication creates a standby cluster that mirrors everything but serves no traffic until promoted. Use performance replicas for low-latency reads and DR replicas for business continuity.
Detailed Answer
Think of a retail bank with branch offices. Performance replication is like having local tellers at each branch who can check your account balance (reads) but must call headquarters to process a transfer (writes). Disaster recovery replication is like keeping a fully stocked backup headquarters that sits idle until the main office goes down, at which point it takes over everything.
Vault replication exists because a single Vault cluster in one region creates two problems: applications in distant regions experience high latency on every secret read, and a regional outage kills all secret access. Vault Enterprise offers two replication modes that address these separately, because the solutions need different architectural tradeoffs.
Performance replication continuously streams encrypted data from the primary cluster to secondary clusters. Each secondary has a complete copy of the secrets, policies, auth methods, and token state from the primary. Secondaries serve read requests locally with low latency: a service in eu-west reading a KV secret hits the local secondary instead of crossing the Atlantic to us-east. Write requests (creating secrets, changing policies, generating dynamic credentials) get forwarded to the primary transparently. The secondary handles the client connection while the primary runs the write, and the result comes back to the client. This is invisible to the application. Performance secondaries also maintain their own token store and can authenticate clients locally.
Disaster recovery replication also streams data from the primary, but the DR secondary does not serve any client traffic at all. It keeps a warm copy of everything, including local-only data like auth mounts and tokens. When the primary becomes unavailable, an operator promotes the DR secondary to primary using the DR promotion API. After promotion, the former secondary becomes the new primary and starts handling traffic. The RPO (how much data you could lose) depends on replication lag, and the RTO (how long until you are back up) depends on how fast you can promote and update DNS or load balancers.
The gotcha that trips up many architects is thinking one type covers both needs. Performance secondaries cannot generate dynamic secrets when the primary is down because dynamic secret creation is a write that forwards to the primary. If teams depend on dynamic database credentials from a performance secondary and the primary fails, all new credential requests fail even though existing leases may still work. DR secondaries require a promotion event, meaning secret access is unavailable during the promotion window. Architects who need both low latency and continuity must deploy both: performance secondaries in each active region for fast reads, plus a DR secondary in a separate failure domain for business continuity.
Code Example
# Enable performance replication on the primary cluster vault write -f sys/replication/performance/primary/enable \ primary_cluster_addr="https://vault-primary.us-east.company.com:8201" # Generate a secondary activation token for the EU cluster vault write sys/replication/performance/primary/secondary-token \ id="eu-west-perf-secondary" \ ttl="30m" # Returns: wrapping_token: s.xxxxxxxx # On the EU secondary cluster, activate performance replication vault write sys/replication/performance/secondary/enable \ token="s.xxxxxxxx" \ primary_api_addr="https://vault-primary.us-east.company.com:8200" # Verify replication status on the secondary vault read sys/replication/performance/status # connection_state: ready # mode: secondary # known_primary_cluster_addrs: [https://vault-primary.us-east.company.com:8201] # Enable DR replication on the primary cluster vault write -f sys/replication/dr/primary/enable \ primary_cluster_addr="https://vault-primary.us-east.company.com:8201" # Generate a DR secondary activation token vault write sys/replication/dr/primary/secondary-token \ id="us-west-dr-secondary" \ ttl="30m" # On the DR secondary cluster, activate DR replication vault write sys/replication/dr/secondary/enable \ token="s.yyyyyyyy" \ primary_api_addr="https://vault-primary.us-east.company.com:8200" # Promote DR secondary to primary during a disaster vault operator raft join https://vault-dr.us-west.company.com:8200 vault write -f sys/replication/dr/secondary/promote \ dr_operation_token="s.DR_BATCH_TOKEN" # Monitor replication lag (critical for RPO calculations) vault read sys/replication/performance/status -format=json | jq '.data.secondaries[].connection_status' vault read sys/replication/dr/status -format=json | jq '.data.last_wal'
Interview Tip
A junior engineer typically says replication copies Vault data to another cluster, but for a senior or architect role, the interviewer wants the architectural distinction between performance and DR replication. Explain how performance secondaries serve reads locally but forward writes, how DR secondaries sit idle until promoted, why performance replication is not a DR solution (dynamic secret generation fails when the primary is down), and why DR promotion has an RTO cost where secrets are unavailable during the switchover. Strong answers include monitoring replication lag, the RPO implications of asynchronous replication, and the practical recommendation to deploy both types when you need both low latency and business continuity.
◈ Architecture Diagram
┌──────────────────────────┐ │ Performance Replication │ │ │ │ ┌────────┐ ┌──────────┐ │ │ │Primary │→ │Perf Sec │ │ │ │R+W │ │Read Only │ │ │ └────────┘ └──────────┘ │ │ ↑ writes forwarded │ └──────────────────────────┘ ┌──────────────────────────┐ │ DR Replication │ │ │ │ ┌────────┐ ┌──────────┐ │ │ │Primary │→ │DR Standby│ │ │ │Active │ │No Traffic│ │ │ └────────┘ └──────────┘ │ │ promote ↓ │ │ ┌──────────┐ │ │ │New Primary│ │ │ └──────────┘ │ └──────────────────────────┘
💬 Comments
Quick Answer
Vault's PKI engine acts as a certificate authority, issuing short-lived TLS certificates from an intermediate CA with automated rotation. Services request certificates programmatically with TTLs as short as minutes. Preventing outages requires monitoring CA expiry, CRL size, issuance rates, and using cert-manager or ACME for automated renewal.
Detailed Answer
Think of an office building where instead of giving out permanent key cards that work forever, the security desk issues temporary visitor badges that expire every 8 hours. If a badge is stolen, it stops working by end of day without anyone having to manually deactivate it. Vault's PKI engine with short-lived certificates works the same way: certificates expire fast enough that revocation becomes less important, and issuance is fully automated.
Traditional certificate management involves a CA (often manual), long-lived certificates lasting 1-2 years, manual renewal, and revocation infrastructure (CRL/OCSP) for compromised certificates. This creates three problems at scale: outages when someone forgets to renew a certificate, large revocation lists when compromised certificates have long lifetimes, and bottlenecks when a security team manually processes certificate signing requests. Vault's PKI engine fixes all three by making certificate issuance programmatic, fast, and short-lived.
Under the hood, Vault's PKI engine supports a CA hierarchy. The root CA is typically kept offline or in a separate, heavily locked-down Vault mount. An intermediate CA is created within Vault and signed by the root CA. This intermediate CA issues the actual certificates your services use. When a service requests a certificate via vault write pki/issue/payments-tls, Vault generates a private key and certificate, signs it with the intermediate CA, and returns both with a TTL. The certificate and key are not stored in Vault after issuance (unless you configure that), making the issuance path stateless and fast. Vault supports RSA, ECDSA, and Ed25519 key types, and can enforce domain restrictions through allowed_domains, key usage constraints, and maximum TTL per role.
At production scale, teams set up cert-manager in Kubernetes to request certificates from Vault automatically, or use Vault's built-in ACME server (added in Vault 1.14) to let any ACME-compatible client get certificates. Short-lived certificates with 24-72 hour TTLs dramatically shrink the revocation problem: even if a certificate is stolen, it expires before a CRL update would have reached everyone in a traditional PKI setup. Monitoring needs to cover intermediate CA certificate expiry (the number one cause of PKI outages), CRL size growth, certificate issuance rate and errors, and storage backend performance under heavy issuance load.
The gotcha that causes the worst outages is intermediate CA rotation. The intermediate CA certificate has its own TTL (typically 1-5 years). If it expires, every certificate it issued becomes invalid, and every service using those certificates breaks at the same time. Vault does not rotate the intermediate CA automatically. You need alerts for intermediate CA expiry, automation for the CSR-sign-import cycle, and the new trust chain distributed to all TLS clients before the old intermediate expires. A second gotcha is CRL endpoint availability: if services check certificates against a CRL or OCSP endpoint hosted by Vault, then Vault's availability becomes part of the TLS handshake path, creating a circular dependency where you need Vault to be up to verify the certificates that connect you to Vault.
Code Example
# Enable the PKI secrets engine for the intermediate CA
vault secrets enable -path=pki_int pki
# Set the maximum TTL for certificates issued by this CA
vault secrets tune -max-lease-ttl=43800h pki_int
# Generate an intermediate CA CSR
vault write -format=json pki_int/intermediate/generate/internal \
common_name="Company Payments Intermediate CA" \
issuer_name="payments-intermediate-2026" \
key_type="ec" \
key_bits=256 | jq -r '.data.csr' > pki_int.csr
# Sign the CSR with the root CA (root CA may be in a separate Vault mount)
vault write -format=json pki/root/sign-intermediate \
csr=@pki_int.csr \
format=pem_bundle \
ttl=43800h | jq -r '.data.certificate' > signed_intermediate.pem
# Import the signed intermediate certificate back into Vault
vault write pki_int/intermediate/set-signed \
certificate=@signed_intermediate.pem
# Create a role for issuing short-lived service certificates
vault write pki_int/roles/payments-tls \
allowed_domains="payments.company.com,payments.svc.cluster.local" \
allow_subdomains=true \
allow_bare_domains=false \
max_ttl=72h \
ttl=24h \
key_type="ec" \
key_bits=256 \
require_cn=false \
generate_lease=true
# Issue a certificate for the payments API service
vault write pki_int/issue/payments-tls \
common_name="payments-api.payments.svc.cluster.local" \
alt_names="payments.company.com" \
ttl=24h
# Configure CRL and OCSP endpoints
vault write pki_int/config/urls \
issuing_certificates="https://vault.company.com:8200/v1/pki_int/ca" \
crl_distribution_points="https://vault.company.com:8200/v1/pki_int/crl" \
ocsp_servers="https://vault.company.com:8200/v1/pki_int/ocsp"
# Monitor intermediate CA expiry (critical operational check)
vault read pki_int/cert/ca -format=json | jq -r '.data.certificate' | openssl x509 -noout -enddate
# cert-manager ClusterIssuer for automated Kubernetes certificate management
apiVersion: cert-manager.io/v1 # cert-manager API for certificate automation
kind: ClusterIssuer # Cluster-wide certificate issuer
metadata:
name: vault-payments-issuer # Issuer name referenced by Certificate resources
spec:
vault:
server: https://vault.company.com:8200 # Vault server URL
path: pki_int/sign/payments-tls # Vault PKI sign endpoint for the role
auth:
kubernetes: # Uses Kubernetes service account for Vault auth
role: cert-manager # Vault Kubernetes auth role for cert-manager
mountPath: /v1/auth/kubernetes # Vault auth mount path
serviceAccountRef:
name: cert-manager # Service account with Vault auth permissionsInterview Tip
A junior engineer typically says Vault can issue TLS certificates and leaves it at that. For a senior or architect role, the interviewer wants to hear about CA hierarchy design and the operational lifecycle. Explain why the root CA should be offline or in a separate mount, how intermediate CA rotation is the most common cause of PKI outages because Vault does not automate it, why short-lived certificates reduce the need for revocation lists, and how CRL endpoint availability can create circular dependencies when Vault hosts both the CRL and the certificates protecting the connection to Vault. Strong answers also cover ACME support for non-Kubernetes clients, cert-manager integration patterns, and monitoring intermediate CA expiry as the single most important PKI operational metric.
◈ Architecture Diagram
┌──────────┐
│ Root CA │
│ (offline)│
└────┬─────┘
↓ signs
┌──────────┐
│ Int. CA │
│ (Vault) │
└────┬─────┘
↓ issues
┌──────────┐
│ Svc Cert │
│ TTL: 24h │
└────┬─────┘
↓
┌──────────┐
│payments │
│-api TLS │
└──────────┘💬 Comments
Quick Answer
Use Vault's Kubernetes auth method with per-service policies, dynamic secrets for databases, the Vault Agent sidecar for transparent secret injection, and dual-credential rotation for zero-downtime.
Detailed Answer
- Vault Kubernetes auth method: pods authenticate using their ServiceAccount JWT token - Each service gets a Vault role mapped to its ServiceAccount, with policies scoped to its secrets only - No static tokens or passwords stored anywhere
1. Dynamic database credentials (preferred): - Vault generates unique DB credentials per pod with TTL (e.g., 1 hour) - Credentials auto-expire — no rotation needed, no credential sharing between services - If compromised, blast radius is one pod's short-lived credential
2. Static secrets (API keys, third-party tokens): - Stored in Vault KV v2 (versioned) - Vault Agent sidecar or CSI driver injects secrets as files into the pod - Vault Agent watches for changes and hot-reloads when secrets rotate
3. PKI certificates: - Vault PKI secrets engine issues short-lived TLS certificates (24h TTL) - Auto-renewal via Vault Agent - mTLS between all services
1. Generate new credential (version N+1) in Vault 2. Both old (N) and new (N+1) credentials are valid simultaneously 3. Applications pick up new credential via Vault Agent file watch 4. After all pods have rotated (verify via audit log), revoke version N 5. Grace period ensures no request fails during the transition
- Vault audit logging to detect unauthorized access attempts - Sentinel policies to enforce naming conventions and TTL limits - Regular access reviews via Vault token accessor listing
Code Example
# Vault Kubernetes auth role (Terraform)
resource "vault_kubernetes_auth_backend_role" "order_service" {
backend = vault_auth_backend.kubernetes.path
role_name = "order-service"
bound_service_account_names = ["order-service"]
bound_service_account_namespaces = ["orders"]
token_policies = ["order-service-policy"]
token_ttl = 3600
}
# Dynamic DB credentials policy
path "database/creds/order-service-db" {
capabilities = ["read"]
}
path "secret/data/orders/*" {
capabilities = ["read"]
}
# Vault Agent sidecar annotation in pod spec
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "order-service"
vault.hashicorp.com/agent-inject-secret-db: "database/creds/order-service-db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "database/creds/order-service-db" -}}
postgresql://{{ .Data.username }}:{{ .Data.password }}@db:5432/orders
{{- end }}Interview Tip
Lead with dynamic secrets as the primary strategy — they eliminate rotation entirely. For static secrets, explain the dual-credential rotation pattern. Mention audit logging and Sentinel policies to show you think about governance, not just plumbing.
💬 Comments
Quick Answer
A sealed Vault has its storage backend encrypted at rest and cannot decrypt any secrets or serve any requests until it's unsealed by reconstructing the master encryption key — Vault seals itself on every startup (and can be sealed manually) as a security property: even someone with full access to the storage backend's raw data cannot read secrets without also performing the unseal process, which requires a quorum of separate key shares (or an auto-unseal mechanism) that no single person holds alone.
Detailed Answer
Vault encrypts all of its persisted data with a root encryption key, and that root key is itself never stored in plaintext — instead it's protected by Shamir's Secret Sharing, split into N key shares at initialization, with a threshold T of those shares required to reconstruct it (a common configuration is 5 shares, 3 required). On every process start, Vault has no way to derive the root key from the encrypted storage alone, so it starts sealed: vault status shows sealed=true, and every API call other than the unseal/status endpoints returns an error.
Unsealing means operators run vault operator unseal T times, each supplying one key share, until the threshold is met and Vault reconstructs the root key in memory (never writing it to disk). This is deliberately not a single password — the point is that no individual operator, and no single compromised credential, is sufficient to unseal Vault; you need collusion or compromise of a quorum of key holders.
For production HA deployments where manual unsealing after every restart isn't operationally acceptable, Vault supports auto-unseal mechanisms (cloud KMS like AWS KMS/GCP KMS/Azure Key Vault, or Vault's own Transit secrets engine acting as an unseal provider for another Vault cluster) which perform the unseal automatically using an external key management service instead of manual Shamir shares — trading a small amount of the 'no single point of trust' property for operational resilience, since restarts (e.g. during upgrades or node failures) no longer require paging someone with a key share.
Code Example
vault operator init -key-shares=5 -key-threshold=3 vault operator unseal <key-share-1> vault operator unseal <key-share-2> vault operator unseal <key-share-3>
Interview Tip
Mention Shamir's Secret Sharing by name and the shares/threshold concept — that's the specific mechanism interviewers are checking you actually understand, not just 'Vault needs an unseal key.'
💬 Comments
Quick Answer
Shamir unseal requires humans to manually supply key shares after every restart; auto-unseal delegates that to an external key management service (cloud KMS, or another Vault cluster's Transit engine) so Vault unseals itself automatically on startup. The failure mode unique to auto-unseal is that Vault now has an external dependency it needs to be available at startup — if the KMS key is deleted/inaccessible, or (for Transit auto-unseal) the upstream Vault's token has expired or that cluster is down, the dependent Vault cannot unseal at all, even with all the 'right' local configuration.
Detailed Answer
With Shamir unseal, the root key material never leaves Vault's own process memory and reconstructing it depends purely on humans supplying key shares — there's no external service dependency, but there IS an operational cost: every restart (planned maintenance, crash, node replacement) requires paging someone with key shares, which doesn't scale well for HA clusters that might restart nodes routinely.
Auto-unseal removes that human dependency by having Vault call out to an external key provider (AWS KMS, GCP Cloud KMS, Azure Key Vault, or Vault's own Transit secrets engine hosted by a separate 'seal' Vault cluster) to decrypt an internally-stored, KMS-wrapped copy of the root key automatically on startup — no human involvement needed for a routine restart.
The tradeoff is a new external dependency and its own failure modes: if the cloud KMS key is deleted, disabled, or the IAM permissions allowing Vault to call it are revoked, the dependent Vault cannot unseal even though its own storage and configuration are perfectly intact. Transit auto-unseal has a documented failure mode where the token Vault uses to authenticate to the upstream Transit-hosting Vault cluster expires or isn't renewed correctly — if that token becomes invalid, the seal wrapper health check starts failing and the dependent Vault cannot complete its unseal on a subsequent restart, effectively creating a hard dependency on the upstream Vault cluster's own availability and the auto-unseal token's lifecycle being actively managed. Production deployments using Transit auto-unseal need their own monitoring on that token's validity and the upstream cluster's health, since a failure there is invisible until the exact moment you need to restart the dependent Vault.
Interview Tip
Bring up the Transit auto-unseal token expiry failure mode specifically — it's a real, documented operational gotcha that shows you've read past the marketing description of auto-unseal's convenience.
💬 Comments
Quick Answer
Static secrets are stored values Vault returns as-is (like a KV secret you wrote yourself); dynamic secrets are generated on-demand by Vault at request time — for a database secrets engine, Vault actually creates a brand-new, short-lived database user/credential pair for each request, with a defined TTL after which Vault automatically revokes it. This is preferred for database credentials because it eliminates long-lived shared passwords, gives each consumer (each app instance, each CI job) its own uniquely-attributable credential, and bounds the blast radius of a leaked credential to its TTL.
Detailed Answer
A static secret in Vault's KV engine is just an encrypted key-value blob Vault stores and returns unchanged — useful for things like API keys issued by a third party that Vault doesn't control, but if that KV value leaks, it's valid until someone manually rotates it, and there's no way to tell which consumer leaked it if many services share the same static secret.
A dynamic secrets engine (database, AWS, PKI/certificates, etc.) instead has Vault actively call out to the backing system to CREATE a new credential at request time — for the database engine, Vault connects to the database using its own admin/root credential and runs a configured creation statement (e.g. CREATE ROLE ... WITH PASSWORD ... VALID UNTIL ...) to provision a brand-new, unique username and password, which it returns to the requester along with a lease and TTL. When the lease expires (or is explicitly revoked), Vault runs a corresponding revocation statement to drop that specific database role.
This gives three concrete production benefits: (1) attributable credentials — because every consumer gets its own uniquely-named database user, audit logs on the database side can show exactly which application instance or CI run performed which queries, rather than everything being attributed to one shared service account; (2) automatic expiry — a leaked dynamic credential is only useful until its TTL lapses, versus a static password that's valid indefinitely until someone notices and rotates it; (3) no credential ever needs to be written into application config or CI secrets at all — the application/pipeline authenticates to Vault (via its own auth method — Kubernetes service account, AppRole, etc.) and requests a fresh credential each time it needs one, so the actual database password is never stored anywhere outside Vault's own lease system.
Code Example
vault write database/roles/my-role \
db_name=postgres \
creation_statements="CREATE ROLE '{{name}}' WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" max_ttl="24h"
vault read database/creds/my-roleInterview Tip
Emphasize attributability (audit trail per-consumer) as a benefit alongside expiry — many candidates only mention the TTL and miss the equally important per-consumer audit angle.
💬 Comments
Quick Answer
AppRole authenticates non-human clients using two pieces of information — a RoleID (like a username, relatively non-secret, tied to a specific policy set) and a SecretID (like a one-time or short-lived password, generated separately and often delivered through a different secure channel) — which together exchange for a Vault token. This split delivery model (RoleID baked into config/CI job, SecretID injected separately at runtime, e.g. via a CI secret store) avoids embedding a single long-lived Vault token or credential directly into pipeline configuration.
Detailed Answer
AppRole is Vault's recommended auth method for applications and automated systems (as opposed to human users, who'd typically use LDAP, OIDC, or similar). A RoleID identifies which AppRole (and therefore which Vault policies/permissions) is being used — it's not treated as highly secret and is often just checked into the pipeline configuration. The SecretID is the actual authentication credential and IS treated as sensitive; it can be configured with its own TTL, a limited number of uses, and CIDR restrictions on which IPs can use it.
The common CI pattern: the RoleID lives in the pipeline's config (low sensitivity), while the SecretID is generated per-run (or per some rotation policy) and delivered through the CI platform's own secret injection mechanism (e.g. a CI-native secret variable), so no long-lived Vault token is ever hardcoded anywhere. The pipeline calls vault write auth/approle/login role_id=... secret_id=... to exchange these for a short-lived Vault token scoped to whatever policies the AppRole is bound to, uses that token for the duration of the job, and the token expires on its own afterward.
This is preferred over embedding a static, long-lived Vault token directly in CI config because: the RoleID+SecretID split limits what a config leak alone exposes (RoleID without a valid SecretID grants nothing), SecretIDs can be constrained (limited uses, TTL, CIDR-bound) far more tightly than a general-purpose token, and different pipelines/services can have distinct AppRoles with least-privilege policies rather than sharing one broad credential.
Code Example
vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"
Interview Tip
Clarify explicitly that RoleID is low-sensitivity and SecretID is the real secret — mixing this up is the most common mistake candidates make when explaining AppRole.
💬 Comments
Quick Answer
Every dynamic secret Vault issues comes with a lease (a TTL and a renewal/revocation contract), and Vault tracks all outstanding leases so it can revoke the underlying credential (e.g. drop a database role) when the lease expires or is explicitly revoked. At scale, if Vault's lease count grows faster than leases are being revoked/expired (e.g. a bug causing excessive credential issuance, or revocation itself failing against a struggling backend), Vault's lease storage and revocation workload can grow large enough to degrade Vault's own performance and slow down new secret issuance.
Detailed Answer
Every response from a dynamic secrets engine includes lease metadata (lease_id, lease_duration, renewable) and Vault persists that lease in its own storage backend alongside a scheduled expiration. A background process continually checks for expired leases and calls the corresponding secrets engine's revocation logic (e.g. connecting to the database to DROP the specific role that was created for that lease). Clients can also proactively renew a lease before it expires if they still need the credential, up to the secret's configured max_ttl.
At scale, two related problems show up in real deployments: (1) lease/credential sprawl — if an application has a bug that requests a new dynamic credential far more often than necessary (e.g. per-request instead of once at startup, or a retry loop that doesn't reuse an existing lease), the sheer volume of outstanding leases and the corresponding load on both Vault's storage backend and the target system (e.g. a database straining under constant CREATE/DROP ROLE churn) can become a self-inflicted incident; (2) revocation backlog — if the backing system (database, cloud provider API) is itself degraded or rate-limiting Vault's revocation calls, expired leases can pile up faster than they're cleaned up, growing Vault's lease storage and potentially leaving stale credentials valid longer than intended if revocation keeps failing silently in the background.
Operationally, teams monitor vault_expire_num_leases (or equivalent) as a capacity signal, cap max_ttl aggressively on secrets engines to bound the blast radius of any credential issuance bug, and treat repeated revocation failures against a backend as a page-worthy signal rather than a silently-retried background task.
Interview Tip
The distinction between 'lease sprawl from over-issuance' and 'revocation backlog from a struggling backend' as two separate failure modes is what shows real operational depth here.
💬 Comments
Quick Answer
Write narrow, path-scoped policies per service (or per service-role) granting only the specific secret paths and capabilities (read, create, update, list) each one genuinely needs, and bind those policies to auth method roles scoped to that service's identity (e.g. its Kubernetes service account). A common anti-pattern is a single broad policy — e.g. granting read access to secret/* — shared across many services for convenience, which means a single compromised service can read every other service's secrets.
Detailed Answer
Vault policies are written in HCL, granting specific capabilities (read, create, update, delete, list, sudo, deny) on specific path patterns, and a token's effective permissions are the union of every policy attached to it. Good least-privilege design scopes each policy as narrowly as the service's actual needs: a payments service's policy should grant access to secret/data/payments/* and the specific database role it uses, not a broad secret/data/*.
Those narrow policies are then bound to an auth method role tied to the service's actual runtime identity — for Kubernetes workloads, the Kubernetes auth method binds a policy to a specific Kubernetes service account (and optionally namespace), so only pods running as that service account can authenticate into a token carrying that policy; for CI, an AppRole per pipeline/service serves the same purpose.
The anti-pattern that shows up repeatedly in real environments: teams under time pressure grant a single shared, broad policy (often literally path "secret/*" { capabilities = ["read"] }) to many services 'temporarily' to unblock a launch, and it never gets narrowed afterward because doing so risks breaking something nobody wants to touch. The consequence is that compromising any one of those services (a dependency vulnerability, a leaked token) gives an attacker read access to every other service's secrets too, turning a single-service compromise into an organization-wide secrets breach. Vault's own audit log and policy-review tooling (or third-party policy linting) should be used periodically to catch and narrow overly broad grants before they're needed in anger.
Code Example
path "secret/data/payments/*" {
capabilities = ["read"]
}
path "database/creds/payments-role" {
capabilities = ["read"]
}Interview Tip
Give the concrete failure story — one compromised service reading every other service's secrets due to a shared broad policy — rather than just reciting 'least privilege' as a buzzword.
💬 Comments
Quick Answer
Centralized secrets management: it stores, generates, rotates, and audits access to secrets instead of scattering them in configs.
Detailed Answer
Vault provides a single API for static secrets (KV), dynamic short-lived credentials (databases, cloud), encryption-as-a-service (transit), and PKI. Everything is authenticated, authorized by policy, leased with a TTL, and audited — replacing plaintext secrets in env vars and Git.
Interview Tip
Highlight dynamic, short-lived secrets as the differentiator.
💬 Comments
Quick Answer
Vault generates credentials on demand with a short TTL and revokes them automatically, minimizing exposure.
Detailed Answer
Instead of one long-lived DB password shared everywhere, Vault's database secrets engine creates a unique username/password per request, valid for minutes/hours, then revokes it. A leaked credential is useless after its lease expires, and every issuance is audited — a huge blast-radius reduction.
Interview Tip
Blast-radius reduction via short TTLs is the key idea.
💬 Comments
Quick Answer
Clients authenticate via an auth method to get a token; policies attached to that token grant path-scoped capabilities.
Detailed Answer
Auth methods (Kubernetes, AWS IAM, AppRole, OIDC) verify identity and issue a token. Policies written in HCL grant capabilities (read/create/update/delete/list) on specific paths. This decouples identity from permissions and enforces least privilege per path.
Code Example
path "secret/data/app/*" {
capabilities = ["read"]
}Interview Tip
Auth method = identity; policy = permissions.
💬 Comments
Quick Answer
Vault starts sealed with its data encrypted; unsealing reconstructs the master key (via Shamir shares or auto-unseal) to decrypt storage.
Detailed Answer
The root/master key that encrypts Vault's storage is itself protected. On start Vault is sealed and unusable until unsealed — either by providing a threshold of Shamir key shares held by different operators, or via auto-unseal backed by a cloud KMS/HSM. Sealing protects data at rest if the host is compromised.
Interview Tip
Mention auto-unseal with a cloud KMS for production.
💬 Comments
Quick Answer
They authenticate with the Kubernetes auth method (service account JWT) and fetch secrets via the Agent injector, CSI driver, or API.
Detailed Answer
The pod's service account token proves identity to Vault's Kubernetes auth method, which returns a token scoped by policy. The Vault Agent Sidecar Injector or the Secrets Store CSI driver then materializes secrets into the pod (files or env) without baking them into images — dynamic, audited, and revocable.
Interview Tip
Name the injector and CSI driver approaches.
💬 Comments
Context
A platform running dozens of microservices had every service connecting to its database using a long-lived, shared credential stored in a config management system, rotated manually (rarely) by whoever remembered to do it. A security audit flagged this as a major risk: credentials were over-shared, rarely rotated, and impossible to attribute to a specific service in database audit logs.
Problem
Rotating a long-lived shared credential required coordinated updates across every consuming service's configuration and a maintenance window, so rotation happened far less often than security policy required. When database-level anomalies were investigated, there was no way to tell which service or instance had issued a particular query, since they all authenticated as the same database user.
Solution
Configure Vault's database secrets engine against each database, with distinct roles per consuming service defining least-privilege creation statements (e.g. a payments-service role that can only create roles with access to payments tables). Each service authenticates to Vault using its own identity (Kubernetes auth method bound to its service account) and requests a fresh dynamic database credential at startup, with a bounded TTL, rather than reading a static password from configuration at all.
Commands
vault write database/config/my-postgres plugin_name=postgresql-database-plugin connection_url="postgresql://{{username}}:{{password}}@db:5432/app" allowed_roles="payments-role"vault write database/roles/payments-role db_name=my-postgres creation_statements="CREATE ROLE '{{name}}' WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT ON payments TO '{{name}}';" default_ttl="1h"Outcome
Every database connection is now attributable to a specific, automatically-expiring, per-service-instance credential, static passwords were removed from configuration management entirely, and credential rotation became continuous (bounded by TTL) rather than a rare manual event requiring a maintenance window.
Lessons Learned
Migrating to dynamic secrets surfaced applications with poor connection-handling patterns — some services opened a new database connection (and thus requested a new Vault credential) far more often than necessary, which became a real incident once traffic grew (see the related lease-sprawl production issue). Rolling out dynamic secrets is a good forcing function to also audit and fix connection pooling and credential caching behavior, not just a drop-in replacement for static passwords.
💬 Comments
Context
A CI/CD platform previously had a single long-lived Vault token with broad read permissions stored as a CI-level secret variable, used by every pipeline across every team to fetch whatever secrets each pipeline happened to need.
Problem
The shared long-lived token meant any pipeline (and, by extension, any engineer with access to trigger or modify a pipeline) effectively had the same broad Vault access as every other pipeline, with no per-pipeline attribution in Vault's audit log and no reasonable way to rotate or scope the token without breaking every pipeline simultaneously.
Solution
Create a distinct AppRole per pipeline/team with a policy scoped only to the secret paths that specific pipeline genuinely needs. Each pipeline's RoleID is checked into its own pipeline configuration (low sensitivity), while a short-lived SecretID is generated and injected at runtime through the CI platform's native secret-injection mechanism, rotated on a defined schedule. Pipelines exchange RoleID+SecretID for a short-lived Vault token scoped to their own AppRole's policy at the start of each run.
Commands
vault write auth/approle/role/ci-payments-pipeline policies="ci-payments-policy" secret_id_ttl="24h" token_ttl="1h"
vault write auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"
Outcome
Each pipeline's Vault access is now individually scoped, attributable in the audit log, and independently revocable without affecting other teams' pipelines. The single broad shared token was retired entirely, closing off the single-point-of-compromise risk it represented.
Lessons Learned
The RoleID/SecretID split is only as strong as how the SecretID is delivered — the team specifically avoided storing SecretIDs as static, long-lived CI variables (which would recreate the same long-lived-credential problem one level down) and instead generates/rotates them on a schedule tied to the CI platform's own secret management, treating SecretID delivery with the same care as any other production credential.
💬 Comments
Symptom
After a node restart, Vault stayed sealed and applications began failing secret reads within eight minutes.
Error Message
Error unsealing: error decrypting seal stored keys: access denied
Root Cause
The cluster used auto-unseal backed by a cloud KMS key. A security cleanup removed the Vault service principal from the KMS key policy. Running nodes stayed unsealed, so the mistake was invisible until a restart required the seal mechanism. Recovery keys could authorize recovery operations but could not decrypt the root key without the seal backend. The underlying issue was a combination of factors that individually seemed harmless but together created a cascading failure. The monitoring that should have caught this early was either missing or configured with thresholds too high to trigger before user impact. The on-call engineer initially pursued the wrong hypothesis because the symptoms resembled a different, more common failure mode. By the time the real root cause was identified, the blast radius had expanded beyond the original service to affect downstream dependencies. This incident exposed a gap in the team's runbooks and highlighted the need for better correlation between metrics, logs, and traces during diagnosis.
Diagnosis Steps
Solution
Restore KMS access for the Vault identity, restart one standby first, and verify auto-unseal before touching the active node. Do not rotate or delete the KMS key during diagnosis.
Commands
vault status
systemctl restart vault
vault operator raft list-peers
Prevention
Protect KMS keys with change control. Alert on sealed nodes. Test restart and auto-unseal in every DR exercise. Document that recovery keys are not a replacement for the seal backend.
◈ Architecture Diagram
┌──────────┐
│ Trigger │
└────┬─────┘
↓
┌──────────┐
│ Incident │
└────┬─────┘
↓
┌──────────┐
│ Verify │
└──────────┘💬 Comments
Symptom
A Vault cluster configured with Transit auto-unseal (using a separate upstream Vault cluster's Transit engine) was restarted for a routine patch. It failed to unseal automatically, and manual investigation revealed repeated warnings that the seal wrapper token had become invalid.
Error Message
seal wrapper health check failed; permission denied errors from the upstream Transit-hosting Vault cluster when the dependent cluster attempted to decrypt its stored root key material
Root Cause
The token the dependent Vault cluster used to authenticate to the upstream Transit-hosting Vault had expired and was not being renewed, either because the token's TTL/renewal policy wasn't configured for the cluster's actual restart cadence, or because a renewal process had silently stopped working. Auto-unseal via Transit depends entirely on that token remaining valid; once it expired, the dependent cluster had no way to complete its unseal on restart, despite its own storage and configuration being intact.
Diagnosis Steps
Solution
Generate a new valid token on the upstream Transit-hosting Vault with the correct policy for seal-wrapping operations, update the dependent cluster's auto-unseal configuration/token reference, and restart to confirm successful automatic unsealing.
Commands
vault status
vault token lookup <seal-token>
vault token renew <seal-token>
Prevention
Monitor the seal-wrapping token's validity and remaining TTL as a first-class operational metric with alerting well before expiry, automate its renewal with its own health check rather than assuming a one-time setup is permanent, and periodically test a full restart of the dependent cluster in a non-production environment to catch a broken auto-unseal path before it's needed during a real production restart.
💬 Comments
Symptom
A service began experiencing intermittent database connection failures and slow query performance under otherwise normal traffic. Around the same time, the database's own connection and role-creation metrics spiked far beyond what the service's actual request volume would explain.
Error Message
database connection pool exhausted; database server logs showing an unusually high rate of CREATE ROLE / DROP ROLE statements; Vault's vault_expire_num_leases metric climbing steadily
Root Cause
A retry loop in the application's database-connection initialization code did not reuse an already-issued Vault dynamic credential on transient connection failures — instead, each retry requested a brand-new dynamic secret from Vault's database secrets engine, each of which caused Vault to create (and later have to revoke) a new database role. Under a period of transient network flakiness, this multiplied into thousands of unnecessary role creations in a short window, straining the database's own capacity for role management operations and its connection limits.
Diagnosis Steps
Solution
Patch the application to cache and reuse its current Vault-issued database credential and only request a new one when the existing lease is genuinely expired or explicitly revoked, add backoff to the retry logic, and manually revoke the large number of orphaned leases to relieve immediate database load.
Commands
vault list database/creds
vault lease revoke -prefix database/creds/my-role
vault read sys/metrics | grep expire_num_leases
Prevention
Cap max_ttl and set reasonable default_ttl on the database secrets engine role to bound how often legitimate renewal is even possible, monitor per-service lease issuance rate as a standard dashboard (not just total lease count), and require credential-caching/reuse patterns as a standard part of any service's Vault integration review before production rollout.
💬 Comments
Symptom
During a security review following an unrelated dependency vulnerability in a low-traffic internal tool, the team discovered that the compromised service's Vault token had read access to secret paths belonging to several unrelated, higher-sensitivity production services — access it had no legitimate reason to need.
Error Message
Vault audit log showing successful reads from secret/data/payments/* and secret/data/user-pii/* by a token bound to the low-risk internal tool's AppRole, none of which were expected or required by that tool
Root Cause
Months earlier, under launch time pressure, a broad policy (granting read on secret/data/* rather than a scoped subpath) had been attached to several services' AppRoles as a temporary convenience during initial rollout, intended to be narrowed afterward. That follow-up narrowing never happened, and the broad policy remained in place indefinitely, meaning any one of those services being compromised effectively exposed every other service's secrets under that same policy.
Diagnosis Steps
Solution
Immediately narrow the shared broad policy to the minimum path scope each affected service actually needs, rotate any secrets that were plausibly exposed during the compromise window (even without direct evidence of exfiltration, given the access existed), and issue new AppRole credentials to each affected service under its newly scoped policy.
Commands
vault audit list
vault policy read shared-broad-policy
vault token capabilities <token> secret/data/payments/config
Prevention
Treat 'temporary' broad grants as tracked technical debt with an owner and a deadline rather than an indefinite convenience, run periodic automated policy audits comparing granted path scope against actual audit-log access patterns to flag unused-but-granted access, and require security review sign-off before any policy grants a wildcard path scope in production.
💬 Comments