Skip to main content
Technology & EngineeringSecurity Practices297 lines

Secrets Management

Securely store, access, rotate, and audit application secrets and credentials using vaults, environment variables, and CI/CD integrations.

Quick Summary28 lines
You are an expert in managing application secrets, API keys, database credentials, and encryption keys securely throughout the development and deployment lifecycle.

## Key Points

- Database connection strings and passwords
- API keys and access tokens (third-party services, internal services)
- OAuth client secrets
- Encryption keys and signing keys
- TLS/SSL private keys and certificates
- Webhook signing secrets
- Service account credentials (GCP, AWS IAM)
1. **Generation**: Create with sufficient entropy using cryptographic random generators.
2. **Storage**: Encrypt at rest in a dedicated secrets manager.
3. **Distribution**: Inject at runtime via environment variables, mounted files, or API calls.
4. **Rotation**: Replace secrets on a regular schedule and immediately on suspected compromise.
5. **Revocation**: Disable compromised secrets immediately.

## Quick Example

```bash
# Initialize baseline
detect-secrets scan > .secrets.baseline

# Audit flagged secrets
detect-secrets audit .secrets.baseline
```
skilldb get security-practices-skills/Secrets ManagementFull skill: 297 lines
Paste into your CLAUDE.md or agent config

Secrets and Credential Management — Application Security

You are an expert in managing application secrets, API keys, database credentials, and encryption keys securely throughout the development and deployment lifecycle.

Core Philosophy

Secrets management begins with a fundamental assumption: any secret that can be exposed will eventually be exposed. The goal is not to create an impenetrable vault but to minimize the blast radius and recovery time when exposure happens. This means secrets should be short-lived, narrowly scoped, automatically rotated, and audited — so that a leaked credential is useless by the time an attacker tries to use it.

The most dangerous secrets are the ones nobody knows about. Secret sprawl — credentials scattered across environment files, CI configs, chat logs, and developer laptops — is the norm in organizations that lack centralized management. A mature secrets practice maintains an inventory of every credential in the system, knows who has access to each one, and can revoke any credential within minutes. Without this visibility, rotation is impossible and incident response becomes a guessing game.

Secrets should be injected, never embedded. The application code should have no knowledge of where secrets are stored or how they are retrieved at the infrastructure level. It receives credentials through environment variables, mounted files, or runtime API calls — and validates their presence at startup with a fast, loud failure. This separation ensures that secrets can be rotated, migrated to a new vault, or revoked without touching application code, and that no credential ever appears in a git commit, Docker layer, or build log.

Anti-Patterns

  • Hardcoding secrets in source code "just for now": Temporary hardcoded credentials have a way of becoming permanent. Once a secret enters version control, it persists in git history indefinitely, even after deletion, and is accessible to anyone with repository access.

  • Using the same credentials across all environments: Sharing secrets between development, staging, and production means a compromised dev environment grants immediate access to production data. Each environment must have its own isolated credentials.

  • Treating secret rotation as a manual, infrequent task: Manual rotation is error-prone and rarely happens on schedule. Without automation, credentials remain valid for months or years, giving attackers an extended window of exploitation after a breach.

  • Sharing secrets through Slack, email, or shared documents: Plaintext communication channels log messages, cache content, and are accessible to anyone with channel access. Use a dedicated secrets-sharing tool with expiring links and access logging.

  • Baking secrets into Docker images or build artifacts: Secrets embedded in image layers persist even if a subsequent layer "deletes" them. Anyone who pulls the image can extract every secret from any layer. Always inject secrets at runtime, never at build time.

Overview

Secrets — API keys, database passwords, tokens, encryption keys, certificates — are the most sensitive data in any application. Exposing a single secret can compromise an entire system. Proper secrets management means secrets are never hardcoded, are stored in encrypted vaults, are injected at runtime, are rotated regularly, and are audited for access.

Core Concepts

What Counts as a Secret

  • Database connection strings and passwords
  • API keys and access tokens (third-party services, internal services)
  • OAuth client secrets
  • Encryption keys and signing keys
  • TLS/SSL private keys and certificates
  • SSH keys
  • Webhook signing secrets
  • Service account credentials (GCP, AWS IAM)

Secrets Lifecycle

  1. Generation: Create with sufficient entropy using cryptographic random generators.
  2. Storage: Encrypt at rest in a dedicated secrets manager.
  3. Distribution: Inject at runtime via environment variables, mounted files, or API calls.
  4. Rotation: Replace secrets on a regular schedule and immediately on suspected compromise.
  5. Revocation: Disable compromised secrets immediately.
  6. Auditing: Log every access and change to secrets.

Storage Hierarchy (Best to Worst)

MethodSecurity Level
Dedicated secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager)Highest
CI/CD platform secrets (GitHub Actions secrets, GitLab CI variables)High
Encrypted environment variable files (.env.enc)Medium
Plain environment variables on the serverLow
Hardcoded in source codeUnacceptable

Implementation Patterns

Environment Variables with dotenv

// .env (NEVER committed to source control)
// DATABASE_URL=postgres://user:pass@host:5432/mydb
// STRIPE_SECRET_KEY=sk_live_...
// JWT_SECRET=a3f8c9...

// app.js
require('dotenv').config();

const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
  throw new Error('DATABASE_URL is not set');
}

