Identity & Access

Managing Secrets with HashiCorp Vault

March 5, 2025

Hardcoded credentials in application code, API keys in environment variables, database passwords in config files. These aren’t edge cases, they’re standard findings in security assessments across organizations of every size. The problem isn’t that teams are careless. It’s that there’s no obvious better place to put secrets when you’re moving fast, so they end up wherever is convenient, which is usually somewhere that doesn’t rotate them, doesn’t log access to them, and doesn’t revoke them when someone leaves.

HashiCorp Vault solves this by providing a centralized, policy-controlled store for secrets with encryption at rest, fine-grained access control, and an audit log of every read and write. This post covers practical secrets management using Vault’s Key-Value engine, from setup through lifecycle operations.

Prerequisites

  • HashiCorp Vault installed, running, and authenticated
  • Administrative access to enable secrets engines
  • Vault CLI configured with a valid token

KV Engine Versions

Vault’s Key-Value engine comes in two versions. For anything beyond a lab environment, use KV v2.

KV v1 — Simple key-value storage. No versioning, no metadata, no soft deletes. Useful for low-complexity environments but leaves no recovery path if a secret is overwritten.

KV v2 — Versioning, metadata, soft deletes, and patch operations. Every write creates a new version. Deleted secrets are recoverable until permanently destroyed. Use this in production.

Step 1 — Enable the Secrets Engine

# Check what's currently mounted
vault secrets list

# Enable KV v2 at the secret/ path
vault secrets enable -path=secret kv-v2

# Verify
vault secrets list

Step 2 — Store Secrets

Organize secrets by path using a consistent hierarchy. The structure you choose now becomes the basis for access policies later, a well-organized path structure makes it straightforward to grant a service account access to exactly what it needs and nothing else.

# API credentials
vault kv put secret/api-keys/openai \
  api_key="«redacted:sk-…»" \
  organization="your-org-id"

vault kv put secret/api-keys/github \
  token="ghp_your-github-token" \
  username="your-github-username"

# Database credentials
vault kv put secret/databases/production \
  username="db_admin" \
  password="secure_db_password" \
  host="db.example.com" \
  port="5432"

# Application config
vault kv put secret/applications/webapp \
  app_secret="your-app-secret-key" \
  jwt_secret="jwt-signing-key"

Path structure A consistent path hierarchy like secret/<category>/<service> is what makes Vault policies maintainable. A policy granting read access to secret/databases/* is far easier to audit than one with a list of individual paths that grew organically over time.

Step 3 — Retrieve Secrets

# Get full secret with metadata
vault kv get secret/api-keys/openai

# Get a specific field value
vault kv get -field=api_key secret/api-keys/openai

# JSON output for automation
vault kv get -format=json secret/databases/production

# Load a value into an environment variable at runtime
export DB_PASSWORD=$(vault kv get -field=password secret/databases/production)

The last pattern, pulling credentials at runtime rather than baking them into the environment at deploy time, is how you eliminate the class of findings where secrets end up in process lists, container environment dumps, or CI/CD logs.

Step 4 — Version Management

KV v2 retains every version of a secret. This matters both operationally and from a security standpoint. If a secret is compromised, you can identify exactly when it was last rotated and whether any version before the compromise is recoverable.

# View version history and metadata
vault kv metadata get secret/api-keys/openai

# Retrieve a specific version
vault kv get -version=1 secret/api-keys/openai

# Update a secret (creates new version, preserves history)
vault kv put secret/api-keys/openai \
  api_key="«redacted:sk-…»" \
  organization="your-org-id"

# Update a single field without touching others
vault kv patch secret/api-keys/openai \
  api_key="«redacted:sk-…»"

Step 5 — Secret Lifecycle and Deletion

# Soft delete — recoverable
vault kv delete secret/api-keys/old-service

# Recover a soft-deleted version
vault kv undelete -versions=3 secret/api-keys/old-service

# Delete specific versions permanently
vault kv destroy -versions=1,2 secret/api-keys/openai

Destroy is irreversible vault kv destroy permanently removes the secret data for those versions. The metadata entry remains but the values are gone. Use soft delete (vault kv delete) unless you have a specific reason to permanently destroy versions.

Troubleshooting

Permission Denied (403) — The token doesn’t have a policy granting access to that path, or the KV engine isn’t mounted where you expect. Run vault secrets list to verify the mount path and check your token’s policies with vault token lookup.

No handler for route — The secrets engine isn’t mounted at that path. Enable it with vault secrets enable -path=secret kv-v2 and verify with vault secrets list.

No value found — The secret either doesn’t exist at that path or was soft-deleted. Run vault kv list secret/ to see what’s there and vault kv metadata get <path> to check deletion status.

Key Points