add brain
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
# env-secrets-manager reference
|
||||
|
||||
## Required Variable Validation Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/validate-env.sh
|
||||
# Run at app startup or in CI before deploy
|
||||
# Exit 1 if any required var is missing or empty
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MISSING=()
|
||||
WARNINGS=()
|
||||
|
||||
# --- Define required vars by environment ---
|
||||
ALWAYS_REQUIRED=(
|
||||
APP_SECRET
|
||||
APP_URL
|
||||
DATABASE_URL
|
||||
AUTH_JWT_SECRET
|
||||
AUTH_REFRESH_SECRET
|
||||
)
|
||||
|
||||
PROD_REQUIRED=(
|
||||
STRIPE_SECRET_KEY
|
||||
STRIPE_WEBHOOK_SECRET
|
||||
SENTRY_DSN
|
||||
)
|
||||
|
||||
# --- Check always-required vars ---
|
||||
for var in "${ALWAYS_REQUIRED[@]}"; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
MISSING+=("$var")
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Check prod-only vars ---
|
||||
if [ "${APP_ENV:-}" = "production" ] || [ "${NODE_ENV:-}" = "production" ]; then
|
||||
for var in "${PROD_REQUIRED[@]}"; do
|
||||
if [ -z "${!var:-}" ]; then
|
||||
MISSING+=("$var (required in production)")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# --- Validate format/length constraints ---
|
||||
if [ -n "${AUTH_JWT_SECRET:-}" ] && [ ${#AUTH_JWT_SECRET} -lt 32 ]; then
|
||||
WARNINGS+=("AUTH_JWT_SECRET is shorter than 32 chars — insecure")
|
||||
fi
|
||||
|
||||
if [ -n "${DATABASE_URL:-}" ]; then
|
||||
if ! echo "$DATABASE_URL" | grep -qE "^(postgres|postgresql|mysql|mongodb|redis)://"; then
|
||||
WARNINGS+=("DATABASE_URL doesn't look like a valid connection string")
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "${APP_PORT:-}" ]; then
|
||||
if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]] || [ "$APP_PORT" -lt 1 ] || [ "$APP_PORT" -gt 65535 ]; then
|
||||
WARNINGS+=("APP_PORT=$APP_PORT is not a valid port number")
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Report ---
|
||||
if [ ${#WARNINGS[@]} -gt 0 ]; then
|
||||
echo "WARNINGS:"
|
||||
for w in "${WARNINGS[@]}"; do
|
||||
echo " ⚠️ $w"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#MISSING[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
echo "FATAL: Missing required environment variables:"
|
||||
for var in "${MISSING[@]}"; do
|
||||
echo " ❌ $var"
|
||||
done
|
||||
echo ""
|
||||
echo "Copy .env.example to .env and fill in missing values."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All required environment variables are set"
|
||||
```
|
||||
|
||||
Node.js equivalent:
|
||||
```typescript
|
||||
// src/config/validateEnv.ts
|
||||
const required = [
|
||||
'APP_SECRET', 'APP_URL', 'DATABASE_URL',
|
||||
'AUTH_JWT_SECRET', 'AUTH_REFRESH_SECRET',
|
||||
]
|
||||
|
||||
const missing = required.filter(key => !process.env[key])
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.error('FATAL: Missing required environment variables:', missing)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (process.env.AUTH_JWT_SECRET && process.env.AUTH_JWT_SECRET.length < 32) {
|
||||
console.error('FATAL: AUTH_JWT_SECRET must be at least 32 characters')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
export const config = {
|
||||
appSecret: process.env.APP_SECRET!,
|
||||
appUrl: process.env.APP_URL!,
|
||||
databaseUrl: process.env.DATABASE_URL!,
|
||||
jwtSecret: process.env.AUTH_JWT_SECRET!,
|
||||
refreshSecret: process.env.AUTH_REFRESH_SECRET!,
|
||||
stripeKey: process.env.STRIPE_SECRET_KEY, // optional
|
||||
port: parseInt(process.env.APP_PORT ?? '3000', 10),
|
||||
} as const
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Secret Leak Detection
|
||||
|
||||
### Scan Working Tree
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/scan-secrets.sh
|
||||
# Scan staged files and working tree for common secret patterns
|
||||
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local label="$1"
|
||||
local pattern="$2"
|
||||
local matches
|
||||
|
||||
matches=$(git diff --cached -U0 2>/dev/null | grep "^+" | grep -vE "^(\+\+\+|#|\/\/)" | \
|
||||
grep -E "$pattern" | grep -v ".env.example" | grep -v "test\|mock\|fixture\|fake" || true)
|
||||
|
||||
if [ -n "$matches" ]; then
|
||||
echo "SECRET DETECTED [$label]:"
|
||||
echo "$matches" | head -5
|
||||
FAIL=1
|
||||
fi
|
||||
}
|
||||
|
||||
# AWS Access Keys
|
||||
check "AWS Access Key" "AKIA[0-9A-Z]{16}"
|
||||
check "AWS Secret Key" "aws_secret_access_key\s*=\s*['\"]?[A-Za-z0-9/+]{40}"
|
||||
|
||||
# Stripe
|
||||
check "Stripe Live Key" "sk_live_[0-9a-zA-Z]{24,}"
|
||||
check "Stripe Test Key" "sk_test_[0-9a-zA-Z]{24,}"
|
||||
check "Stripe Webhook" "whsec_[0-9a-zA-Z]{32,}"
|
||||
|
||||
# JWT / Generic secrets
|
||||
check "Hardcoded JWT" "eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}"
|
||||
check "Generic Secret" "(secret|password|passwd|api_key|apikey|token)\s*[:=]\s*['\"][^'\"]{12,}['\"]"
|
||||
|
||||
# Private keys
|
||||
check "Private Key Block" "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----"
|
||||
check "PEM Certificate" "-----BEGIN CERTIFICATE-----"
|
||||
|
||||
# Connection strings with credentials
|
||||
check "DB Connection" "(postgres|mysql|mongodb)://[^:]+:[^@]+@"
|
||||
check "Redis Auth" "redis://:[^@]+@\|rediss://:[^@]+@"
|
||||
|
||||
# Google
|
||||
check "Google API Key" "AIza[0-9A-Za-z_-]{35}"
|
||||
check "Google OAuth" "[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com"
|
||||
|
||||
# GitHub
|
||||
check "GitHub Token" "gh[ps]_[A-Za-z0-9]{36,}"
|
||||
check "GitHub Fine-grained" "github_pat_[A-Za-z0-9_]{82}"
|
||||
|
||||
# Slack
|
||||
check "Slack Token" "xox[baprs]-[0-9A-Za-z]{10,}"
|
||||
check "Slack Webhook" "https://hooks\.slack\.com/services/[A-Z0-9]{9,}/[A-Z0-9]{9,}/[A-Za-z0-9]{24,}"
|
||||
|
||||
# Twilio
|
||||
check "Twilio SID" "AC[a-z0-9]{32}"
|
||||
check "Twilio Token" "SK[a-z0-9]{32}"
|
||||
|
||||
if [ $FAIL -eq 1 ]; then
|
||||
echo ""
|
||||
echo "BLOCKED: Secrets detected in staged changes."
|
||||
echo "Remove secrets before committing. Use environment variables instead."
|
||||
echo "If this is a false positive, add it to .secretsignore or use:"
|
||||
echo " git commit --no-verify (only if you're 100% certain it's safe)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No secrets detected in staged changes."
|
||||
```
|
||||
|
||||
### Scan Git History (post-incident)
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/scan-history.sh — scan entire git history for leaked secrets
|
||||
|
||||
PATTERNS=(
|
||||
"AKIA[0-9A-Z]{16}"
|
||||
"sk_live_[0-9a-zA-Z]{24}"
|
||||
"sk_test_[0-9a-zA-Z]{24}"
|
||||
"-----BEGIN.*PRIVATE KEY-----"
|
||||
"AIza[0-9A-Za-z_-]{35}"
|
||||
"ghp_[A-Za-z0-9]{36}"
|
||||
"xox[baprs]-[0-9A-Za-z]{10,}"
|
||||
)
|
||||
|
||||
for pattern in "${PATTERNS[@]}"; do
|
||||
echo "Scanning for: $pattern"
|
||||
git log --all -p --no-color 2>/dev/null | \
|
||||
grep -n "$pattern" | \
|
||||
grep "^+" | \
|
||||
grep -v "^+++" | \
|
||||
head -10
|
||||
done
|
||||
|
||||
# Alternative: use truffleHog or gitleaks for comprehensive scanning
|
||||
# gitleaks detect --source . --log-opts="--all"
|
||||
# trufflehog git file://. --only-verified
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pre-commit Hook Installation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Install the pre-commit hook
|
||||
HOOK_PATH=".git/hooks/pre-commit"
|
||||
|
||||
cat > "$HOOK_PATH" << 'HOOK'
|
||||
#!/bin/bash
|
||||
# Pre-commit: scan for secrets before every commit
|
||||
|
||||
SCRIPT="scripts/scan-secrets.sh"
|
||||
|
||||
if [ -f "$SCRIPT" ]; then
|
||||
bash "$SCRIPT"
|
||||
else
|
||||
# Inline fallback if script not present
|
||||
if git diff --cached -U0 | grep "^+" | grep -qE "AKIA[0-9A-Z]{16}|sk_live_|-----BEGIN.*PRIVATE KEY"; then
|
||||
echo "BLOCKED: Possible secret detected in staged changes."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
HOOK
|
||||
|
||||
chmod +x "$HOOK_PATH"
|
||||
echo "Pre-commit hook installed at $HOOK_PATH"
|
||||
```
|
||||
|
||||
Using `pre-commit` framework (recommended for teams):
|
||||
```yaml
|
||||
# .pre-commit-config.yaml
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.18.0
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: validate-env-example
|
||||
name: "check-envexample-is-up-to-date"
|
||||
language: script
|
||||
entry: bash scripts/check-env-example.sh
|
||||
pass_filenames: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credential Rotation Workflow
|
||||
|
||||
When a secret is leaked or compromised:
|
||||
|
||||
### Step 1 — Detect & Confirm
|
||||
```bash
|
||||
# Confirm which secret was exposed
|
||||
git log --all -p --no-color | grep -A2 -B2 "AKIA\|sk_live_\|SECRET"
|
||||
|
||||
# Check if secret is in any open PRs
|
||||
gh pr list --state open | while read pr; do
|
||||
gh pr diff $(echo $pr | awk '{print $1}') | grep -E "AKIA|sk_live_" && echo "Found in PR: $pr"
|
||||
done
|
||||
```
|
||||
|
||||
### Step 2 — Identify Exposure Window
|
||||
```bash
|
||||
# Find first commit that introduced the secret
|
||||
git log --all -p --no-color -- "*.env" "*.json" "*.yaml" "*.ts" "*.py" | \
|
||||
grep -B 10 "THE_LEAKED_VALUE" | grep "^commit" | tail -1
|
||||
|
||||
# Get commit date
|
||||
git show --format="%ci" COMMIT_HASH | head -1
|
||||
|
||||
# Check if secret appears in public repos (GitHub)
|
||||
gh api search/code -X GET -f q="THE_LEAKED_VALUE" | jq '.total_count, .items[].html_url'
|
||||
```
|
||||
|
||||
### Step 3 — Rotate Credential
|
||||
Per service — rotate immediately:
|
||||
- **AWS**: IAM console → delete access key → create new → update everywhere
|
||||
- **Stripe**: Dashboard → Developers → API keys → Roll key
|
||||
- **GitHub PAT**: Settings → Developer Settings → Personal access tokens → Revoke → Create new
|
||||
- **DB password**: `ALTER USER app_user PASSWORD 'new-strong-password-here';`
|
||||
- **JWT secret**: Rotate key (all existing sessions invalidated — users re-login)
|
||||
|
||||
### Step 4 — Update All Environments
|
||||
```bash
|
||||
# Update secret manager (source of truth)
|
||||
# Then redeploy to pull new values
|
||||
|
||||
# Vault KV v2
|
||||
vault kv put secret/myapp/prod \
|
||||
STRIPE_SECRET_KEY="sk_live_NEW..." \
|
||||
APP_SECRET="new-secret-here"
|
||||
|
||||
# AWS SSM
|
||||
aws ssm put-parameter \
|
||||
--name "/myapp/prod/STRIPE_SECRET_KEY" \
|
||||
--value "sk_live_NEW..." \
|
||||
--type "SecureString" \
|
||||
--overwrite
|
||||
|
||||
# 1Password
|
||||
op item edit "MyApp Prod" \
|
||||
--field "STRIPE_SECRET_KEY[password]=sk_live_NEW..."
|
||||
|
||||
# Doppler
|
||||
doppler secrets set STRIPE_SECRET_KEY="sk_live_NEW..." --project myapp --config prod
|
||||
```
|
||||
|
||||
### Step 5 — Remove from Git History
|
||||
```bash
|
||||
# WARNING: rewrites history — coordinate with team first
|
||||
git filter-repo --path-glob "*.env" --invert-paths
|
||||
|
||||
# Or remove specific string from all commits
|
||||
git filter-repo --replace-text <(echo "LEAKED_VALUE==>REDACTED")
|
||||
|
||||
# Force push all branches (requires team coordination + force push permissions)
|
||||
git push origin --force --all
|
||||
|
||||
# Notify all developers to re-clone
|
||||
```
|
||||
|
||||
### Step 6 — Verify
|
||||
```bash
|
||||
# Confirm secret no longer in history
|
||||
git log --all -p | grep "LEAKED_VALUE" | wc -l # should be 0
|
||||
|
||||
# Test new credentials work
|
||||
curl -H "Authorization: Bearer $NEW_TOKEN" https://api.service.com/test
|
||||
|
||||
# Monitor for unauthorized usage of old credential (check service audit logs)
|
||||
```
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user