Skip to main content
Technology & EngineeringVibe Coding Security391 lines

Credential Management

Quick Summary24 lines
AI-generated code loves hardcoded secrets. API keys inline, database passwords in config files, tokens committed to git. The AI doesn't understand that the string it just wrote will end up on GitHub, searchable by bots that scrape for credentials 24/7.

## Key Points

- repo: https://github.com/gitleaks/gitleaks
- repo: https://github.com/trufflesecurity/trufflehog

## Quick Example

```bash
# Install pre-commit and set up hooks
pip install pre-commit
pre-commit install
```

```bash
# After cleaning history, force push
git push --force --all

# Notify all team members to re-clone
# The old history with secrets may still exist in local clones
```
skilldb get vibe-coding-security-skills/credential-managementFull skill: 391 lines
Paste into your CLAUDE.md or agent config

Credential Management

AI-generated code loves hardcoded secrets. API keys inline, database passwords in config files, tokens committed to git. The AI doesn't understand that the string it just wrote will end up on GitHub, searchable by bots that scrape for credentials 24/7.

This skill covers every layer of credential management: how to store them, rotate them, detect leaks, and recover when things go wrong.

The Hardcoded Credentials Anti-Pattern

What AI generates constantly:

const stripe = require('stripe')('sk_live_abc123...');

const db = new Pool({
  host: 'db.example.com',
  user: 'admin',
  password: 'SuperSecret123!',
  database: 'production'
});

const AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE';
const AWS_SECRET_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';

This code will work perfectly. It will also expose your credentials the moment it touches version control.

Environment Variables: The First Layer

Environment variables are the minimum viable approach. Not the best — but infinitely better than hardcoding.

Correct pattern:

// Load from environment
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const db = new Pool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
});

// Validate that required env vars are present at startup
const required = ['STRIPE_SECRET_KEY', 'DB_HOST', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
for (const key of required) {
  if (!process.env[key]) {
    console.error(`Missing required environment variable: ${key}`);
    process.exit(1);
  }
}

Python equivalent:

import os
import sys

def require_env(key: str) -> str:
    value = os.environ.get(key)
    if not value:
        print(f"Missing required environment variable: {key}", file=sys.stderr)
        sys.exit(1)
    return value

DATABASE_URL = require_env("DATABASE_URL")
STRIPE_KEY = require_env("STRIPE_SECRET_KEY")

.env Files and .gitignore

The .env file (never committed):

# .env — LOCAL DEVELOPMENT ONLY
STRIPE_SECRET_KEY=sk_test_abc123
DB_HOST=localhost
DB_USER=dev
DB_PASSWORD=localdevpassword
DB_NAME=myapp_dev

The .env.example file (committed, no real values):

# .env.example — Template for developers
STRIPE_SECRET_KEY=sk_test_YOUR_KEY_HERE
DB_HOST=localhost
DB_USER=dev
DB_PASSWORD=changeme
DB_NAME=myapp_dev

The .gitignore entries you must have:

# Environment files
.env
.env.local
.env.*.local
.env.production
.env.staging

# Key files
*.pem
*.key
*.p12
*.pfx

# Service account files
*-credentials.json
service-account*.json
firebase-adminsdk*.json

# IDE files that may cache env vars
.idea/
.vscode/settings.json

Secret Managers: The Production Approach

Environment variables in production should come from a secret manager, not from files on disk.

AWS Secrets Manager

import { SecretsManagerClient, GetSecretValueCommand } from '@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);
  return JSON.parse(response.SecretString);
}

// At startup
const dbCreds = await getSecret('prod/myapp/database');
const db = new Pool({
  host: dbCreds.host,
  user: dbCreds.username,
  password: dbCreds.password,
  database: dbCreds.dbname,
});

GCP Secret Manager

import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

const client = new SecretManagerServiceClient();

async function getSecret(secretName) {
  const [version] = await client.accessSecretVersion({
    name: `projects/my-project/secrets/${secretName}/versions/latest`,
  });
  return version.payload.data.toString('utf8');
}

// At startup
const dbPassword = await getSecret('database-password');
const stripeKey = await getSecret('stripe-secret-key');

HashiCorp Vault

import vault from 'node-vault';

const vaultClient = vault({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN, // Use AppRole in production
});

async function getSecret(path) {
  const result = await vaultClient.read(path);
  return result.data.data;
}

const creds = await getSecret('secret/data/myapp/database');

Rotating Secrets

Secrets should rotate regularly. AI never sets up rotation.

Database Password Rotation with AWS

# Lambda function for rotating RDS credentials
import boto3
import json
import string
import random

