Loading content...
In 2020, a Fortune 500 company discovered that their centralized logging system contained three years of production database credentials in plaintext. The credentials appeared in connection error messages, debug logs, and startup banners. Thousands of engineers with log access had inadvertently had database admin access.
This wasn't a hack. It wasn't malware. It was ordinary logging doing what logging is designed to do: capture application state for debugging and monitoring. The problem was that application state included secrets.
Logs are essential for operations, debugging, and security monitoring. But logs are also stored, searched, aggregated, and often retained for years. They're frequently accessible to large teams, third-party monitoring services, and automated systems. When secrets appear in logs, they're effectively published to a wide audience with persistent access.
Preventing secrets from entering logs is the last line of defense—the safety net that catches secrets that slip through other protections.
By the end of this page, you will understand: why logs are high-risk for secrets exposure, the common patterns that leak secrets to logs, how to design secret-safe types and data structures, framework-level protection strategies, structured logging approaches for sensitive data, log sanitization and filtering techniques, and audit and monitoring for secrets in logs.
Logs amplify secrets exposure in ways that code or configuration do not. Understanding these risk factors is essential for appreciating why log protection deserves special attention.
The Unique Risk Profile of Logs:
password=, apiKey:, or connectionString.| Factor | Secrets in Code | Secrets in Logs |
|---|---|---|
| Access Control | Repository permissions | Log platform permissions (often broader) |
| Retention | Git history (permanent) | Retention policy (months to years) |
| Discoverability | Requires code access | Searchable by pattern |
| Third-Party Risk | Code review tools | Log aggregation services |
| Accidental Discovery | During code review | During incident investigation |
| Detection Difficulty | Pre-commit hooks can catch | Post-facto detection only |
Logs are designed for visibility—that's their purpose. Every feature that makes logs useful (aggregation, search, retention, access) also makes them dangerous for secrets. You can't fix this with access controls alone; you must prevent secrets from entering logs in the first place.
Secrets rarely end up in logs through deliberate action. They slip in through common patterns that seem harmless but create significant exposure. Recognizing these patterns is the first step in prevention.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
// ============================================// LEAK PATTERN 1: Request/Response Logging// ============================================ app.use((req, res, next) => { // ❌ DANGEROUS: Logs entire request including Authorization header console.log('Incoming request:', { method: req.method, url: req.url, headers: req.headers, // Contains Authorization: Bearer <token> body: req.body, // May contain passwords, API keys }); next();}); // ============================================// LEAK PATTERN 2: Error Messages with Context// ============================================ async function connectToDatabase(connectionString: string): Promise<void> { try { await client.connect(connectionString); } catch (error) { // ❌ DANGEROUS: Connection string (with password) in error log console.error(`Failed to connect to ${connectionString}: ${error}`); throw error; }} // ============================================// LEAK PATTERN 3: Object Serialization// ============================================ interface AppConfig { port: number; database: { host: string; password: string; // Secret mixed with config }; jwtSecret: string; // Secret mixed with config} function startApplication(config: AppConfig): void { // ❌ DANGEROUS: Logs entire config including secrets console.log('Starting application with config:', config); console.log('Config:', JSON.stringify(config, null, 2));} // ============================================// LEAK PATTERN 4: Debug Logging in Production// ============================================ class PaymentService { async processPayment(request: PaymentRequest): Promise<void> { // ❌ DANGEROUS: Debug log left in production code console.debug('Processing payment:', { amount: request.amount, cardNumber: request.cardNumber, // PCI violation! cvv: request.cvv, // PCI violation! apiKey: this.stripeApiKey, // API key exposure }); // Process payment... }} // ============================================// LEAK PATTERN 5: Exception/Error Object Logging// ============================================ class DatabaseError extends Error { constructor( message: string, public connectionString: string, // Stored in error object public query: string ) { super(message); }} try { await executeQuery(query, connectionString);} catch (error) { // ❌ DANGEROUS: Error object contains connectionString console.error('Query failed:', error); // Full error object with connection credentials is logged} // ============================================// LEAK PATTERN 6: String Interpolation// ============================================ const apiKey = process.env.API_KEY; // ❌ DANGEROUS: API key in log messageconsole.log(`Initialized with API key: ${apiKey}`);console.log(`Using API key ${apiKey} to connect to service`); // Even partial exposure is dangerousconsole.log(`API key starts with: ${apiKey?.substring(0, 10)}`); // ============================================// LEAK PATTERN 7: Audit Logging with Too Much Detail// ============================================ function auditUserAction(userId: string, action: string, details: object): void { // ❌ DANGEROUS: Details may contain changed secrets console.log(`AUDIT: User ${userId} performed ${action}`, details); // If action was "update password", details contains the new password!} auditUserAction('user123', 'update password', { previousPassword: 'oldpass', // Logged! newPassword: 'newpass' // Logged!}); // ============================================// LEAK PATTERN 8: Environment Dump// ============================================ // ❌ CATASTROPHIC: Dumps all environment variablesconsole.log('Environment:', process.env); // ❌ DANGEROUS: Conditional dump in error handlerprocess.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); console.error('Environment state:', process.env); // Don't do this! process.exit(1);});In Python, logging locals() or vars() during exception handling is especially dangerous because local variables often contain secrets being processed. This pattern appears in many debug examples but is catastrophic in production.
The most effective protection against log leakage is designing types that cannot be accidentally logged. By encapsulating secrets in types that override serialization methods, you create defense-in-depth at the language level.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
/** * Secret-Safe Type Design * * Types that cannot be accidentally logged or serialized. */ // ============================================// SECURE SECRET TYPE// ============================================ class SecureSecret { private readonly value: string; constructor(value: string) { this.value = value; // Freeze the object to prevent modification Object.freeze(this); } /** * Get the actual secret value. * The method name is intentionally scary to make accidental use obvious. */ dangerouslyExposeValue(): string { return this.value; } /** * Use the secret in a callback without exposing it. * This pattern ensures the secret never escapes the controlled context. */ use<T>(callback: (value: string) => T): T { return callback(this.value); } /** * Compare secrets without exposing values. * Uses timing-safe comparison to prevent timing attacks. */ equals(other: SecureSecret): boolean { const a = Buffer.from(this.value); const b = Buffer.from(other.value); if (a.length !== b.length) { return false; } // Timing-safe comparison return require('crypto').timingSafeEqual(a, b); } // ============================================ // SERIALIZATION PROTECTION // These methods are ALWAYS called when logging/serializing // ============================================ toString(): string { return '[REDACTED]'; } toJSON(): string { return '[REDACTED]'; } valueOf(): string { return '[REDACTED]'; } // NodeJS custom inspect for console.log [Symbol.for('nodejs.util.inspect.custom')](): string { return '[SecureSecret: REDACTED]'; } // For debugging without exposing value get length(): number { return this.value.length; } get prefix(): string { // Show only first 4 characters for debugging return this.value.substring(0, 4) + '...'; }} // Usage examplesconst apiKey = new SecureSecret('sk_live_abcd1234efgh5678'); // These are all safe - will output [REDACTED]console.log(apiKey); // [SecureSecret: REDACTED]console.log(`Key: ${apiKey}`); // Key: [REDACTED]console.log(JSON.stringify({ key: apiKey })); // {"key":"[REDACTED]"}console.error('Error with key:', apiKey); // Error with key: [SecureSecret: REDACTED] // Intentional access is explicit and auditablefetch(url, { headers: { 'Authorization': `Bearer ${apiKey.dangerouslyExposeValue()}` }}); // Callback pattern for controlled useawait apiKey.use(async (key) => { return stripe.charges.create({ api_key: key, amount: 1000 });}); // ============================================// SECURE CONFIGURATION OBJECT// ============================================ interface ConfigWithSecretsRaw { port: number; logLevel: string; databaseUrl: string; // Secret jwtSecret: string; // Secret apiKey: string; // Secret} class SecureConfig { // Public configuration (safe to log) readonly port: number; readonly logLevel: string; // Private secrets (never logged) private readonly _databaseUrl: SecureSecret; private readonly _jwtSecret: SecureSecret; private readonly _apiKey: SecureSecret; constructor(raw: ConfigWithSecretsRaw) { this.port = raw.port; this.logLevel = raw.logLevel; this._databaseUrl = new SecureSecret(raw.databaseUrl); this._jwtSecret = new SecureSecret(raw.jwtSecret); this._apiKey = new SecureSecret(raw.apiKey); Object.freeze(this); } // Explicit accessors for secrets get databaseUrl(): SecureSecret { return this._databaseUrl; } get jwtSecret(): SecureSecret { return this._jwtSecret; } get apiKey(): SecureSecret { return this._apiKey; } // Safe serialization shows only public config toJSON(): object { return { port: this.port, logLevel: this.logLevel, databaseUrl: '[REDACTED]', jwtSecret: '[REDACTED]', apiKey: '[REDACTED]', }; } toString(): string { return JSON.stringify(this.toJSON()); } [Symbol.for('nodejs.util.inspect.custom')](): object { return this.toJSON(); }} // Usageconst config = new SecureConfig({ port: 3000, logLevel: 'info', databaseUrl: 'postgresql://admin:secretpass@db.example.com/prod', jwtSecret: 'super-secret-jwt-key', apiKey: 'sk_live_abc123',}); // Safe to log - secrets are redactedconsole.log('Starting with config:', config);// Output: Starting with config: {"port":3000,"logLevel":"info","databaseUrl":"[REDACTED]",...} // Use secrets explicitlyconst pool = new Pool({ connectionString: config.databaseUrl.dangerouslyExposeValue()});Secret-safe types are defense in depth, not the only defense. They catch accidental exposure but can be bypassed with dangerouslyExposeValue(). Combine with log filtering, code review, and scanning for comprehensive protection.
While secret-safe types protect individual values, framework-level protections apply consistently across the entire application. These include logging middleware, request sanitization, and error handling customization.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
/** * Framework-Level Log Protection */ import { Request, Response, NextFunction } from 'express'; // ============================================// REQUEST LOGGING MIDDLEWARE WITH SANITIZATION// ============================================ interface SanitizedRequest { method: string; url: string; params: Record<string, string>; query: Record<string, string>; headers: Record<string, string>; body: unknown;} const SENSITIVE_HEADERS = new Set([ 'authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-access-token',]); const SENSITIVE_BODY_FIELDS = new Set([ 'password', 'secret', 'token', 'apikey', 'api_key', 'credential', 'credit_card', 'creditcard', 'card_number', 'cvv', 'ssn',]); function sanitizeHeaders(headers: Record<string, string>): Record<string, string> { const sanitized: Record<string, string> = {}; for (const [key, value] of Object.entries(headers)) { if (SENSITIVE_HEADERS.has(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = value; } } return sanitized;} function sanitizeObject(obj: unknown, depth: number = 0): unknown { if (depth > 10) return '[MAX_DEPTH]'; if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { // Check if string looks like a secret if (obj.length > 20 && /^(sk_|pk_|api_|key_|token_)/i.test(obj)) { return '[POSSIBLE_SECRET]'; } return obj; } if (Array.isArray(obj)) { return obj.map(item => sanitizeObject(item, depth + 1)); } if (typeof obj === 'object') { const sanitized: Record<string, unknown> = {}; for (const [key, value] of Object.entries(obj as Record<string, unknown>)) { if (SENSITIVE_BODY_FIELDS.has(key.toLowerCase())) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = sanitizeObject(value, depth + 1); } } return sanitized; } return obj;} function sanitizeRequest(req: Request): SanitizedRequest { return { method: req.method, url: req.url, params: req.params, query: req.query as Record<string, string>, headers: sanitizeHeaders(req.headers as Record<string, string>), body: sanitizeObject(req.body), };} // Express middlewareexport function requestLoggingMiddleware( req: Request, res: Response, next: NextFunction): void { const sanitized = sanitizeRequest(req); console.log('Incoming request:', JSON.stringify(sanitized)); next();} // ============================================// ERROR HANDLING WITH SANITIZATION// ============================================ interface SafeError { message: string; code?: string; stack?: string;} function sanitizeError(error: Error): SafeError { let message = error.message; // Remove common secret patterns from error messages const secretPatterns = [ /postgresql:\/\/[^@]+@/gi, // DB connection strings /mongodb:\/\/[^@]+@/gi, // MongoDB connection strings /redis:\/\/:[^@]+@/gi, // Redis connection strings /Bearer\s+[A-Za-z0-9\-_.]+/gi, // Bearer tokens /api[_-]?key[=:\s]+\S+/gi, // API keys /password[=:\s]+\S+/gi, // Passwords /secret[=:\s]+\S+/gi, // Secrets ]; for (const pattern of secretPatterns) { message = message.replace(pattern, '[REDACTED]'); } return { message, code: (error as any).code, // Only include stack in development stack: process.env.NODE_ENV === 'development' ? error.stack : undefined, };} // Express error handlerexport function errorLoggingMiddleware( error: Error, req: Request, res: Response, next: NextFunction): void { const safeError = sanitizeError(error); const safeRequest = sanitizeRequest(req); console.error('Request error:', { error: safeError, request: { method: safeRequest.method, url: safeRequest.url, }, }); // Send sanitized error to client res.status(500).json({ error: { message: process.env.NODE_ENV === 'production' ? 'Internal server error' : safeError.message, }, });} // ============================================// CUSTOM LOGGER WITH BUILT-IN SANITIZATION// ============================================ type LogLevel = 'debug' | 'info' | 'warn' | 'error'; class SafeLogger { private readonly sensitivePatterns: RegExp[] = [ /postgresql:\/\/[^\s]+/gi, /mongodb:\/\/[^\s]+/gi, /Bearer\s+[A-Za-z0-9\-_.]+/gi, /"password"\s*:\s*"[^"]+"/gi, /"secret"\s*:\s*"[^"]+"/gi, /"api[_-]?key"\s*:\s*"[^"]+"/gi, ]; private sanitize(message: string): string { let sanitized = message; for (const pattern of this.sensitivePatterns) { sanitized = sanitized.replace(pattern, '[REDACTED]'); } return sanitized; } private sanitizeArgs(...args: unknown[]): unknown[] { return args.map(arg => { if (typeof arg === 'string') { return this.sanitize(arg); } if (typeof arg === 'object' && arg !== null) { return sanitizeObject(arg); } return arg; }); } log(level: LogLevel, message: string, ...args: unknown[]): void { const sanitizedMessage = this.sanitize(message); const sanitizedArgs = this.sanitizeArgs(...args); const logFn = console[level] || console.log; logFn(`[${level.toUpperCase()}] ${sanitizedMessage}`, ...sanitizedArgs); } debug(message: string, ...args: unknown[]): void { this.log('debug', message, ...args); } info(message: string, ...args: unknown[]): void { this.log('info', message, ...args); } warn(message: string, ...args: unknown[]): void { this.log('warn', message, ...args); } error(message: string, ...args: unknown[]): void { this.log('error', message, ...args); }} // Usageconst logger = new SafeLogger(); // These are automatically sanitizedlogger.info('Connecting to postgresql://user:pass@db.com/mydb');// Output: [INFO] Connecting to [REDACTED] logger.error('Auth failed', { password: 'secret123' });// Output: [ERROR] Auth failed { password: '[REDACTED]' }Structured logging provides a systematic approach to handling sensitive data. By using explicit field marking and consistent formatting, you can implement centralized redaction and auditing policies.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
/** * Structured Logging with Sensitive Data Handling */ // ============================================// TYPED LOG FIELDS WITH SENSITIVITY MARKERS// ============================================ enum FieldSensitivity { PUBLIC = 'public', // Safe to log and aggregate INTERNAL = 'internal', // Log to internal systems only SENSITIVE = 'sensitive', // Redact in logs, keep in secure audit RESTRICTED = 'restricted', // Never log, even in audit} interface LogField<T = unknown> { value: T; sensitivity: FieldSensitivity;} // Field factory functions for clarityfunction publicField<T>(value: T): LogField<T> { return { value, sensitivity: FieldSensitivity.PUBLIC };} function internalField<T>(value: T): LogField<T> { return { value, sensitivity: FieldSensitivity.INTERNAL };} function sensitiveField<T>(value: T): LogField<T> { return { value, sensitivity: FieldSensitivity.SENSITIVE };} function restrictedField<T>(value: T): LogField<T> { return { value, sensitivity: FieldSensitivity.RESTRICTED };} // ============================================// STRUCTURED LOG ENTRY// ============================================ interface StructuredLogEntry { timestamp: string; level: string; message: string; service: string; traceId?: string; spanId?: string; fields: Record<string, LogField>;} class StructuredLogger { constructor( private service: string, private outputSensitivity: FieldSensitivity = FieldSensitivity.INTERNAL ) {} log( level: string, message: string, fields: Record<string, LogField> ): void { const entry: StructuredLogEntry = { timestamp: new Date().toISOString(), level, message, service: this.service, traceId: this.getCurrentTraceId(), spanId: this.getCurrentSpanId(), fields, }; const outputEntry = this.prepareForOutput(entry); console.log(JSON.stringify(outputEntry)); } private prepareForOutput( entry: StructuredLogEntry ): Record<string, unknown> { const output: Record<string, unknown> = { timestamp: entry.timestamp, level: entry.level, message: entry.message, service: entry.service, traceId: entry.traceId, spanId: entry.spanId, }; // Process fields based on sensitivity for (const [key, field] of Object.entries(entry.fields)) { output[key] = this.processField(field); } return output; } private processField(field: LogField): unknown { // Determine what to output based on field sensitivity vs output sensitivity const sensitivityOrder = [ FieldSensitivity.PUBLIC, FieldSensitivity.INTERNAL, FieldSensitivity.SENSITIVE, FieldSensitivity.RESTRICTED, ]; const fieldLevel = sensitivityOrder.indexOf(field.sensitivity); const outputLevel = sensitivityOrder.indexOf(this.outputSensitivity); if (fieldLevel > outputLevel) { // Field is more sensitive than output allows if (field.sensitivity === FieldSensitivity.RESTRICTED) { return undefined; // Don't even indicate existence } return '[REDACTED]'; } return field.value; } private getCurrentTraceId(): string | undefined { // Integration with tracing (OpenTelemetry, etc.) return undefined; } private getCurrentSpanId(): string | undefined { return undefined; } // Convenience methods info(message: string, fields: Record<string, LogField> = {}): void { this.log('INFO', message, fields); } error(message: string, fields: Record<string, LogField> = {}): void { this.log('ERROR', message, fields); }} // ============================================// USAGE EXAMPLE// ============================================ const logger = new StructuredLogger('api-service', FieldSensitivity.INTERNAL); // Log a user login eventlogger.info('User login successful', { userId: publicField('user-12345'), email: internalField('user@example.com'), ipAddress: internalField('192.168.1.1'), password: restrictedField('actualPassword'), // Never logged sessionToken: sensitiveField('sess_abc123'), // Redacted in output}); // Output (when outputSensitivity is INTERNAL):// {// "timestamp": "2024-01-15T10:30:00Z",// "level": "INFO",// "message": "User login successful",// "service": "api-service",// "userId": "user-12345",// "email": "user@example.com",// "ipAddress": "192.168.1.1",// "sessionToken": "[REDACTED]"// // password is not present at all// } // ============================================// AUDIT LOGGER (HIGHER SENSITIVITY OUTPUT)// ============================================ class SecureAuditLogger extends StructuredLogger { constructor(service: string) { // Audit logs can include SENSITIVE fields super(service, FieldSensitivity.SENSITIVE); } log(level: string, message: string, fields: Record<string, LogField>): void { // Audit logs go to a separate, secure destination const entry = { timestamp: new Date().toISOString(), level, message, fields: this.processFieldsForAudit(fields), // Additional audit metadata auditId: crypto.randomUUID(), }; // Write to secure audit system instead of regular logs this.writeToSecureAuditSystem(entry); } private processFieldsForAudit( fields: Record<string, LogField> ): Record<string, unknown> { const processed: Record<string, unknown> = {}; for (const [key, field] of Object.entries(fields)) { // Audit logs include SENSITIVE but never RESTRICTED if (field.sensitivity === FieldSensitivity.RESTRICTED) { // Just record that the field existed processed[`${key}_present`] = true; } else { processed[key] = field.value; } } return processed; } private writeToSecureAuditSystem(entry: unknown): void { // Implementation: write to encrypted audit store console.log('[AUDIT]', JSON.stringify(entry)); }}In production, route logs to different destinations based on sensitivity. PUBLIC and INTERNAL fields go to general log aggregation. SENSITIVE fields go only to encrypted audit logs. RESTRICTED fields are never logged but might trigger alerts on attempted access.
Even with prevention measures, secrets occasionally slip into logs. Post-facto detection provides a safety net, enabling rapid response before exposure causes harm.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
/** * Log Secrets Scanner * * Detects potential secrets in log entries. */ interface SecretPattern { name: string; pattern: RegExp; severity: 'critical' | 'high' | 'medium' | 'low';} interface ScanResult { found: boolean; matches: Array<{ pattern: string; severity: string; location: number; sample: string; // Redacted sample for investigation }>;} class LogSecretScanner { private patterns: SecretPattern[] = [ // API Keys { name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/g, severity: 'critical', }, { name: 'AWS Secret Key', pattern: /[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/g, severity: 'critical', }, { name: 'Stripe Secret Key', pattern: /sk_live_[a-zA-Z0-9]{24,}/g, severity: 'critical', }, { name: 'GitHub Token', pattern: /ghp_[a-zA-Z0-9]{36}/g, severity: 'critical', }, { name: 'Generic API Key', pattern: /api[_-]?key[=:\s]+['"]?[a-zA-Z0-9]{20,}['"]?/gi, severity: 'high', }, // Credentials { name: 'Database Connection String', pattern: /(postgresql|mysql|mongodb):\/\/[^:]+:[^@]+@[^\s]+/gi, severity: 'critical', }, { name: 'Password in URL', pattern: /:\/\/[^:]+:([^@]{4,})@/g, severity: 'critical', }, { name: 'Password Field', pattern: /"password"\s*:\s*"[^"]{4,}"/gi, severity: 'high', }, // Tokens { name: 'JWT Token', pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, severity: 'high', }, { name: 'Bearer Token', pattern: /Bearer\s+[a-zA-Z0-9\-_.]+/gi, severity: 'high', }, // Crypto { name: 'Private Key', pattern: /-----BEGIN (RSA|EC|DSA|OPENSSH) PRIVATE KEY-----/g, severity: 'critical', }, ]; scan(logContent: string): ScanResult { const matches: ScanResult['matches'] = []; for (const pattern of this.patterns) { let match; const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags); while ((match = regex.exec(logContent)) !== null) { matches.push({ pattern: pattern.name, severity: pattern.severity, location: match.index, sample: this.redactSample(match[0]), }); } } // Also check entropy for potential unknown secrets const highEntropyMatches = this.findHighEntropyStrings(logContent); matches.push(...highEntropyMatches); return { found: matches.length > 0, matches, }; } private redactSample(match: string): string { // Show only first and last 4 characters if (match.length <= 12) { return '[REDACTED]'; } return match.substring(0, 4) + '...' + match.substring(match.length - 4); } private findHighEntropyStrings(content: string): ScanResult['matches'] { const matches: ScanResult['matches'] = []; // Find strings that look like random secrets (high entropy) const potentialSecrets = content.match(/[a-zA-Z0-9+/=_-]{32,}/g) || []; for (const str of potentialSecrets) { const entropy = this.calculateEntropy(str); if (entropy > 4.5) { // High entropy threshold matches.push({ pattern: 'High Entropy String', severity: 'medium', location: content.indexOf(str), sample: this.redactSample(str), }); } } return matches; } private calculateEntropy(str: string): number { const freq: Record<string, number> = {}; for (const char of str) { freq[char] = (freq[char] || 0) + 1; } let entropy = 0; const len = str.length; for (const count of Object.values(freq)) { const p = count / len; entropy -= p * Math.log2(p); } return entropy; }} // ============================================// REAL-TIME LOG MONITORING// ============================================ interface Alert { timestamp: Date; severity: string; pattern: string; source: string;} class LogMonitor { private scanner = new LogSecretScanner(); private alertCallbacks: ((alert: Alert) => void)[] = []; onAlert(callback: (alert: Alert) => void): void { this.alertCallbacks.push(callback); } processLogEntry(source: string, logEntry: string): void { const result = this.scanner.scan(logEntry); if (result.found) { for (const match of result.matches) { const alert: Alert = { timestamp: new Date(), severity: match.severity, pattern: match.pattern, source, }; for (const callback of this.alertCallbacks) { callback(alert); } } } }} // Usage: Real-time monitoringconst monitor = new LogMonitor(); monitor.onAlert((alert) => { console.error(`[SECURITY ALERT] Potential secret in logs!`, { severity: alert.severity, pattern: alert.pattern, source: alert.source, timestamp: alert.timestamp.toISOString(), }); // In production: send to security team, rotate the secret if (alert.severity === 'critical') { // notifySecurityTeam(alert); // triggerSecretRotation(alert.pattern); }}); // Process log streammonitor.processLogEntry('api-server', 'Connection string: postgresql://user:pass123@db.com/prod');// ALERT: Potential secret detected!When a secret is detected in logs, response must be immediate: 1) Alert security team, 2) Begin secret rotation, 3) Assess exposure (who has log access), 4) Determine root cause, 5) Fix to prevent recurrence. The secret should be considered compromised from the moment it appeared in logs.
Log protection is the last line of defense in secrets management. Let's consolidate the key takeaways:
Module Complete:
You've now completed the comprehensive Secrets Management module. You understand what constitutes sensitive data, how to distinguish configuration from secrets, techniques for injecting secrets at runtime, and how to prevent secrets from appearing in logs. Together, these form a complete defense-in-depth strategy for protecting the most sensitive data in your systems.
You now have a comprehensive framework for secrets management across the entire software lifecycle. From classification through storage, injection, and log protection, you can implement defense-in-depth strategies that keep sensitive data safe. Apply these patterns consistently to build systems that protect secrets as a fundamental design concern, not an afterthought.