Secrets Management: Best Practices and Tool Guide

Type: Software Reference Confidence: 0.94 Sources: 7 Verified: 2026-02-27 Freshness: 2026-02-27

TL;DR

Constraints

Quick Reference

Tool Comparison

ToolTypeBest ForRotationCostComplexityCloud Lock-in
HashiCorp VaultSelf-hosted / HCPMulti-cloud, enterprise, dynamic secretsBuilt-in (dynamic)Free OSS / HCP from $0.03/hrHighNone
AWS Secrets ManagerManaged serviceAWS-native workloadsBuilt-in (Lambda)$0.40/secret/mo + $0.05/10K APILowAWS
GCP Secret ManagerManaged serviceGCP-native workloadsCustom (Cloud Functions)$0.06/active version/moLowGCP
Azure Key VaultManaged serviceAzure-native workloadsBuilt-in (certificates)$0.03/10K ops (Standard)LowAzure
SOPS (CNCF)File encryptionGitOps, encrypted config in reposManualFree OSSMediumNone
git-cryptFile encryptionSimple repo encryptionManualFree OSSLowNone
dotenv (.env)Local fileLocal development onlyManualFreeLowNone

Secret Types and Recommended Storage

Secret TypeRecommended StorageRotation FrequencyNotes
Database credentialsVault dynamic secrets / cloud SMPer-connection or monthlyPrefer dynamic (ephemeral) credentials
API keys (third-party)Cloud secrets managerQuarterly or on compromiseStore with metadata (expiry, owner, scope)
TLS certificatesVault PKI / cloud certificate manager90 days or annuallyAutomate with cert-manager in K8s
SSH keysVault SSH secrets enginePer-session or dailySigned certificates preferred over static keys
Encryption keysCloud KMS (not secrets manager)Annually or per policyEnvelope encryption -- never store KEK with DEK
CI/CD tokensCI/CD built-in secrets + short TTLPer-pipeline runAvoid long-lived tokens; use OIDC federation
.env filesLocal only, never committedN/AAdd to .gitignore; use secrets manager for production

Decision Tree

START: What is your deployment environment?
├── Single cloud (AWS/GCP/Azure)?
│   ├── YES → Use cloud-native secrets manager (AWS SM, GCP SM, Azure KV)
│   └── NO ↓
├── Multi-cloud or hybrid?
│   ├── YES → Use HashiCorp Vault (self-hosted or HCP)
│   └── NO ↓
├── Kubernetes-only?
│   ├── YES → External Secrets Operator + cloud SM or Vault
│   └── NO ↓
├── Small team, few secrets, GitOps workflow?
│   ├── YES → SOPS + cloud KMS for encryption keys
│   └── NO ↓
├── Local development only?
│   ├── YES → dotenv (.env files in .gitignore) + git-secrets pre-commit hook
│   └── NO ↓
└── DEFAULT → HashiCorp Vault (most flexible, no lock-in)

Step-by-Step Guide

1. Install pre-commit secret detection

Prevent secrets from ever reaching version control. Install git-secrets as a pre-commit hook. [src6]

# Install git-secrets
brew install git-secrets   # macOS

# Initialize hooks in your repo
cd /path/to/your/repo
git secrets --install
git secrets --register-aws

# Add custom patterns
git secrets --add 'PRIVATE KEY'
git secrets --add 'password\s*=\s*["\047][^"\047]+'

Verify: git secrets --scan → should report any existing secrets in the repo.

2. Set up a centralized secrets manager

Choose based on your infrastructure. Example: HashiCorp Vault dev server for evaluation. [src2]

# Start Vault dev server (evaluation only)
vault server -dev -dev-root-token-id="dev-only-token"

# Store a secret
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='dev-only-token'
vault kv put secret/myapp/db username="dbadmin" password="s3cur3-p@ssw0rd"

Verify: vault kv get secret/myapp/db → should display stored key-value pairs.

