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. ## 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 linesCredential 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
| Check | Tool | Frequency |
|---|---|---|
| No hardcoded secrets in code | gitleaks + pre-commit | Every commit |
| Git history is clean | truffleHog full scan | Weekly / before release |
| Secrets are rotated | Secret manager rotation | Every 90 days minimum |
| Unused credentials removed | Cloud IAM audit | Monthly |
| .gitignore covers all secret files | Manual review | Every PR that adds config |
| .env.example has no real values | PR review | Every PR |
| Service account keys are minimal | IAM recommender | Monthly |
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