// Validate that all required secrets are present at startup
const REQUIRED_SECRETS = ['DATABASE_URL', 'STRIPE_SECRET_KEY', 'JWT_SECRET'];

function validateSecrets() {
  const missing = REQUIRED_SECRETS.filter(key => !process.env[key]);
  if (missing.length > 0) {
    throw new Error(`Missing required secrets: ${missing.join(', ')}`);
  }
}

validateSecrets();
# .gitignore — always exclude secret files
.env
.env.local
.env.production
*.pem
*.key
credentials.json
service-account.json

AWS Secrets Manager

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

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName) {
  const command = new GetSecretValueCommand({ SecretId: secretName });
  const response = await client.send(command);

  if (response.SecretString) {
    return JSON.parse(response.SecretString);
  }
  throw new Error('Secret is binary, not string');
}

// Cache secrets to avoid repeated API calls
const secretsCache = new Map();

async function getCachedSecret(name, ttlMs = 300000) {
  const cached = secretsCache.get(name);
  if (cached && Date.now() - cached.fetchedAt < ttlMs) {
    return cached.value;
  }

  const value = await getSecret(name);
  secretsCache.set(name, { value, fetchedAt: Date.now() });
  return value;
}

// Usage
async function connectDatabase() {
  const creds = await getCachedSecret('prod/database');
  return createPool({
    host: creds.host,
    port: creds.port,
    user: creds.username,
    password: creds.password,
    database: creds.dbname,
  });
}

GCP Secret Manager

from google.cloud import secretmanager

def get_secret(project_id: str, secret_id: str, version: str = "latest") -> str:
    client = secretmanager.SecretManagerServiceClient()
    name = f"projects/{project_id}/secrets/{secret_id}/versions/{version}"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")

# Usage
db_password = get_secret("my-project", "db-password")

HashiCorp Vault

const vault = require('node-vault')({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN,
});

async function getDbCredentials() {
  // Read from KV secrets engine
  const result = await vault.read('secret/data/production/database');
  return result.data.data; // { username, password, host, port }
}

// Dynamic secrets — Vault generates temporary database credentials
async function getDynamicDbCreds() {
  const result = await vault.read('database/creds/app-role');
  return {
    username: result.data.username,
    password: result.data.password,
    lease_id: result.lease_id,
    ttl: result.lease_duration,
  };
}

GitHub Actions Secrets

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          # Secrets are masked in logs automatically
          npm run deploy

      # NEVER echo or print secrets
      # BAD: run: echo ${{ secrets.API_KEY }}

Secret Rotation Script

import secrets
import string
import boto3
from datetime import datetime

def generate_strong_password(length=32):
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
    return ''.join(secrets.choice(alphabet) for _ in range(length))

def rotate_database_password(secret_name: str):
    sm = boto3.client('secretmanager')

    # Generate new password
    new_password = generate_strong_password()

    # Update in the database first
    update_db_user_password("app_user", new_password)

    # Then update the secret
    current = sm.get_secret_value(SecretId=secret_name)
    secret_data = json.loads(current['SecretString'])
    secret_data['password'] = new_password
    secret_data['rotated_at'] = datetime.utcnow().isoformat()

    sm.update_secret(
        SecretId=secret_name,
        SecretString=json.dumps(secret_data)
    )

    print(f"Rotated secret {secret_name} at {datetime.utcnow()}")

Pre-Commit Hook for Secret Detection

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']
# Initialize baseline
detect-secrets scan > .secrets.baseline

# Audit flagged secrets
detect-secrets audit .secrets.baseline

Best Practices

  1. Never hardcode secrets: Not in source code, not in configuration files committed to git, not in Docker images.
  2. Use a dedicated secrets manager in production: AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or Azure Key Vault.
  3. Validate secrets at startup: Fail fast with a clear error if a required secret is missing rather than failing later with a cryptic error.
  4. Rotate secrets regularly: Automate rotation. Use short-lived credentials (dynamic secrets, short-expiry tokens) where possible.
  5. Use pre-commit hooks to detect secrets: Tools like detect-secrets, trufflehog, or gitleaks catch accidental commits.
  6. Scope secrets narrowly: Each service should have its own credentials with minimal permissions.
  7. Audit secret access: Enable logging in your secrets manager to track who accessed what and when.
  8. Encrypt secrets at rest and in transit: Secrets managers handle this, but if using env files, encrypt them (e.g., sops, age).
  9. Treat CI/CD secrets carefully: Use the platform's built-in secrets feature. Never print secrets to build logs.

Common Pitfalls

  • Committing .env files: Even if deleted later, secrets remain in git history. Use .gitignore from the start and audit with gitleaks.
  • Logging secrets accidentally: Debug logging, error messages, and stack traces can leak secrets. Redact sensitive fields in log output.
  • Using the same secret across environments: Dev, staging, and production should have separate credentials.
  • Never rotating secrets: If a secret is compromised, old credentials remain valid indefinitely. Rotation limits the window of exposure.
  • Storing secrets in Docker images: Secrets baked into image layers persist even if "deleted" in a later layer. Use runtime injection.
  • Sharing secrets via Slack or email: Use a secrets manager or a short-lived secure sharing tool. Never share via plaintext channels.
  • Ignoring secret sprawl: As the system grows, track all secrets in a centralized inventory. Unknown secrets cannot be rotated or revoked.

Install this skill directly: skilldb add security-practices-skills

Get CLI access →