3. Configure application to fetch secrets at runtime

Replace hardcoded credentials with secrets manager SDK calls. [src1]

import os, hvac  # pip install hvac>=2.4.0

client = hvac.Client(
    url=os.environ['VAULT_ADDR'],
    token=os.environ['VAULT_TOKEN']
)
secret = client.secrets.kv.v2.read_secret_version(
    path='myapp/db', mount_point='secret'
)
db_password = secret['data']['data']['password']

Verify: Application starts and connects to the database without credentials in source code.

4. Implement automated secret rotation

Configure automatic rotation to limit blast radius of compromised credentials. [src3]

# AWS: Enable rotation with Lambda
aws secretsmanager rotate-secret \
  --secret-id myapp/db-credentials \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:123456789:function:SecretsRotation \
  --rotation-rules '{"AutomaticallyAfterDays": 30}'

Verify: aws secretsmanager describe-secret --secret-id myapp/db-credentialsRotationEnabled: true.

5. Encrypt secrets in configuration files (GitOps)

For GitOps workflows, encrypt secrets in-repo with SOPS. [src7]

# Encrypt a secrets file (values only)
sops --encrypt secrets/production.yaml > secrets/production.enc.yaml

# Decrypt at deploy time
sops --decrypt secrets/production.enc.yaml > /tmp/production.yaml

Verify: cat secrets/production.enc.yaml → keys readable, values encrypted.

Code Examples

Python: HashiCorp Vault with hvac

# Input:  VAULT_ADDR and VAULT_TOKEN environment variables
# Output: Database credentials fetched from Vault KV v2

import os
import hvac  # pip install hvac>=2.4.0

def get_vault_client():
    client = hvac.Client(
        url=os.environ['VAULT_ADDR'],
        token=os.environ['VAULT_TOKEN']
    )
    if not client.is_authenticated():
        raise RuntimeError("Vault authentication failed")
    return client

def get_db_credentials(client, path='myapp/db'):
    resp = client.secrets.kv.v2.read_secret_version(
        path=path, mount_point='secret'
    )
    return resp['data']['data']

Node.js: AWS Secrets Manager

// Input:  AWS credentials (IAM role or env vars) + secret name
// Output: Parsed secret object with database credentials

const { SecretsManagerClient, GetSecretValueCommand }
  = require("@aws-sdk/client-secrets-manager");  // ^3.500.0

async function getSecret(secretId, region = "us-east-1") {
  const client = new SecretsManagerClient({ region });
  const resp = await client.send(
    new GetSecretValueCommand({ SecretId: secretId })
  );
  return JSON.parse(resp.SecretString);
}

Python: AWS Secrets Manager with boto3

# Input:  AWS credentials (IAM role or env vars) + secret name
# Output: Parsed secret dict with credentials

import json, boto3  # pip install boto3>=1.34.0

def get_secret(secret_name, region="us-east-1"):
    client = boto3.client("secretsmanager", region_name=region)
    resp = client.get_secret_value(SecretId=secret_name)
    return json.loads(resp["SecretString"])

Bash: git-secrets Pre-Commit Hook Setup

# Input:  A git repository
# Output: Pre-commit hook that blocks secret commits

git secrets --install
git secrets --register-aws
git secrets --add 'PRIVATE KEY'
git secrets --add 'password\s*=\s*["\047][^"\047]+'

# Verify: attempt to commit a test secret
echo "AWS_SECRET=AKIAIOSFODNN7EXAMPLE" > test.txt
git add test.txt && git commit -m "test"  # blocked

Anti-Patterns

Wrong: Hardcoded secrets in source code

# BAD -- CWE-798: hardcoded credentials
import psycopg2
conn = psycopg2.connect(
    host="prod-db.example.com",
    user="admin",
    password="SuperSecret123!"  # hardcoded
)

Correct: Fetch secrets from a secrets manager

# GOOD -- credentials fetched at runtime from Vault
import os, hvac, psycopg2
client = hvac.Client(url=os.environ['VAULT_ADDR'], token=os.environ['VAULT_TOKEN'])
creds = client.secrets.kv.v2.read_secret_version(path='myapp/db')['data']['data']
conn = psycopg2.connect(
    host=creds['host'], user=creds['username'], password=creds['password']
)

Wrong: Secrets committed to git history

# BAD -- git history retains the secret forever
echo "API_KEY=sk-live-abc123xyz" > config.env
git add config.env && git commit -m "add config"
# Even git rm config.env doesn't remove from history

Correct: Pre-commit hooks + .gitignore

# GOOD -- prevent secrets from ever entering the repo
echo "*.env" >> .gitignore
git secrets --install
git secrets --register-aws

Wrong: Secrets baked into Docker image

# BAD -- secret visible via docker history
FROM node:20-alpine
ENV DATABASE_URL="postgresql://admin:secret@db:5432/myapp"
COPY . /app
CMD ["node", "server.js"]

Correct: Inject secrets at container runtime

# GOOD -- no secrets in the image
FROM node:20-alpine
COPY . /app
RUN npm install
# Run with: docker run -e DATABASE_URL="$(vault kv get ...)" myapp
CMD ["node", "server.js"]

Wrong: Env vars visible in process listing

# BAD -- visible via /proc/PID/environ
docker run -e DB_PASSWORD="secret123" myapp

Correct: Mount secrets as files in tmpfs

# GOOD -- Kubernetes: secrets via sidecar on tmpfs
apiVersion: v1
kind: Pod
spec:
  volumes:
    - name: secrets
      emptyDir:
        medium: Memory  # tmpfs
  containers:
    - name: app
      volumeMounts:
        - name: secrets
          mountPath: /mnt/secrets
          readOnly: true

Common Pitfalls

Diagnostic Commands

# Scan repo for hardcoded secrets
git secrets --scan

# Check for high-entropy strings with trufflehog
trufflehog git file://. --only-verified

# Verify Vault connectivity and authentication
vault status
vault token lookup

# List secrets in a Vault KV path
vault kv list secret/myapp/

# Check AWS Secrets Manager rotation status
aws secretsmanager describe-secret --secret-id myapp/db-credentials \
  --query '{RotationEnabled: RotationEnabled, LastRotated: LastRotatedDate}'

# Scan Docker image for embedded secrets
docker history --no-trunc myapp:latest | grep -i -E 'password|secret|key|token'

# Verify SOPS encryption
sops --decrypt secrets/production.enc.yaml > /dev/null && echo "OK"

Version History & Compatibility

ToolVersionStatusKey Change
HashiCorp Vault1.20.xCurrent (GA June 2025)Secrets Operator for K8s, IBM RACF support
HashiCorp Vault1.18.xLTSVault Proxy, Secrets Sync
AWS Secrets ManagerCurrentGACross-account sharing, Lambda rotation v2
GCP Secret Managerv1GARegional replication, IAM conditions
Azure Key VaultCurrentGARBAC model, soft-delete default
SOPS3.9.xCurrent (CNCF Sandbox)age encryption support, audit logging
hvac (Python)2.4.xCurrentPython 3.8+, KV v2 improvements
External Secrets Operator0.10.xCurrentMulti-provider, PushSecret CRD

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Any production credentials (DB, API keys, tokens)Storing non-sensitive config (feature flags, app settings)Environment variables or config files
Multi-service architecture sharing credentialsSingle-developer local prototype.env file with .gitignore
Compliance requires audit trail for credential accessStoring user passwords for authenticationPassword hashing (bcrypt, argon2)
Credentials need automatic rotationManaging public keys onlyStandard key distribution / PKI
GitOps workflow needs encrypted secrets in repoSmall static secrets that never changeSOPS may be overkill; consider sealed secrets

Important Caveats

Related Units