v0.1.0-draft AI Drafted

HashiCorp Vault Hardening Guide

Secrets Last updated: 2025-12-14

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 (Baseline): Essential controls for all organizations
  • L2 (Hardened): Enhanced controls for security-sensitive environments
  • L3 (Maximum Security): 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

  1. Authentication & Access Controls
  2. Secrets Engine Security
  3. Network & API Security
  4. Audit Logging
  5. CI/CD Integration Security
  6. Operational Security
  7. Compliance Quick Reference

1. Authentication & Access Controls

1.1 Implement Least-Privilege Auth Methods

Profile Level: L1 (Baseline) 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

# 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

Step 2: Configure OIDC for User Authentication

# 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"

Step 3: Configure AppRole for Applications

# 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"

Code Implementation

Terraform Configuration:

# terraform/vault/auth-methods.tf

# OIDC for human users
resource "vault_jwt_auth_backend" "oidc" {
  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     = "8h"
  }
}

# AppRole for applications
resource "vault_auth_backend" "approle" {
  type = "approle"

  tune {
    default_lease_ttl = "1h"
    max_lease_ttl     = "4h"
  }
}

resource "vault_approle_auth_backend_role" "jenkins" {
  backend        = vault_auth_backend.approle.path
  role_name      = "jenkins"
  token_policies = ["jenkins-secrets"]
  token_ttl      = 3600
  token_max_ttl  = 14400

  # Bind to CIDR (L2)
  token_bound_cidrs = ["10.0.0.0/8"]
  secret_id_bound_cidrs = ["10.0.0.0/8"]
}

# Kubernetes auth for cloud-native workloads
resource "vault_auth_backend" "kubernetes" {
  type = "kubernetes"
}

resource "vault_kubernetes_auth_backend_config" "config" {
  backend         = vault_auth_backend.kubernetes.path
  kubernetes_host = var.kubernetes_host
}

resource "vault_kubernetes_auth_backend_role" "app" {
  backend                          = vault_auth_backend.kubernetes.path
  role_name                        = "app"
  bound_service_account_names      = ["app-sa"]
  bound_service_account_namespaces = ["production"]
  token_ttl                        = 3600
  token_policies                   = ["app-secrets"]
}

Validation & Testing

  1. Attempt to use root token - should be revoked
  2. Login via OIDC - should succeed with appropriate policies
  3. AppRole authentication - verify CIDR binding works
  4. Check token TTLs are enforced

Expected result: Each auth method provides minimal required access

Monitoring & Maintenance

# Monitor auth method usage
vault read sys/auth

# Check token counts by auth method
vault read sys/internal/counters/tokens

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

1.2 Implement Granular Policies

Profile Level: L1 (Baseline) NIST 800-53: AC-3, AC-6

Description

Create fine-grained policies limiting access to specific paths. Avoid wildcard policies that grant excessive access.

ClickOps Implementation

# Bad: Overly permissive policy
path "secret/*" {
  capabilities = ["read", "list"]
}

# Good: Scoped policy
path "secret/data//*" {
  capabilities = ["read"]
}

# Better: Application-specific policy
path "secret/data/jenkins/+/credentials" {
  capabilities = ["read"]
}

# Deny access to sensitive paths explicitly
path "secret/data/production/+/admin" {
  capabilities = ["deny"]
}

Step 1: Create Hierarchical Policy Structure

# 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 (Hardened) NIST 800-53: AC-2

Description

Use Vault’s identity system to manage users and groups across auth methods, enabling consistent policy application.

# 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>"

2. Secrets Engine Security

2.1 Use Dynamic Secrets Where Possible

Profile Level: L1 (Baseline) 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

ClickOps Implementation

Database Dynamic Secrets:

# Enable database secrets engine
vault secrets enable database

# Configure PostgreSQL connection
vault write database/config/production \
    plugin_name=postgresql-database-plugin \
    connection_url="postgresql://:@db.company.com:5432/prod" \
    allowed_roles="readonly,readwrite" \
    username="vault_admin" \
    password="$ADMIN_PASSWORD"

# Create role for read-only access
vault write database/roles/readonly \
    db_name=production \
    creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\";" \
    revocation_statements="DROP ROLE IF EXISTS \"\";" \
    default_ttl="1h" \
    max_ttl="24h"