def lambda_handler(event, context):
    secret_client = boto3.client('secretsmanager')
    rds_client = boto3.client('rds')

    step = event['Step']
    secret_arn = event['SecretId']

    if step == 'createSecret':
        # Generate new password
        chars = string.ascii_letters + string.digits + '!@#$%^&*'
        new_password = ''.join(random.SystemRandom().choice(chars) for _ in range(32))

        secret_client.put_secret_value(
            SecretId=secret_arn,
            ClientRequestToken=event['ClientRequestToken'],
            SecretString=json.dumps({
                'username': 'app_service',
                'password': new_password
            }),
            VersionStages=['AWSPENDING']
        )

    elif step == 'setSecret':
        # Update the password in the database
        pending = json.loads(secret_client.get_secret_value(
            SecretId=secret_arn,
            VersionStage='AWSPENDING'
        )['SecretString'])
        # Execute ALTER USER with new password
        # ...

    elif step == 'finishSecret':
        secret_client.update_secret_version_stage(
            SecretId=secret_arn,
            VersionStage='AWSCURRENT',
            MoveToVersionId=event['ClientRequestToken']
        )

API Key Rotation Pattern

// Support dual keys during rotation
class ApiKeyManager {
  constructor(secretManager) {
    this.secretManager = secretManager;
  }

  async validateKey(providedKey) {
    const currentKey = await this.secretManager.getSecret('api-key-current');
    const previousKey = await this.secretManager.getSecret('api-key-previous');

    // Accept both keys during rotation window
    return providedKey === currentKey || providedKey === previousKey;
  }

  async rotateKey() {
    const currentKey = await this.secretManager.getSecret('api-key-current');
    const newKey = crypto.randomBytes(32).toString('hex');

    // Move current to previous
    await this.secretManager.setSecret('api-key-previous', currentKey);
    // Set new as current
    await this.secretManager.setSecret('api-key-current', newKey);

    return newKey;
  }
}

Detecting Leaked Credentials

truffleHog: Scan Git History

# Install
pip install trufflehog

# Scan entire git history
trufflehog git file://. --only-verified

# Scan a specific branch
trufflehog git file://. --branch main --only-verified

# Scan before a commit (pre-commit hook)
trufflehog git file://. --since-commit HEAD~1

gitleaks: Fast Regex-Based Scanning

# Install
brew install gitleaks

# Scan repository
gitleaks detect --source . --verbose

# Scan staged changes only (for pre-commit)
gitleaks protect --staged --verbose

# Use in CI
gitleaks detect --source . --report-format json --report-path gitleaks-report.json

Pre-Commit Hook Setup

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

  - repo: https://github.com/trufflesecurity/trufflehog
    rev: v3.63.0
    hooks:
      - id: trufflehog
        args: ['git', 'file://.',  '--only-verified', '--since-commit', 'HEAD']
# Install pre-commit and set up hooks
pip install pre-commit
pre-commit install

When Credentials Are Leaked

If you find credentials in your git history, act immediately:

Step 1: Revoke the Credential

# Don't just rotate — the old one is exposed. Revoke it entirely.
# AWS: Deactivate the access key
aws iam update-access-key --access-key-id AKIAIOSFODNN7EXAMPLE --status Inactive
aws iam delete-access-key --access-key-id AKIAIOSFODNN7EXAMPLE

# GCP: Delete the service account key
gcloud iam service-accounts keys delete KEY_ID --iam-account=SA_EMAIL

# Stripe: Roll the API key in the dashboard immediately

Step 2: Check for Unauthorized Use

# AWS: Check CloudTrail for the compromised key
aws cloudtrail lookup-events --lookup-attributes \
  AttributeKey=AccessKeyId,AttributeValue=AKIAIOSFODNN7EXAMPLE

# GCP: Check audit logs
gcloud logging read 'protoPayload.authenticationInfo.principalEmail="compromised-sa@project.iam.gserviceaccount.com"'

Step 3: Remove from Git History

# Use git-filter-repo (preferred over BFG)
pip install git-filter-repo

# Remove a file containing secrets from all history
git filter-repo --invert-paths --path config/secrets.json

# Replace a specific string in all history
git filter-repo --replace-text expressions.txt
# expressions.txt contains: sk_live_abc123==>REDACTED

Step 4: Force Push and Notify

# After cleaning history, force push
git push --force --all

# Notify all team members to re-clone
# The old history with secrets may still exist in local clones

Credential Hygiene Checklist

CheckToolFrequency
No hardcoded secrets in codegitleaks + pre-commitEvery commit
Git history is cleantruffleHog full scanWeekly / before release
Secrets are rotatedSecret manager rotationEvery 90 days minimum
Unused credentials removedCloud IAM auditMonthly
.gitignore covers all secret filesManual reviewEvery PR that adds config
.env.example has no real valuesPR reviewEvery PR
Service account keys are minimalIAM recommenderMonthly

The Golden Rule

Never store secrets in code. Never store secrets in git. Never log secrets. Never pass secrets as command-line arguments (they show up in ps). Never include secrets in Docker image layers. Treat every secret as if it will be leaked — because eventually, one will be.

Install this skill directly: skilldb add vibe-coding-security-skills

Get CLI access →