HashiCorp Vault Hardening Guide
Secrets management security including auth methods, policies, and audit logging
Overview
HashiCorp Vault is the industry-standard secrets management solution used enterprise-wide for database credentials, API keys, PKI certificates, and dynamic secrets. The Codecov breach (2021) exposed HashiCorp’s GPG signing key through supply chain attack, forcing rotation of all signing keys and validation of all software releases. CI/CD integrations with CircleCI, GitLab, and Jenkins create numerous OAuth and token-based access points.
Intended Audience
- Security engineers managing secrets infrastructure
- DevOps engineers configuring Vault integrations
- GRC professionals assessing secrets management compliance
- Platform teams implementing zero-trust architectures
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Vault-specific security configurations including authentication methods, secrets engine hardening, audit logging, and CI/CD integration security.
Table of Contents
- Authentication & Access Controls
- Secrets Engine Security
- Network & API Security
- Audit Logging
- CI/CD Integration Security
- Operational Security
- Host & Platform Hardening
- Compliance Quick Reference
1. Authentication & Access Controls
1.1 Implement Least-Privilege Auth Methods
Profile Level: L1 (Crawl) CIS Controls: 6.3, 6.8 NIST 800-53: AC-6, IA-2
Description
Configure Vault authentication methods appropriate to each use case. Avoid using root tokens for regular operations; implement workload identity where possible.
Rationale
Why This Matters:
- Root tokens provide unlimited access
- Long-lived tokens create persistent risk
- Workload identity eliminates stored secrets
Attack Prevented: Token theft, credential stuffing, privilege escalation
Real-World Incidents:
- Codecov Breach (2021): Compromised CI environment extracted secrets, including HashiCorp’s GPG signing key
Prerequisites
- Vault cluster deployed and initialized
- Authentication backends configured
- Policy structure designed
- Identity provider integration (for OIDC)
ClickOps Implementation
Step 1: Disable Root Token After Initial Setup
- Revoke the root token after initial configuration
- Create an admin-emergency policy for break-glass scenarios
- Generate emergency tokens with short TTLs and use limits
Step 2: Configure OIDC for User Authentication
- Enable the OIDC auth method
- Configure OIDC with your identity provider (Okta, Azure AD, etc.)
- Create role mappings with bound audiences and redirect URIs
Step 3: Configure AppRole for Applications
- Enable the AppRole auth method
- Create roles with limited TTLs and SecretID constraints
- Bind roles to specific CIDRs (L2)
Validation & Testing
- Attempt to use root token - should be revoked
- Login via OIDC - should succeed with appropriate policies
- AppRole authentication - verify CIDR binding works
- Check token TTLs are enforced
Expected result: Each auth method provides minimal required access
Monitoring & Maintenance
Maintenance schedule:
- Weekly: Review failed authentication attempts
- Monthly: Audit auth method configurations
- Quarterly: Rotate AppRole SecretIDs
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | IA-2, IA-5 | Authentication and token management |
| ISO 27001 | A.9.2.1 | User registration and de-registration |
Code Pack: Terraform
# --- OIDC Auth Method (preferred for human users) ---
resource "vault_jwt_auth_backend" "oidc" {
description = "OIDC authentication for human users"
path = "oidc"
type = "oidc"
oidc_discovery_url = var.oidc_discovery_url
oidc_client_id = var.oidc_client_id
oidc_client_secret = var.oidc_client_secret
default_role = "default"
tune {
default_lease_ttl = "1h"
max_lease_ttl = "4h"
token_type = "default-service"
}
}
resource "vault_jwt_auth_backend_role" "oidc_default" {
backend = vault_jwt_auth_backend.oidc.path
role_name = "default"
role_type = "oidc"
token_policies = ["default"]
user_claim = "email"
groups_claim = "groups"
allowed_redirect_uris = var.oidc_redirect_uris
token_ttl = 3600
token_max_ttl = 14400
}
# --- AppRole Auth Method (for CI/CD and automation) ---
resource "vault_auth_backend" "approle" {
type = "approle"
path = "approle"
description = "AppRole authentication for CI/CD pipelines and automation"
tune {
default_lease_ttl = "30m"
max_lease_ttl = "1h"
}
}
resource "vault_approle_auth_backend_role" "ci_cd" {
backend = vault_auth_backend.approle.path
role_name = "ci-cd"
token_policies = ["ci-cd-read"]
token_ttl = 1800
token_max_ttl = 3600
secret_id_num_uses = 1
secret_id_ttl = 600
token_num_uses = 10
}
# --- Kubernetes Auth Method (for workloads running in K8s) ---
resource "vault_auth_backend" "kubernetes" {
type = "kubernetes"
path = "kubernetes"
description = "Kubernetes authentication for in-cluster workloads"
tune {
default_lease_ttl = "15m"
max_lease_ttl = "1h"
}
}
resource "vault_kubernetes_auth_backend_config" "k8s" {
backend = vault_auth_backend.kubernetes.path
kubernetes_host = var.kubernetes_host
kubernetes_ca_cert = var.kubernetes_ca_cert
issuer = var.kubernetes_issuer
}
Code Pack: API Script
# Check for root token usage (critical finding)
info "1.1 Checking for root token usage..."
TOKEN_INFO=$(vault_get "/auth/token/lookup-self" 2>/dev/null || echo '{}')
TOKEN_POLICIES=$(echo "${TOKEN_INFO}" | jq -r '.data.policies // [] | join(",")' 2>/dev/null || echo "")
if echo "${TOKEN_POLICIES}" | grep -q "root"; then
fail "1.1 CRITICAL: Current token has root policy -- rotate to a scoped admin token immediately"
warn "1.1 Root tokens should only be used for break-glass emergency procedures"
increment_failed
else
pass "1.1 Current token does not use root policy"
fi
# List all enabled auth methods
info "1.1 Listing enabled auth methods..."
AUTH_METHODS=$(vault_get "/sys/auth" 2>/dev/null || echo '{}')
AUTH_PATHS=$(echo "${AUTH_METHODS}" | jq -r '.data // . | to_entries[] | select(.value.type?) | "\(.key) (\(.value.type))"' 2>/dev/null || true)
if [ -n "${AUTH_PATHS}" ]; then
pass "1.1 Enabled auth methods:"
echo "${AUTH_PATHS}" | while read -r line; do
echo " - ${line}"
done
else
fail "1.1 Could not retrieve auth methods -- check token permissions"
increment_failed
fi
# Verify OIDC is configured (preferred for human users)
info "1.1 Checking for OIDC auth method..."
OIDC_ENABLED=$(echo "${AUTH_METHODS}" | jq -r '.data // . | to_entries[] | select(.value.type == "oidc") | .key' 2>/dev/null || true)
if [ -n "${OIDC_ENABLED}" ]; then
pass "1.1 OIDC auth method enabled at path: ${OIDC_ENABLED}"
# Verify OIDC configuration is complete
OIDC_CONFIG=$(vault_get "/auth/oidc/config" 2>/dev/null || echo '{}')
OIDC_DISCOVERY=$(echo "${OIDC_CONFIG}" | jq -r '.data.oidc_discovery_url // empty' 2>/dev/null || true)
if [ -n "${OIDC_DISCOVERY}" ]; then
pass "1.1 OIDC discovery URL configured: ${OIDC_DISCOVERY}"
else
warn "1.1 OIDC auth method enabled but discovery URL not configured"
fi
else
warn "1.1 OIDC auth method not enabled -- recommended for human user authentication"
warn "1.1 Enable with: vault auth enable oidc"
fi
# Check for deprecated or insecure auth methods
info "1.1 Checking for deprecated auth methods..."
USERPASS_ENABLED=$(echo "${AUTH_METHODS}" | jq -r '.data // . | to_entries[] | select(.value.type == "userpass") | .key' 2>/dev/null || true)
if [ -n "${USERPASS_ENABLED}" ]; then
warn "1.1 Userpass auth method detected at: ${USERPASS_ENABLED}"
warn "1.1 Consider migrating to OIDC for stronger authentication guarantees"
fi
Code Pack: CLI Script
# After initial configuration, revoke root token
vault token revoke <root-token>
# Create admin policy for emergency use
vault policy write admin-emergency - <<EOF
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOF
# Create emergency token with TTL
vault token create -policy=admin-emergency -ttl=1h -use-limit=5
# Enable OIDC auth method
vault auth enable oidc
# Configure OIDC with your IdP
vault write auth/oidc/config \
oidc_discovery_url="https://your-idp.okta.com" \
oidc_client_id="$CLIENT_ID" \
oidc_client_secret="$CLIENT_SECRET" \
default_role="default"
# Create role mapping
vault write auth/oidc/role/default \
bound_audiences="$CLIENT_ID" \
allowed_redirect_uris="https://vault.company.com/ui/vault/auth/oidc/oidc/callback" \
allowed_redirect_uris="http://localhost:8250/oidc/callback" \
user_claim="email" \
groups_claim="groups" \
policies="default"
# Enable AppRole
vault auth enable approle
# Create role with limited TTL
vault write auth/approle/role/jenkins \
token_policies="jenkins-secrets" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=24h \
secret_id_num_uses=10
# Bind to specific CIDR (L2)
vault write auth/approle/role/jenkins \
token_bound_cidrs="10.0.0.0/8" \
secret_id_bound_cidrs="10.0.0.0/8"
# Monitor auth method usage
vault read sys/auth
# Check token counts by auth method
vault read sys/internal/counters/tokens
Code Pack: Sigma Detection Rule
detection:
selection_root_token:
auth.policies|contains: 'root'
selection_auth_changes:
request.path|startswith: 'sys/auth'
request.operation:
- 'update'
- 'delete'
condition: selection_root_token or selection_auth_changes
fields:
- auth.display_name
- auth.policies
- request.path
- request.operation
- request.remote_address
- time
1.2 Implement Granular Policies
Profile Level: L1 (Crawl) NIST 800-53: AC-3, AC-6
Description
Create fine-grained policies limiting access to specific paths. Avoid wildcard policies that grant excessive access.
Rationale
Why This Matters:
- Vault policies are deny-by-default, so a wildcard or overly broad policy silently grants access to every secret path it matches
- A token scoped by a tight policy can only reach the handful of paths it needs, containing the blast radius if it is stolen
- Path-scoped capabilities (read vs. create vs. update vs. sudo) let you grant just enough access rather than full control of a mount
- Separating base, team, and application policies makes access auditable and prevents privilege creep as teams add new secrets
- Identity-templated policies that leaned on glob expansion no longer behave the same way — Vault 2.0.0 rejects them, so a policy that silently over-granted before will now fail closed on upgrade
Attack Prevented: Privilege escalation, lateral movement, over-broad secret access, blast-radius expansion
Changed Default (Vault 2.0.0): Vault 2.0.0 rejects the + and * wildcards in the rendered path of an identity-templated policy, and rejects uncleaned paths containing /../, /./, or //. A policy such as path "secret/data/{{identity.entity.name}}/*" still works because the wildcard is outside the template, but any policy whose template expands into a wildcard, or whose rendered path is not already clean, is refused at write time and breaks on upgrade. Rewrite affected policies to explicit paths before upgrading. (Important changes)
ClickOps Implementation
Step 1: Create Hierarchical Policy Structure
- Create a base read-only policy for all authenticated users
- Create team-specific policies scoped to team secret paths
- Create application policies with the most restrictive access
Step 2: Audit Templated Policies Before Upgrading to 2.0.0
- List every policy with
vault policy listand read each withvault policy read <name> - Flag any policy containing
{{identity.whose substituted value could contain+,*,..,., or an empty segment - Flag any path containing
/../,/./, or a doubled// - Rewrite flagged policies to explicit, fully-qualified paths — one
pathblock per real destination rather than one glob standing in for many - Re-write each corrected policy with
vault policy write <name> <file>and confirm it is accepted
Validation & Testing
vault policy writeon a rewritten policy succeeds without a path-validation error- A token bound to the rewritten policy can still read every path it legitimately needs
- The same token is denied on a path the old glob would have matched but the new explicit list does not
Expected result: No policy depends on wildcard expansion inside a rendered identity template, and no rendered path requires cleaning.
Code Pack: Terraform
# --- Base read-only policy for all authenticated users ---
resource "vault_policy" "base_read" {
name = "base-read"
policy = <<-EOT
# Allow lookup of own token capabilities
path "sys/capabilities-self" {
capabilities = ["update"]
}
# Allow reading own identity
path "auth/token/lookup-self" {
capabilities = ["read"]
}
# Allow renewing own token
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Deny access to root-level system paths
path "sys/raw/*" {
capabilities = ["deny"]
}
path "sys/seal" {
capabilities = ["deny"]
}
EOT
}
# --- Team-scoped policy (per-team secret paths) ---
resource "vault_policy" "team_secrets" {
for_each = var.team_names
name = "team-${each.value}-secrets"
policy = <<-EOT
# Read and write secrets under team namespace
path "secret/data/teams/${each.value}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/teams/${each.value}/*" {
capabilities = ["list", "read", "delete"]
}
# Deny cross-team access
path "secret/data/teams/*" {
capabilities = ["deny"]
}
EOT
}
# --- CI/CD read-only policy (AppRole consumers) ---
resource "vault_policy" "ci_cd_read" {
name = "ci-cd-read"
policy = <<-EOT
# Read-only access to application secrets
path "secret/data/apps/+/config" {
capabilities = ["read"]
}
# Read dynamic database credentials
path "database/creds/*" {
capabilities = ["read"]
}
# Read PKI certificates
path "pki/issue/*" {
capabilities = ["read", "update"]
}
# Deny all write operations on secret engines
path "secret/data/*" {
capabilities = ["deny"]
}
EOT
}
# --- Admin policy (Vault operators) ---
resource "vault_policy" "admin" {
name = "vault-admin"
policy = <<-EOT
# Manage auth methods
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Manage policies
path "sys/policies/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Manage secret engines
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Read audit configuration
path "sys/audit*" {
capabilities = ["read", "list", "sudo"]
}
# Manage identity entities and groups
path "identity/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Deny seal/unseal (requires root or auto-unseal)
path "sys/seal" {
capabilities = ["deny"]
}
path "sys/raw/*" {
capabilities = ["deny"]
}
EOT
}
Code Pack: CLI Script
# Base policy - all authenticated users
vault policy write base - <<EOF
path "secret/data/shared/*" {
capabilities = ["read", "list"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
EOF
# Team-specific policy
vault policy write team-platform - <<EOF
path "secret/data/platform/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "aws/creds/platform-deploy" {
capabilities = ["read"]
}
EOF
# Application policy (most restrictive)
vault policy write app-frontend - <<EOF
path "secret/data/frontend/config" {
capabilities = ["read"]
}
path "database/creds/frontend-readonly" {
capabilities = ["read"]
}
EOF
1.3 Enable Entity and Group Management
Profile Level: L2 (Walk) NIST 800-53: AC-2
Description
Use Vault’s identity system to manage users and groups across auth methods, enabling consistent policy application.
Rationale
Why This Matters:
- Vault entities tie multiple auth-method aliases (OIDC, LDAP, AppRole) to one identity, so policy is applied consistently no matter how a user logs in
- Group-based policy assignment lets you change or revoke access for many users at once instead of editing tokens individually
- Mapping external IdP groups to Vault groups keeps authorization in sync with joiner-mover-leaver processes, removing orphaned access automatically
- Entity-level audit data attributes every request to a real human or workload, which is essential for investigation and accountability
Attack Prevented: Orphaned-account access, inconsistent authorization, privilege drift, untraceable activity
Code Pack: CLI Script
# Create identity group
vault write identity/group \
name="platform-team" \
policies="team-platform" \
member_entity_ids=""
# Create entity for user
vault write identity/entity \
name="john.doe@company.com" \
policies="base"
# Link OIDC alias to entity
vault write identity/entity-alias \
name="john.doe@company.com" \
canonical_id="<entity-id>" \
mount_accessor="<oidc-accessor>"
1.4 Patch the 2025 Authentication, Identity, and Authorization CVE Class
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 7.1, 7.3 |
| NIST 800-53 | SI-2, AC-6 |
Description
In 2025, security research firm Cyata disclosed nine zero-day vulnerabilities in Vault’s authentication, identity, and authorization layers – including the first publicly documented remote code execution in Vault’s history. Run a version that carries all of the resulting fixes, then add the compensating detections and surface reductions that limit what a future bug in the same layers can reach.
Rationale
Why This Matters:
- CVE-2025-5999 – a policy name with leading whitespace (
" root") was normalized to the reservedrootpolicy, letting any operator holding write access tosys/policies/aclescalate from admin to full root - CVE-2025-6000 – an audit device’s log prefix could be used to write an attacker-controlled executable payload into the
plugin_directory, which was then loaded as a plugin: Vault’s first public RCE - CVE-2025-6004, CVE-2025-6010, and CVE-2025-6011 – normalization mismatches between the lockout tracker and the credential lookup let attackers bypass
userpassand LDAP lockout entirely and enumerate valid usernames - CVE-2025-6016 – TOTP validation accepted padded values, reducing MFA to a brute-forceable guess
- CVE-2025-6037 – certificate auth could be spoofed by presenting a certificate whose Common Name matched a trusted identity without full chain binding
- The common thread is that identity, lockout, and policy names were compared after inconsistent normalization – so patching matters more than usual here, and so does not leaving unused auth methods enabled as spare attack surface
Attack Prevented: Admin-to-root privilege escalation, remote code execution on the Vault node, lockout bypass, username enumeration, MFA brute force, certificate identity spoofing
Real-World Research:
- Cyata “Cracking the Vault” (2025): Nine zero-days across authentication, identity, and authorization, including Vault’s first public RCE (disclosure writeup)
ClickOps Implementation
Step 1: Confirm You Are on a Patched Release
- Run
vault status(orvault read sys/health) on every node and record the version - Compare against the HashiCorp security bulletins for the HCSEC-2025 advisories covering CVE-2025-5999 through CVE-2025-6037
- Upgrade any node behind the fixed release, and treat this as a patch-cadence commitment rather than a one-time action
Step 2: Hunt for Whitespace-Variant Root Grants
- Run
vault policy listand inspect every name that differs fromrootonly by leading or trailing whitespace or by case - Delete any such policy with
vault policy delete "<name>"and investigate who created it - Query the audit log for all writes to
sys/policies/acl/*and confirm each one maps to a known operator and change ticket
Step 3: Restrict Who Can Write Policies
- Grant
createandupdateonsys/policies/acl/*to a single named operator policy, not to general admin policies - Confirm no application or CI token holds that capability with
vault token capabilities <token> sys/policies/acl/test
Step 4: Disable Unused Auth Methods
- Run
vault auth listand identify every mount with no active production consumer - Disable each one with
vault auth disable <path>/ - Re-check after every project offboarding – unused mounts accumulate silently
Step 5: Protect the Plugin Directory
- Confirm
plugin_directoryin the server config is owned by root and is not writable by the Vault runtime user - Confirm no audit device writes anywhere inside
plugin_directory(see 4.1)
Validation & Testing
- Attempt
vault policy write " root" /dev/null– a patched Vault rejects the whitespace-variant name rather than silently overwriting root vault auth listreturns only auth methods with a named production consumer- A non-operator admin token returns no policy-write capability from
vault token capabilities - The Vault runtime user cannot create a file inside
plugin_directory
Expected result: Every node runs a patched release, no whitespace-variant root policy exists, policy-write access is held by a small operator set, and unused auth surface is disabled.
Monitoring & Maintenance
Maintenance schedule:
- Continuous: Alert on any write to
sys/policies/acl/*and any policy name that normalizes toroot - Monthly: Review the auth method inventory against the list of active consumers
- Per advisory: Subscribe to HashiCorp security bulletins and evaluate every Vault CVE against your deployed version
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC7.1 | Vulnerability identification and remediation |
| NIST 800-53 | SI-2, AC-6 | Flaw remediation and least privilege |
| ISO 27001 | A.12.6.1 | Management of technical vulnerabilities |
1.5 Enforce the User Lockout Baseline
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 6.2 |
| NIST 800-53 | AC-7 |
Description
Vault’s user_lockout configuration locks out a user after repeated failed logins. It ships enabled with defaults of 5 failed attempts, a 15-minute lockout duration, and a 15-minute counter reset, and it applies only to the userpass, ldap, and approle auth methods. Confirm the defaults are in force, tune them per mount where warranted, and monitor the locked-user list.
Rationale
Why This Matters:
- Without lockout, Vault’s login endpoints accept password guesses as fast as the network allows, which makes any weak
userpassor LDAP credential a matter of time - The protection covers only
userpass,ldap, andapprole– every other auth method needs a different control, such as the rate-limit quotas in 3.2 or lockout enforced at the identity provider - Lockout state is queryable at
sys/locked-users, which turns a burst of failed logins into a monitorable signal rather than a silent event - The
VAULT_DISABLE_USER_LOCKOUTenvironment variable switches the protection off entirely – and the 2025 lockout-bypass CVEs in 1.4 show that even an enabled lockout is only as good as the version implementing it
Attack Prevented: Password brute force, credential stuffing, automated login abuse, username enumeration via response timing
ClickOps Implementation
Step 1: Set the Global Baseline in Server Config
- Add a
user_lockout "all"stanza to the Vault server configuration file - Set
lockout_threshold(attempts before lockout),lockout_duration(how long the lockout lasts), andlockout_counter_reset(how long before the failure counter clears) - Keep or tighten the defaults of 5 attempts,
15mduration, and15mreset – do not loosen them without a documented reason
Step 2: Tune Per Auth Type Where Needed
- Add
user_lockout "userpass",user_lockout "ldap", oruser_lockout "approle"stanzas to override the global values for a specific auth type - Reserve
disable_lockout = truefor auth types where an upstream control already enforces lockout, and record the justification
Step 3: Tune Per Mount at Runtime
- Apply a stricter policy to a sensitive mount with
vault auth tune -user-lockout-threshold=3 -user-lockout-duration=30m userpass/ - Confirm the applied values with
vault read sys/auth/userpass/tune
Step 4: Confirm Lockout Is Never Disabled in Production
- Search every environment file, systemd unit, Helm values file, and container spec for
VAULT_DISABLE_USER_LOCKOUT - Remove it wherever it appears outside a disposable test environment – setting it disables lockout for the entire Vault process
Step 5: Monitor and Unlock
- List currently locked users with
vault list sys/locked-users - Alert when the count rises sharply, which indicates either an attack or a broken integration retrying bad credentials
- Unlock a specific alias with
vault write -f sys/locked-users/<mount_accessor>/unlock/<alias_identifier>
Validation & Testing
- Submit five bad passwords for a test
userpassuser, then submit the correct password – it must be refused while the lockout holds vault list sys/locked-usersshows the locked alias- After
lockout_durationelapses, the correct password succeeds vault read sys/auth/userpass/tunereflects the intended per-mount valuesVAULT_DISABLE_USER_LOCKOUTis absent from every production process environment
Expected result: Repeated failed logins lock the account, lockouts are visible at sys/locked-users, and the protection cannot be silently disabled.
Monitoring & Maintenance
Maintenance schedule:
- Continuous: Alert on locked-user count spikes and on repeated lockouts for the same alias
- Quarterly: Re-verify per-mount tune values and confirm no
disable_lockoutoverride has crept in
Reference: User lockout
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | AC-7 | Unsuccessful logon attempts |
| ISO 27001 | A.9.4.2 | Secure log-on procedures |
2. Secrets Engine Security
2.1 Use Dynamic Secrets Where Possible
Profile Level: L1 (Crawl) NIST 800-53: IA-5(7)
Description
Configure dynamic secrets engines that generate credentials on-demand with automatic expiration, eliminating static credential risk.
Rationale
Why This Matters:
- Static credentials never expire without rotation
- Dynamic credentials auto-revoke after TTL
- Limits blast radius of credential theft
Attack Prevented: Theft and long-term reuse of static credentials that never expire
Code Pack: Terraform
# --- Database secrets engine mount ---
resource "vault_mount" "database" {
path = "database"
type = "database"
description = "Dynamic database credential generation"
default_lease_ttl_seconds = 1800
max_lease_ttl_seconds = 3600
}
# --- PostgreSQL connection configuration ---
resource "vault_database_secret_backend_connection" "postgres" {
backend = vault_mount.database.path
name = "postgres-app"
allowed_roles = ["app-readonly", "app-readwrite"]
postgresql {
connection_url = var.postgres_connection_url
username = var.postgres_admin_username
password = var.postgres_admin_password
}
verify_connection = true
}
# --- Read-only dynamic role ---
resource "vault_database_secret_backend_role" "app_readonly" {
backend = vault_mount.database.path
name = "app-readonly"
db_name = vault_database_secret_backend_connection.postgres.name
default_ttl = 1800
max_ttl = 3600
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
]
revocation_statements = [
"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";",
"DROP ROLE IF EXISTS \"{{name}}\";",
]
}
# --- Read-write dynamic role ---
resource "vault_database_secret_backend_role" "app_readwrite" {
backend = vault_mount.database.path
name = "app-readwrite"
db_name = vault_database_secret_backend_connection.postgres.name
default_ttl = 900
max_ttl = 1800
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
]
revocation_statements = [
"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";",
"DROP ROLE IF EXISTS \"{{name}}\";",
]
}
2.2 Implement Secrets Versioning and Rotation
Profile Level: L1 (Crawl) NIST 800-53: IA-5(1)
Description
Enable KV v2 secrets engine with versioning for audit trail and rollback capability.
Rationale
Why This Matters:
- KV v2 versioning keeps a history of every secret change, so an accidental or malicious overwrite can be rolled back instead of causing an outage
- Version metadata records when a secret changed, providing the audit trail required to investigate tampering
- Regular rotation reduces the window in which a leaked credential remains valid
- Soft-delete and destroy controls let you remove exposed secret values while retaining the change history for forensics
Attack Prevented: Secret tampering, accidental destruction, prolonged credential exposure, undetected modification
Code Pack: CLI Script
# Enable KV v2
vault secrets enable -version=2 -path=secret kv
# Configure version retention
vault write secret/config \
max_versions=10 \
cas_required=true
# Write secret with CAS (check-and-set) for conflict prevention
vault kv put -cas=0 secret/myapp/config \
api_key="secret123" \
db_password="dbpass456"
# Read specific version
vault kv get -version=2 secret/myapp/config
# Delete version (soft delete)
vault kv delete -versions=1 secret/myapp/config
# Destroy version permanently (L3 only)
vault kv destroy -versions=1 secret/myapp/config
2.3 Enable Transit Engine for Encryption-as-a-Service
Profile Level: L2 (Walk) NIST 800-53: SC-28
Description
Use Transit secrets engine for application-level encryption without exposing encryption keys.
Rationale
Why This Matters:
- Transit performs encryption and decryption inside Vault so application servers never hold the raw key material, removing a common theft target
- Centralized key management enables key rotation and re-wrapping without re-encrypting data in every application
- Access to encrypt vs. decrypt vs. rewrap is governed by policy, so a compromised service can be limited to a single operation
- Keeping keys in Vault, backed by audit logging, produces a clear record of every cryptographic operation for compliance
Attack Prevented: Encryption-key theft, plaintext data exposure, unauthorized decryption, key sprawl
Code Pack: CLI Script
# Enable transit
vault secrets enable transit
# Create encryption key
vault write -f transit/keys/payment-data \
type=aes256-gcm96 \
exportable=false \
allow_plaintext_backup=false
# Encrypt data
vault write transit/encrypt/payment-data \
plaintext=$(echo "4111111111111111" | base64)
# Decrypt data
vault write transit/decrypt/payment-data \
ciphertext="vault:v1:..."
# Enable key rotation
vault write -f transit/keys/payment-data/rotate
# Configure minimum decryption version (after key rotation)
vault write transit/keys/payment-data/config \
min_decryption_version=2
3. Network & API Security
3.1 Configure TLS and API Security
Profile Level: L1 (Crawl) NIST 800-53: SC-8
Description
Secure Vault API with TLS, client certificates, and rate limiting.
Rationale
Why This Matters:
- All Vault traffic carries secrets and tokens; without TLS those values are exposed to network sniffing and man-in-the-middle attacks
- Enforcing strong TLS and client certificates ensures only trusted callers can reach the API, not anyone who can route to the listener
- Rate limiting on the API blunts brute-force and credential-stuffing attempts against authentication endpoints
- A hardened listener prevents downgrade and protocol attacks that could strip transport protection
Attack Prevented: Man-in-the-middle interception, token sniffing, credential brute force, protocol downgrade
ClickOps Implementation
Code Pack: CLI Script
# Listener configuration
listener "tcp" {
address = "0.0.0.0:8200"
tls_cert_file = "/vault/certs/vault.crt"
tls_key_file = "/vault/certs/vault.key"
tls_min_version = "tls12"
tls_cipher_suites = "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
# Client certificate verification (L2)
tls_require_and_verify_client_cert = true
tls_client_ca_file = "/vault/certs/client-ca.crt"
}
# API address
api_addr = "https://vault.company.com:8200"
cluster_addr = "https://vault-node:8201"
# Disable insecure TLS skip verify
disable_tls_cert_verification = false
3.2 Implement Request Rate Limiting
Profile Level: L2 (Walk) NIST 800-53: SC-5
Description
Configure rate limiting to prevent abuse and detect anomalous access patterns.
Rationale
Why This Matters:
- Rate limit quotas cap how fast a client can hit Vault, stopping a single compromised token from enumerating secrets at scale
- Throttling authentication paths slows brute-force and credential-stuffing attacks against login endpoints
- Limiting request volume protects the cluster from resource exhaustion that could deny service to legitimate workloads
- Quota breaches are observable, turning abnormal request spikes into an early signal of abuse or misconfiguration
- Rate limiting is the only throttle available for auth methods outside the
user_lockoutscope in 1.5 – JWT/OIDC, Kubernetes, cloud auth, and TLS certificate logins all depend on quotas instead
Attack Prevented: Brute-force authentication, secret enumeration, denial of service, automated abuse
ClickOps Implementation
Step 1: Understand What Your Edition Actually Enforces
- In Vault Community, a rate limit quota is enforced per client IP address, per node – a client behind many source addresses, or a cluster with many nodes, gets a correspondingly higher effective ceiling
- In Vault Enterprise, quotas can group by identity using
group_by(ip,none,entity_then_ip,entity_then_none) and can be enforced collectively across the cluster rather than per node - Size the limit against the edition you actually run – a Community quota of 100 requests/second on a five-node cluster is not a 100 request/second ceiling
Step 2: Create the Quotas
- Set a global default with
vault write sys/quotas/rate-limit/global rate=1000 interval=1s - Add a tighter quota on the login surface, for example
vault write sys/quotas/rate-limit/userpass-login path="auth/userpass/login" rate=10 interval=1s - Add per-mount quotas for high-value secret mounts, for example
vault write sys/quotas/rate-limit/pki path="pki/" rate=50 interval=1s - On Enterprise, add
group_by=entity_then_ipso an authenticated attacker cannot multiply their budget by rotating source addresses - Review the resulting set with
vault list sys/quotas/rate-limitandvault read sys/quotas/rate-limit/<name>
Step 3: Understand Precedence Before You Rely on a Limit
- Vault applies the most granular matching quota, not the most restrictive one – a specific path quota overrides a mount quota, which overrides a namespace quota, which overrides the global quota
- A permissive path-level quota therefore raises the ceiling that a stricter mount-level quota would otherwise impose
- Enumerate quotas from most to least specific and confirm no narrow rule accidentally exempts a sensitive path
Step 4: Apply Namespace Quotas (Enterprise)
- Create a quota in a parent namespace with
inheritable=trueso descendant namespaces are covered without a per-namespace rule - Confirm inheritance is what you want – a non-inheritable namespace quota applies only to that namespace’s own paths
Step 5: Configure Global Quota Behavior and Observability
- Set defaults and exemptions at
sys/quotas/config–rate_limit_exempt_paths(paths excluded from all rate limiting),enable_rate_limit_audit_logging(log each rejection to the audit device), andenable_rate_limit_response_headers(return the retry hint to clients) - Keep the exempt-path list minimal and reviewed; anything listed there has no rate limit at all
- Turn on audit logging for rejections so quota breaches reach the detections in 4.2
Step 6: Cap Lease Volume (Enterprise)
- Add a lease count quota with
vault write sys/quotas/lease-count/<name> path="<mount>/" max_leases=<n> - This bounds how many active leases a single mount or role can hold, which limits the damage from a runaway or hostile client generating dynamic credentials in bulk
Validation & Testing
- Exceed a configured quota and confirm Vault returns HTTP 429 with a
Retry-Afterheader when response headers are enabled - Confirm the rejection appears in the audit log when
enable_rate_limit_audit_loggingis on - Confirm a path listed in
rate_limit_exempt_pathsis genuinely intended to be unlimited - On Enterprise, authenticate as one entity from two source addresses and confirm
entity_then_ipgrouping counts them against a single budget
Expected result: Login and high-value secret paths carry explicit quotas, precedence has been checked end to end, and every rejection is observable.
Reference: Resource quotas
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6 | Protection against external threats |
| NIST 800-53 | SC-5 | Denial of service protection |
| ISO 27001 | A.12.1.3 | Capacity management |
4. Audit Logging
4.1 Enable Comprehensive Audit Logging
Profile Level: L1 (Crawl) NIST 800-53: AU-2, AU-3
Description
Enable audit logging to file and SIEM for all Vault operations.
Rationale
Why This Matters:
- Audit devices record every request and response, giving the tamper-evident trail needed to detect and investigate secret access
- Multiple devices (file, syslog, socket) ensure logging survives a single failure and can stream to a SIEM in real time
- Vault blocks requests if it cannot write to a configured audit device, guaranteeing no secret access goes unrecorded
- Forwarding logs off-box prevents an attacker who compromises the node from quietly erasing their tracks
- Audit devices are not only a detection control – they are a privilege-escalation surface, because enabling one tells Vault to write attacker-influenced content to an operator-chosen filesystem path
Attack Prevented: Undetected secret access, log tampering, repudiation, delayed breach discovery, audit-device-mediated code execution
Security Advisory: Vault guards audit file destinations so an audit device cannot write into the plugin_directory – but CVE-2026-5051 / HCSEC-2026-16 showed that the deprecated path parameter bypassed that guard, letting anyone with sys/audit write access drop a file into the plugin directory and reach code execution. Fixed in Vault 2.0.1, 1.21.6, 1.20.11, and 1.19.17. (HCSEC-2026-16)
ClickOps Implementation
- Enable the file audit device using the
file_pathoption – for examplevault audit enable file file_path=/var/log/vault/audit.log - Never use the legacy
pathoption. It is deprecated and was the vector for the plugin-directory guard bypass; audit any existing device withvault audit list -detailedand re-enable anything still configured withpath - Confirm every audit destination resolves outside the configured
plugin_directory, and thatplugin_directoryis not writable by the Vault runtime user - Restrict
create,update,delete, andsudoonsys/audit/*to a small, named trusted-operator policy – audit-device management is root-equivalent in practice, so it does not belong in a general admin policy - Enable syslog audit device for centralized log forwarding
- Enable socket audit device for real-time SIEM streaming
- Verify all audit devices are active with
vault audit list -detailed - Run a version carrying the CVE-2026-5051 fix (2.0.1, 1.21.6, 1.20.11, or 1.19.17 and later)
Validation & Testing
vault audit list -detailedshows every device configured withfile_path, and none with the legacypathoption- No audit destination path resolves inside
plugin_directory vault token capabilities <non-operator-token> sys/audit/filereturns no write or sudo capability- Disabling an audit device produces an audit event that reaches the SIEM before the device stops writing
Expected result: Audit devices capture every request, are managed by a small operator set, and cannot be used to write into the plugin directory.
Code Pack: Terraform
# --- File audit device (local persistent log) ---
resource "vault_audit" "file" {
type = "file"
path = "file"
description = "File-based audit log for local retention"
options = {
file_path = var.audit_file_path
log_raw = "false"
hmac_accessor = "true"
mode = "0600"
}
}
# --- Syslog audit device (centralized log forwarding) ---
resource "vault_audit" "syslog" {
type = "syslog"
path = "syslog"
description = "Syslog audit device for SIEM integration"
options = {
tag = "vault-audit"
facility = "AUTH"
log_raw = "false"
}
}
# --- Socket audit device (real-time log streaming, L2+) ---
resource "vault_audit" "socket" {
count = var.profile_level >= 2 ? 1 : 0
type = "socket"
path = "socket"
description = "Socket audit device for real-time log streaming"
options = {
address = var.audit_socket_address
socket_type = "tcp"
log_raw = "false"
}
}
Code Pack: API Script
# Enable file audit device
if [ -n "${FILE_ENABLED}" ]; then
pass "4.1 File audit device already enabled"
else
info "4.1 Enabling file audit device..."
vault_put "/sys/audit/file" '{
"type": "file",
"description": "File-based audit log for local retention",
"options": {
"file_path": "/var/log/vault/audit.log",
"log_raw": false,
"hmac_accessor": true,
"mode": "0600"
}
}' > /dev/null 2>&1 && pass "4.1 File audit device enabled" \
|| { fail "4.1 Failed to enable file audit device"; increment_failed; }
fi
# Enable syslog audit device
if [ -n "${SYSLOG_ENABLED}" ]; then
pass "4.1 Syslog audit device already enabled"
else
info "4.1 Enabling syslog audit device..."
vault_put "/sys/audit/syslog" '{
"type": "syslog",
"description": "Syslog audit device for SIEM integration",
"options": {
"tag": "vault-audit",
"facility": "AUTH",
"log_raw": false
}
}' > /dev/null 2>&1 && pass "4.1 Syslog audit device enabled" \
|| { fail "4.1 Failed to enable syslog audit device"; increment_failed; }
fi
Code Pack: CLI Script
# Enable file audit device
vault audit enable file file_path=/vault/audit/vault-audit.log
# Enable syslog audit device
vault audit enable syslog tag="vault" facility="AUTH"
# Enable socket audit device (for SIEM)
vault audit enable socket \
address="siem.company.com:514" \
socket_type="tcp"
# Verify audit devices
vault audit list -detailed
Code Pack: Sigma Detection Rule
detection:
selection:
request.path|startswith: 'sys/audit'
request.operation:
- 'delete'
condition: selection
fields:
- auth.display_name
- auth.policies
- request.path
- request.operation
- request.remote_address
- time
4.2 Configure Audit Log Alerting
Profile Level: L1 (Crawl)
Description
Build detection rules and alerts on Vault audit logs so that suspicious operations – such as root token use, policy changes, or bulk secret reads – trigger timely security notifications.
Rationale
Why This Matters:
- Audit logs only add value if someone acts on them; alerting turns passive records into timely detection of abuse
- Real-time alerts on high-risk events (root token use, policy edits, mass secret reads) shrink attacker dwell time
- Detecting anomalous access patterns surfaces compromised tokens before they are used to exfiltrate large numbers of secrets
- Routing alerts to on-call and SIEM workflows ensures security teams respond during, not after, an incident
Attack Prevented: Delayed breach detection, undetected token abuse, silent privilege changes, bulk secret exfiltration
Detection Use Cases
Code Pack: SDK Script
import json
from collections import defaultdict
from datetime import datetime, timedelta
def detect_mass_secret_access(logs, threshold=100, window_minutes=5):
"""Detect unusual volume of secret reads"""
access_counts = defaultdict(int)
window_start = datetime.utcnow() - timedelta(minutes=window_minutes)
for log in logs:
if log.get('request', {}).get('path', '').startswith('secret/'):
if log['request']['operation'] == 'read':
accessor = log.get('auth', {}).get('accessor', 'unknown')
access_counts[accessor] += 1
alerts = []
for accessor, count in access_counts.items():
if count > threshold:
alerts.append(f"High secret access: {accessor} read {count} secrets")
return alerts
def detect_auth_failures(logs, threshold=10, window_minutes=5):
"""Detect brute force attempts"""
failures = defaultdict(int)
for log in logs:
if log.get('type') == 'response':
if not log.get('response', {}).get('succeeded', True):
remote_addr = log.get('request', {}).get('remote_address', 'unknown')
failures[remote_addr] += 1
return [f"Auth failures from {ip}: {count}"
for ip, count in failures.items() if count > threshold]
5. CI/CD Integration Security
5.1 Secure Jenkins Integration
Profile Level: L1 (Crawl)
Description
Configure secure Vault integration for Jenkins with minimal privileges and short-lived tokens.
Rationale
Why This Matters:
- CI/CD systems are prime targets for supply chain attacks
- CircleCI breach (2023) exposed customer secrets
- Jenkins compromise = access to all pipelines’ secrets
Attack Prevented: CI/CD supply chain compromise, pipeline-wide secret exposure via compromised Jenkins
ClickOps Implementation
Jenkins Configuration (Jenkinsfile):
Configure a Jenkinsfile that uses the withVault step to securely retrieve secrets during pipeline execution. The Vault URL and AppRole credential ID are injected via environment variables, and secrets are mapped to environment variables within the build step scope only.
Code Pack: CLI Script
# Step 1: Create Jenkins-Specific Policy
vault policy write jenkins-secrets - <<EOF
# Read secrets for Jenkins builds
path "secret/data/jenkins/*" {
capabilities = ["read"]
}
# Generate AWS credentials for deployments
path "aws/creds/jenkins-deploy" {
capabilities = ["read"]
}
# No access to production secrets
path "secret/data/production/*" {
capabilities = ["deny"]
}
EOF
# Step 2: Configure AppRole with Restrictions
vault write auth/approle/role/jenkins \
token_policies="jenkins-secrets" \
token_ttl=15m \
token_max_ttl=30m \
secret_id_ttl=1h \
secret_id_num_uses=1 \
token_bound_cidrs="10.0.0.0/8"
Code Pack: SDK Script
// Jenkinsfile
pipeline {
agent any
environment {
VAULT_ADDR = 'https://vault.company.com'
}
stages {
stage('Get Secrets') {
steps {
withVault(configuration: [
vaultUrl: "${VAULT_ADDR}",
vaultCredentialId: 'vault-approle'
], vaultSecrets: [
[path: 'secret/data/jenkins/api-keys',
secretValues: [[envVar: 'API_KEY', vaultKey: 'data.api_key']]]
]) {
sh 'echo "Using secret safely"'
}
}
}
}
}
5.2 Implement OIDC for GitHub Actions
Profile Level: L2 (Walk)
Description
Use GitHub Actions OIDC to authenticate to Vault without storing long-lived tokens.
Configure JWT authentication for GitHub Actions using OIDC federation. This eliminates long-lived tokens by using GitHub’s OIDC provider to authenticate directly to Vault with short-lived JWTs bound to specific repositories and branches.
Rationale
Why This Matters:
- GitHub Actions OIDC lets workflows authenticate to Vault with short-lived JWTs, eliminating long-lived tokens stored as repository secrets
- Tokens stored in CI are a prime exfiltration target; removing them closes off a common supply-chain attack path
- Binding the Vault role to specific repositories, branches, and claims ensures only the intended pipeline can obtain secrets
- Short-lived credentials expire automatically, so a token leaked from a build log is useless minutes later
Attack Prevented: CI secret theft, supply-chain compromise, long-lived token abuse, unauthorized pipeline access
Code Pack: CLI Script
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: hashicorp/vault-action@v2
with:
url: https://vault.company.com
method: jwt
role: deploy
jwtGithubAudience: https://github.com/your-org
secrets: |
secret/data/deploy/credentials api_key | API_KEY ;
6. Operational Security
6.1 Configure Auto-Unseal
Profile Level: L2 (Walk) NIST 800-53: SC-12
Description
Configure auto-unseal using cloud KMS to eliminate manual unseal key management.
Rationale
Why This Matters:
- Auto-unseal stores the unseal key in a cloud KMS, removing the need to distribute and manually enter Shamir key shares on every restart
- Eliminating manual unseal removes the risk of key shares being mishandled, lost, or captured by an operator
- KMS-backed unsealing ties Vault availability to a hardened, access-controlled key service with its own audit trail
- Automated recovery lets clusters restart unattended, avoiding prolonged outages where secrets are unavailable
Attack Prevented: Unseal-key compromise, insider key capture, operational key mishandling, prolonged seal outages
Code Pack: CLI Script
# AWS KMS auto-unseal
seal "awskms" {
region = "us-east-1"
kms_key_id = "alias/vault-unseal-key"
}
# Azure Key Vault auto-unseal
seal "azurekeyvault" {
tenant_id = "your-tenant-id"
client_id = "your-client-id"
client_secret = "your-client-secret"
vault_name = "vault-unseal"
key_name = "vault-key"
}
# GCP Cloud KMS auto-unseal
seal "gcpckms" {
project = "your-project"
region = "us-east1"
key_ring = "vault"
crypto_key = "unseal"
}
6.2 Implement Disaster Recovery
Profile Level: L2 (Walk) NIST 800-53: CP-9, CP-10
Description
Configure Vault disaster recovery and backup procedures.
Use Raft snapshots for backup and restore operations. Create snapshots regularly, verify their integrity, and test restoration procedures. For Enterprise deployments, configure DR replication for automated failover.
Rationale
Why This Matters:
- Regular Raft snapshots ensure a corrupted, deleted, or ransomware-encrypted Vault can be restored without losing all secrets
- Verifying snapshot integrity and testing restores confirms backups actually work before a real disaster strikes
- DR replication provides automated failover so a region or node loss does not leave applications unable to retrieve credentials
- Off-site, access-controlled backups protect against both accidental loss and an attacker attempting to destroy the only copy of secrets
Attack Prevented: Data destruction, ransomware lockout, single-point failure, irrecoverable secret loss
Code Pack: CLI Script
# Create Raft snapshot
vault operator raft snapshot save backup.snap
# Verify snapshot
vault operator raft snapshot inspect backup.snap
# Restore from snapshot (DR scenario)
vault operator raft snapshot restore backup.snap
# For Enterprise: Configure DR replication
vault write -f sys/replication/dr/primary/enable
6.3 Require Authentication for Rekey and Root-Token Generation
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 6.8 |
| NIST 800-53 | AC-3, SC-5 |
Description
Vault 2.0.0 changes sys/rekey, sys/generate-root, and DR operation-token generation from unauthenticated to authenticated endpoints, closing an unauthenticated denial-of-service path. Run 2.0.0 or later and confirm the requirement has not been reverted – the change is revertible by configuration.
Rationale
Why This Matters:
- Before 2.0.0, any client that could reach the listener could initiate a rekey or root-token generation without credentials, and repeated initialization attempts could deny those operations to legitimate operators (CVE-2026-5807 / HCSEC-2026-08)
- These endpoints govern unseal key material and root token issuance – the two most powerful operations in the cluster – so anonymous access to even their initialization step is disproportionate
- Because the pre-2.0.0 behavior is revertible, an operator restoring an old runbook or automation can silently reopen the exposure long after the upgrade
- Requiring authentication makes every initialization attempt attributable in the audit log rather than an anonymous event
Attack Prevented: Unauthenticated denial of service against unseal and root operations, anonymous rekey initialization, unattributable root-token generation attempts
ClickOps Implementation
Step 1: Upgrade and Confirm the Version
- Upgrade every node to Vault 2.0.0 or later
- Confirm with
vault statusorvault read sys/healthon each node
Step 2: Prove the Endpoints Now Require Authentication
- From a shell with no
VAULT_TOKENset, runvault operator rekey -init– it must fail with a permission error rather than starting a rekey - Repeat for
vault operator generate-root -init - On Enterprise DR clusters, repeat for DR operation-token generation
Step 3: Confirm the Behavior Has Not Been Reverted
- Review the server configuration and process environment on every node for any setting that restores unauthenticated access to these endpoints
- Add this check to your configuration drift detection – a revert is a one-line change that produces no visible symptom
- Update any runbook or automation that assumed unauthenticated rekey so no one has a reason to revert it
Step 4: Scope Who May Call Them
- Grant
sys/rekey/*,sys/generate-root/*, and the DR operation-token paths only to a named break-glass operator policy - Verify no general admin, application, or CI token holds those capabilities using
vault token capabilities <token> sys/generate-root/attempt
Step 5: Alert on Use
- Add a detection for any successful write to
sys/rekey/*orsys/generate-root/*and route it to on-call as a high-severity event (see 4.2)
Validation & Testing
- Unauthenticated
vault operator rekey -initreturns a permission error - Unauthenticated
vault operator generate-root -initreturns a permission error - An authenticated break-glass operator can still complete a rekey, and the attempt appears in the audit log with an identity attached
vault read sys/healthreports version 2.0.0 or later on every node
Expected result: No unseal, rekey, or root-generation operation can be initiated anonymously, and every attempt is attributable.
Reference: HCSEC-2026-08
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access controls |
| NIST 800-53 | AC-3, SC-5 | Access enforcement and denial of service protection |
| ISO 27001 | A.9.4.1 | Information access restriction |
7. Host & Platform Hardening
7.1 Apply the Production Hardening Baseline
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 4.4, 8.4 |
| NIST 800-53 | CM-6, SC-28, AU-8 |
Description
Vault’s own security model assumes the host is trusted. HashiCorp publishes a production hardening baseline covering the operating system, process isolation, network posture, and operational process around a Vault node. Apply it – these are the assumptions every control elsewhere in this guide depends on.
Rationale
Why This Matters:
- Vault’s threat model explicitly treats root access on the host as a full compromise: anyone who can read Vault’s memory or attach a debugger can read decrypted secrets regardless of policy
- Secrets are decrypted in memory, so swap files and core dumps can write plaintext secret material to disk where it survives a reboot and lands in backups
- Vault should be the only service on the machine (single tenancy) – every co-tenant application is another path to the memory holding your entire secret store
- Outbound firewall rules matter as much as inbound: they are what stops a compromised node from exfiltrating secrets to an attacker-controlled destination
- Vault issues and validates time-bound credentials (leases, TTLs, tokens, certificates), so clock drift directly undermines expiry enforcement
Attack Prevented: Host-level secret extraction from memory or swap, plaintext secrets persisted to disk via core dumps, lateral movement from co-tenant services, secret exfiltration from a compromised node, TTL bypass via clock drift
ClickOps Implementation
Step 1: Isolate the Process
- Run Vault as a dedicated, unprivileged user (for example
vault) that owns no other service and has no login shell - Enforce single tenancy – no other application, agent, or workload runs on the Vault host
- Set filesystem permissions so only the Vault user can read the configuration, TLS private keys, and storage directory; keep the binary owned by root and non-writable by the Vault user
Step 2: Prevent Secrets From Reaching Disk
- Disable swap on the host so decrypted secrets in memory are never paged out
- Disable core dumps for the Vault process so a crash cannot write memory contents to disk
- Disable shell command history for the Vault operator account so tokens and unseal shares typed at a prompt are not persisted
- If you run Vault in a container without the
IPC_LOCKcapability, you must setdisable_mlock = true– Vault cannot lock memory without that capability. Doing so removes Vault’s own protection against paging, which makes disabled (or encrypted) swap a hard requirement rather than a recommendation
Step 3: Control the Network in Both Directions
- Restrict inbound traffic to the API port (8200) and cluster port (8201) from known client and peer ranges only
- Restrict outbound traffic to the destinations Vault genuinely needs – storage backend, cloud KMS for auto-unseal, identity provider, log destinations
- Do not expose the Vault API directly to the internet
Step 4: Harden Transport and Time
- Prefer TLS 1.3 on the listener, and disable legacy protocol versions and weak cipher suites
- Synchronize the host clock with NTP and monitor for drift, since token, lease, and certificate expiry all depend on accurate time
Step 5: Close the Human Loop
- Document an off-boarding process that revokes departing operators’ Vault access, rotates any unseal key shares they held, and rotates credentials they could have read
- Trigger it from the same HR event that drives IdP deprovisioning, not from a separate manual checklist
Validation & Testing
ps -o user= -p $(pgrep vault)returns the dedicated unprivileged user, not rootswapon --showreturns nothing, or swap is confirmed encrypted- The Vault process core dump limit is zero, and no core file appears after a forced crash in a test environment
- Only Vault-related listeners are bound on the host
- An outbound connection attempt from the host to an unapproved destination is blocked
- A TLS scan of the listener negotiates TLS 1.3 and refuses legacy versions and weak ciphers
- Host clock offset from the NTP source is within tolerance
- A test off-boarding produces revoked Vault access and a rotation record
Expected result: The Vault host runs one unprivileged service, cannot page or dump secrets to disk, talks only to approved destinations in both directions, keeps accurate time, and has a documented operator off-boarding path.
Reference: Production hardening
Monitoring & Maintenance
Maintenance schedule:
- Continuous: Alert on new listeners, new processes, or swap being re-enabled on the Vault host
- Monthly: Re-verify firewall rules in both directions against the approved destination list
- Quarterly: Re-run the TLS scan and confirm off-boarding was executed for every departure
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.6, CC6.8 | Boundary protection and unauthorized software prevention |
| NIST 800-53 | CM-6, SC-28, AU-8 | Configuration settings, protection at rest, time stamps |
| ISO 27001 | A.12.1.2, A.13.1.1 | Change management and network controls |
7.2 Apply Extended Platform Isolation
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 4.8 |
| NIST 800-53 | CM-7, AC-6(10), SI-7 |
Description
Beyond the baseline, HashiCorp documents extended hardening for regulated and high-value deployments: kernel-enforced process confinement, mandatory access control, immutable upgrades, and administrative isolation of the sys/ surface.
Rationale
Why This Matters:
- Systemd sandboxing and a tight capability set mean that even a code-execution bug in Vault runs in a process that cannot write to the filesystem or acquire new privileges – directly limiting the class of attack demonstrated by the audit-device RCE in 1.4
- SELinux or AppArmor enforce confinement the process cannot opt out of, unlike file permissions that root can override
- Immutable upgrades – replacing nodes rather than patching them in place – guarantee that a compromised host is destroyed rather than carried forward, and that every running node matches a known-good image
- An administrative namespace (Enterprise) separates the operators who manage
sys/from the tenants who consume secrets, so tenant admins cannot reach cluster-wide operations
Attack Prevented: Post-exploitation persistence on the Vault host, privilege escalation from a Vault process compromise, drift between running nodes and approved images, tenant-to-cluster administrative escalation
ClickOps Implementation
Step 1: Sandbox the Service Unit
- In the systemd unit, set
ProtectSystem=fullandProtectHome=read-onlyso Vault cannot write to system or user directories - Set
PrivateTmp=yesandPrivateDevices=yesto isolate temporary files and device access - Set
NoNewPrivileges=yesso the process and its children can never gain privileges - Constrain
CapabilityBoundingSettoCAP_IPC_LOCKonly – the single capability Vault needs to lock memory - Reload with
systemctl daemon-reloadand restart Vault, then confirm the service still starts and can lock memory
Step 2: Enforce Mandatory Access Control
- Deploy an SELinux policy (or AppArmor profile) confining the Vault process to its configuration, storage, TLS, and audit paths
- Run in permissive/complain mode first, collect denials, then switch to enforcing
- Confirm the enforcement mode after every host reboot – a policy silently left permissive provides no protection
Step 3: Adopt Immutable Upgrades
- Build Vault nodes from a versioned image and upgrade by replacing nodes rather than patching in place
- Disable interactive shell access to running nodes so the image remains the only source of truth
- Verify each node reports the expected version with
vault statusafter replacement
Step 4: Isolate Administration (Enterprise)
- Configure an administrative namespace so cluster-wide
sys/operations live in a dedicated namespace separate from tenant namespaces - Grant the administrative namespace only to platform operators, and confirm tenant admins cannot reach
sys/operations outside their own namespace
Validation & Testing
systemctl show vaultreflects the sandboxing directives, and Vault still starts cleanly- The Vault process cannot write to a system directory, and a privilege-raising
execattempt fails getenforcereturnsEnforcing(or the AppArmor profile is in enforce mode), and no unexpected denials appear- Replacing a node produces the expected version and no manual post-install steps
- A tenant-namespace admin token is denied on a cluster-level
sys/path
Expected result: Vault runs confined by both systemd and mandatory access control, nodes are replaced rather than patched, and cluster administration is isolated from tenant administration.
Reference: Production hardening
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.8, CC8.1 | Unauthorized software prevention and change management |
| NIST 800-53 | CM-7, AC-6(10), SI-7 | Least functionality, privilege restriction, software integrity |
| ISO 27001 | A.12.5.1, A.12.6.2 | Installation of software and software installation restrictions |
8. Compliance Quick Reference
SOC 2 Mapping
| Control ID | Vault Control | Guide Section |
|---|---|---|
| CC6.1 | Auth methods and policies | 1.1 |
| CC6.2 | Granular policies | 1.2 |
| CC6.1 | User lockout baseline | 1.5 |
| CC6.6 | Request rate limiting | 3.2 |
| CC6.6 | Host and network hardening | 7.1 |
| CC7.1 | Vulnerability remediation | 1.4 |
| CC7.2 | Audit logging | 4.1 |
| CC8.1 | Immutable upgrades | 7.2 |
NIST 800-53 Mapping
| Control | Vault Control | Guide Section |
|---|---|---|
| AC-3 | Authenticated rekey and root generation | 6.3 |
| AC-6 | Least privilege policies | 1.2 |
| AC-7 | Unsuccessful logon attempts | 1.5 |
| IA-5 | Token and auth management | 1.1 |
| AU-2 | Audit logging | 4.1 |
| CM-6 | Production hardening baseline | 7.1 |
| CM-7 | Least functionality and process confinement | 7.2 |
| SC-5 | Rate limit quotas | 3.2 |
| SC-28 | Transit encryption | 2.3 |
| SI-2 | Flaw remediation for known Vault CVEs | 1.4 |
Appendix A: Edition Compatibility
| Control | Community | Enterprise | HCP Vault |
|---|---|---|---|
| Auth Methods | ✅ | ✅ | ✅ |
| Audit Logging | ✅ | ✅ | ✅ |
| Dynamic Secrets | ✅ | ✅ | ✅ |
| Namespaces | ❌ | ✅ | ✅ |
| Sentinel Policies | ❌ | ✅ | ✅ |
| DR Replication | ❌ | ✅ | ✅ |
| Performance Replication | ❌ | ✅ | ✅ |
Appendix B: References
Official HashiCorp Documentation:
- HashiCorp Security
- Compliance Overview
- Vault Documentation
- Production Hardening
- Security Model
- Auth Methods
- Audit Devices
- User Lockout
- Resource Quotas
- Important Changes (upgrade-breaking behavior)
- Kubernetes Security Considerations
API & Developer Tools:
Compliance Frameworks:
- SOC 2 Type II, ISO 27001, ISO 27017, ISO 27018 (for HCP Vault) – reports available under NDA via Compliance Overview
Security Incidents & Advisories:
- Codecov Supply Chain Attack (Apr 2021): Compromised CI environment at Codecov was used to exfiltrate environment variables from CI builds. HashiCorp’s GPG signing key was exposed, forcing rotation of all signing keys and validation of all published software releases.
- Cyata “Cracking the Vault” (2025): Nine zero-day vulnerabilities in Vault’s authentication, identity, and authorization layers, including Vault’s first publicly documented RCE – disclosure writeup (see 1.4)
- HCSEC-2026-08 (CVE-2026-5807): Denial of service via unauthenticated root token generation and rekey operations – advisory (see 6.3)
- HCSEC-2026-16 (CVE-2026-5051): Audit device plugin directory guard bypass via the legacy
pathoption – advisory (see 4.1)
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-08-08 | 0.2.1 | draft | Cheat-sheet cell repair: added missing Attack Prevented line(s) to §2.1, §5.1 (no content-facts changed) | Claude Code (Fable 5) |
| 2026-08-03 | 0.2.0 | draft | Add 2025 Cyata CVE-class patching control (1.4), user lockout baseline (1.5), authenticated rekey and root-generation control (6.3), and new Host & Platform Hardening section (7.1, 7.2); update policy control for the Vault 2.0.0 templated-path change, rate limiting with concrete quota implementation, and audit logging for the CVE-2026-5051 file_path mandate |
Claude Code (Sonnet 5) |
| 2026-06-29 | 0.1.1 | draft | Add cheat-sheet Description and Rationale for all controls | Claude Code (Opus 4.8) |
| 2025-12-14 | 0.1.0 | draft | Initial HashiCorp Vault hardening guide | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide