Scan git history for leaked secrets with truffleHog, prevent commits with detect-secrets pre-commit hooks, and test that secrets are never exposed in API responses.
Secrets in source code are a common, catastrophic security failure. The 2022 Samsung breach exposed 190 GB of source code including API keys and credentials. Once a secret is committed to git, it exists in history forever — even if deleted from the current file.
This lesson covers the complete secrets management lifecycle: prevention (pre-commit hooks), detection (history scanning), and testing (verifying secrets don't leak in responses).
truffleHog scans every commit using entropy analysis and regex patterns to find secrets that were once committed and later deleted.
1# Install truffleHog
2pip install trufflehog
3
4# Scan repository history (only verified secrets)
5trufflehog git file://. --only-verified --json > truffle.json 2>/dev/null
6
7# Count findings
8FOUND=$(wc -l < truffle.json | tr -d ' ')
9if [ "$FOUND" -gt 0 ]; then
10 echo "CRITICAL: $FOUND verified secrets found in git history!"
11 cat truffle.json # review and rotate immediately
12 exit 1
13fi
14
15# Scan a remote GitHub repo
16trufflehog github --repo=https://github.com/org/repo --only-verified --json
17
18# If a secret is found:
19# 1. Rotate the secret IMMEDIATELY — assume it was accessed
20# 2. Clean git history with BFG Repo Cleaner:
21# bfg --replace-text replacements.txt
22# replacements.txt: sk_live_abc123==>REMOVED_SECRET
23# 3. Force push (coordinate with team)
24# 4. Investigate access logs for the compromised secretThe --only-verified flag skips suspected secrets that fail live validation — dramatically reducing false positives.
detect-secrets scans staged files before commit and blocks the commit if secrets are found.
1# Install
2pip install detect-secrets pre-commit
3
4# Create baseline of known-safe occurrences
5detect-secrets scan > .secrets.baseline
6# Review .secrets.baseline — contains hashes of detected 'secrets'
7# Secrets in the baseline are whitelisted (test secrets, examples, etc.)
8
9# Install hooks
10pre-commit install1# .pre-commit-config.yaml
2repos:
3 - repo: https://github.com/Yelp/detect-secrets
4 rev: v1.4.0
5 hooks:
6 - id: detect-secrets
7 args: ['--baseline', '.secrets.baseline']
8 - repo: https://github.com/trufflesecurity/trufflehog
9 rev: v3.63.7
10 hooks:
11 - id: trufflehog
12 args: ['--only-verified', '--fail']
13 - repo: https://github.com/returntocorp/semgrep
14 rev: v1.52.0
15 hooks:
16 - id: semgrep
17 args: ['--config=p/secrets', '--error']Even if secrets are not committed to code, they can leak through API responses or error messages.
1// apps/api/src/__tests__/secret-exposure.spec.ts
2const SENSITIVE_PATTERNS = [
3 /sk_live_[a-zA-Z0-9]+/, // Stripe live key
4 /ghp_[a-zA-Z0-9]+/, // GitHub personal token
5 /AKIA[A-Z0-9]{16}/, // AWS access key
6 /password["']?\s*:\s*["'][^"']+["']/i,
7 /secret["']?\s*:\s*["'][^"']+["']/i,
8];
9
10describe('Secret Exposure Prevention', () => {
11 it('error responses do not expose secrets or stack traces', async () => {
12 const res = await request(app.getHttpServer())
13 .post('/auth/login')
14 .send({ email: null, password: null });
15
16 const body = JSON.stringify(res.body);
17 for (const pattern of SENSITIVE_PATTERNS) {
18 expect(body).not.toMatch(pattern);
19 }
20 expect(body).not.toContain('at Object.');
21 expect(body).not.toContain('at processTicksAndRejections');
22 });
23
24 it('user profile does not include password hash', async () => {
25 const res = await request(app.getHttpServer())
26 .get('/user/profile')
27 .set('Authorization', 'Bearer ' + userToken);
28
29 expect(res.body.passwordHash).toBeUndefined();
30 expect(res.body.password).toBeUndefined();
31 expect(res.body.twoFactorSecret).toBeUndefined();
32 expect(res.body.salt).toBeUndefined();
33 });
34
35 it('404 responses do not reveal filesystem paths', async () => {
36 const res = await request(app.getHttpServer()).get('/nonexistent-route');
37 const body = JSON.stringify(res.body);
38 expect(body).not.toMatch(/\/home\/|C:\\|src\/modules\//i);
39 });
40});Test that missing secrets cause a clean startup failure, not a silent runtime error.
1// apps/api/src/config/env.validation.ts
2import { plainToInstance } from 'class-transformer';
3import { IsString, MinLength, validateSync } from 'class-validator';
4
5class EnvironmentVariables {
6 @IsString() @MinLength(32)
7 JWT_SECRET!: string;
8
9 @IsString()
10 DATABASE_URL!: string;
11
12 @IsString() @MinLength(16)
13 TOTP_ENCRYPTION_KEY!: string;
14}
15
16export function validateEnv(config: Record<string, unknown>) {
17 const validated = plainToInstance(EnvironmentVariables, config);
18 const errors = validateSync(validated);
19 if (errors.length > 0) {
20 throw new Error('Invalid environment variables: ' + errors.toString());
21 }
22 return validated;
23}
24
25// Test:
26describe('Environment Variable Validation', () => {
27 it('throws on missing JWT_SECRET', () => {
28 expect(() => validateEnv({ DATABASE_URL: 'postgres://...' })).toThrow(/JWT_SECRET/);
29 });
30
31 it('throws on JWT_SECRET shorter than 32 chars', () => {
32 expect(() => validateEnv({ JWT_SECRET: 'short', DATABASE_URL: 'postgres://' }))
33 .toThrow(/MinLength/);
34 });
35
36 it('accepts valid configuration', () => {
37 expect(() => validateEnv({
38 JWT_SECRET: 'a'.repeat(64),
39 DATABASE_URL: 'postgresql://user:pass@localhost:5432/db',
40 TOTP_ENCRYPTION_KEY: 'b'.repeat(32),
41 })).not.toThrow();
42 });
43});Test that rotating a JWT secret works without downtime.
1describe('JWT Secret Rotation', () => {
2 it('new tokens with new secret work immediately after rotation', async () => {
3 // After rotation, new logins use the new secret
4 const loginRes = await request(app.getHttpServer())
5 .post('/auth/login')
6 .send({ email: '[email protected]', password: 'Pass123!' });
7 const newToken = loginRes.body.accessToken;
8 const profileRes = await request(app.getHttpServer())
9 .get('/user/profile')
10 .set('Authorization', 'Bearer ' + newToken);
11 expect(profileRes.status).toBe(200);
12 });
13
14 it('session token is invalidated on logout', async () => {
15 const loginRes = await request(app.getHttpServer())
16 .post('/auth/login')
17 .send({ email: '[email protected]', password: 'Pass123!' });
18 const token = loginRes.body.accessToken;
19
20 await request(app.getHttpServer())
21 .post('/auth/logout')
22 .set('Authorization', 'Bearer ' + token);
23
24 const profileRes = await request(app.getHttpServer())
25 .get('/user/profile')
26 .set('Authorization', 'Bearer ' + token);
27 expect(profileRes.status).toBe(401);
28 });
29});Complete secrets management: git history scan, pre-commit hooks, and API response tests verifying no secret exposure.
1#!/usr/bin/env bash
2# scripts/secrets-scan.sh
3set -euo pipefail
4REPORT_DIR='reports/security'
5FAILED=0
6mkdir -p "$REPORT_DIR"
7
8echo '=== Secrets Detection Scan ==='
9
10# Step 1: truffleHog — scan git history
11echo 'Step 1: Scanning git history (truffleHog)...'
12if command -v trufflehog &>/dev/null; then
13 trufflehog git file://. --only-verified --json 2>/dev/null > "$REPORT_DIR/truffle.json" || true
14 FOUND=$(wc -l < "$REPORT_DIR/truffle.json" | tr -d ' ')
15 if [ "$FOUND" -gt 0 ]; then
16 echo " CRITICAL: $FOUND verified secrets found in git history!"
17 echo ' Rotate these credentials immediately before pushing.'
18 FAILED=1
19 else
20 echo ' PASS: No verified secrets in git history'
21 fi
22else
23 echo ' SKIP: trufflehog not installed (pip install trufflehog)'
24fi
25
26# Step 2: detect-secrets — scan current files
27echo ''
28echo 'Step 2: Scanning current files (detect-secrets)...'
29if command -v detect-secrets &>/dev/null; then
30 NEW=$(detect-secrets scan --baseline .secrets.baseline 2>/dev/null | \
31 node -e "
32 let d=''; process.stdin.on('data',c=>d+=c);
33 process.stdin.on('end',()=>{
34 try {
35 const p = JSON.parse(d);
36 const n = Object.values(p.results||{}).flat().filter(f=>!f.is_verified).length;
37 process.stdout.write(String(n));
38 } catch { process.stdout.write('0'); }
39 });
40 " 2>/dev/null || echo '0')
41 if [ "$NEW" -gt 0 ]; then
42 echo " FAIL: $NEW new potential secrets detected"
43 FAILED=1
44 else
45 echo ' PASS: No new secrets (baseline matches)'
46 fi
47else
48 echo ' SKIP: detect-secrets not installed'
49fi
50
51# Step 3: Semgrep secret patterns
52echo ''
53echo 'Step 3: Semgrep secret pattern scan...'
54if command -v semgrep &>/dev/null; then
55 COUNT=$(semgrep --config=p/secrets --json --quiet apps/ packages/ 2>/dev/null | \
56 node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{
57 try{process.stdout.write(String((JSON.parse(d).results||[]).length))}catch{process.stdout.write('0')}
58 })" 2>/dev/null || echo '0')
59 if [ "$COUNT" -gt 0 ]; then
60 echo " FAIL: $COUNT secret patterns in source code"
61 FAILED=1
62 else
63 echo ' PASS: No secret patterns found'
64 fi
65else
66 echo ' SKIP: semgrep not installed'
67fi
68
69echo ''
70if [ "$FAILED" -eq 0 ]; then
71 echo 'PASS: All secrets checks passed'
72 exit 0
73else
74 echo 'FAIL: Secrets detected — rotate any exposed credentials immediately'
75 exit 1
76fiThis three-step pipeline runs truffleHog (verified secrets in git history), detect-secrets (staged file comparison against baseline), and Semgrep p/secrets (source code pattern matching). truffleHog --only-verified dramatically reduces false positives by only reporting secrets that successfully authenticate against external services. The detect-secrets baseline file tracks known-safe detections so they don't trigger false alarms. Any secret found should trigger immediate credential rotation.
The next lesson covers SSRF and insecure design testing — testing for server-side request forgery to internal services, information disclosure in error messages, and email enumeration vulnerabilities.
Your progress is saved in this browser, on this device only.