AWS Dynamic Secrets:

# Enable AWS secrets engine
vault secrets enable aws

# Configure AWS backend
vault write aws/config/root \
    access_key=$AWS_ACCESS_KEY \
    secret_key=$AWS_SECRET_KEY \
    region=us-east-1

# Create role for S3 access
vault write aws/roles/s3-readonly \
    credential_type=iam_user \
    policy_document=-<<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": ["arn:aws:s3:::company-data/*"]
    }
  ]
}
EOF

2.2 Implement Secrets Versioning and Rotation

Profile Level: L1 (Baseline) NIST 800-53: IA-5(1)

Description

Enable KV v2 secrets engine with versioning for audit trail and rollback capability.

# 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 (Hardened) NIST 800-53: SC-28

Description

Use Transit secrets engine for application-level encryption without exposing encryption keys.

# 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 (Baseline) NIST 800-53: SC-8

Description

Secure Vault API with TLS, client certificates, and rate limiting.

ClickOps Implementation

vault.hcl configuration:

# 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 (Hardened) NIST 800-53: SC-5

Description

Configure rate limiting to prevent abuse and detect anomalous access patterns.

# In vault.hcl (Enterprise only)
default_lease_ttl = "1h"
max_lease_ttl = "24h"

# Rate limiting
rate_limit {
  rate = 100.0
  burst = 200

  # Per-path limits
  path {
    glob = "auth/*"
    rate = 50.0
    burst = 100
  }

  path {
    glob = "secret/*"
    rate = 200.0
    burst = 400
  }
}

4. Audit Logging

4.1 Enable Comprehensive Audit Logging

Profile Level: L1 (Baseline) NIST 800-53: AU-2, AU-3

Description

Enable audit logging to file and SIEM for all Vault operations.

ClickOps Implementation

# 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

Audit Log Format:

{
  "time": "2025-01-15T10:30:00Z",
  "type": "request",
  "auth": {
    "client_token": "hmac-sha256:xxx",
    "accessor": "hmac-sha256:xxx",
    "display_name": "approle",
    "policies": ["jenkins-secrets"],
    "token_policies": ["jenkins-secrets"],
    "metadata": {
      "role_name": "jenkins"
    }
  },
  "request": {
    "id": "req-xxx",
    "operation": "read",
    "path": "secret/data/jenkins/credentials",
    "remote_address": "10.0.1.50"
  },
  "response": {
    "succeeded": true
  }
}

4.2 Configure Audit Log Alerting

Profile Level: L1 (Baseline)

Detection Use Cases

#!/usr/bin/env python3
# vault-audit-monitor.py - Monitor for suspicious patterns

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 (Baseline)

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

ClickOps Implementation

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"

Step 3: Jenkins Configuration

// 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 (Hardened)

Description

Use GitHub Actions OIDC to authenticate to Vault without storing long-lived tokens.

# Configure JWT auth for GitHub Actions
vault auth enable -path=github-actions jwt

vault write auth/github-actions/config \
    oidc_discovery_url="https://token.actions.githubusercontent.com" \
    bound_issuer="https://token.actions.githubusercontent.com"

vault write auth/github-actions/role/deploy \
    role_type="jwt" \
    bound_audiences="https://github.com/your-org" \
    bound_subject="repo:your-org/your-repo:ref:refs/heads/main" \
    user_claim="sub" \
    policies="deploy-secrets" \
    ttl=5m

GitHub Actions Workflow:

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 (Hardened) NIST 800-53: SC-12

Description

Configure auto-unseal using cloud KMS to eliminate manual unseal key management.

# 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 (Hardened) NIST 800-53: CP-9, CP-10

Description

Configure Vault disaster recovery and backup procedures.

# 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

7. 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
CC7.2 Audit logging 4.1

NIST 800-53 Mapping

Control Vault Control Guide Section
AC-6 Least privilege policies 1.2
IA-5 Token and auth management 1.1
AU-2 Audit logging 4.1
SC-28 Transit encryption 2.3

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:

Supply Chain Incident:

  • Codecov breach (2021) exposed HashiCorp’s GPG signing key via compromised CI environment

Changelog

Date Version Maturity Changes Author
2025-12-14 0.1.0 draft Initial HashiCorp Vault hardening guide Claude Code (Opus 4.5)