Files
Sankofa/api/src/lib/secret-validation.ts
defiQUG 8436e22f4c API: Phoenix railing proxy, API key auth for /api/v1/*, schema export, docs, migrations, tests
- Phoenix API Railing: proxy to PHOENIX_RAILING_URL, tenant me routes
- Tenant-auth: X-API-Key support for /api/v1/* (api_keys table)
- Migration 026: api_keys table; 025 sovereign stack marketplace
- GET /graphql/schema, GET /graphql-playground, api/docs OpenAPI
- Integration tests: phoenix-railing.test.ts
- docs/api/API_VERSIONING: /api/v1/ railing alignment
- docs/phoenix/PORTAL_RAILING_WIRING

Made-with: Cursor
2026-03-11 12:57:41 -07:00

325 lines
8.5 KiB
TypeScript

/**
* Secret Validation Framework
*
* Implements FIPS 140-2 Level 2+ secret validation per NIST SP 800-53 SC-12
* and NIST SP 800-171 3.5.10 (Cryptographic Key Management)
*
* This module ensures that:
* - No default or insecure secrets are used in production
* - Secrets meet minimum complexity requirements
* - Secrets are properly validated before use
*/
import { logger } from './logger'
/**
* Default/insecure secrets that must never be used in production
*/
const INSECURE_SECRETS = [
'your-secret-key-change-in-production',
'change-me',
'secret',
'password',
'admin',
'root',
'postgres',
'default',
'test',
'dev',
'development',
'123456',
'password123',
'',
]
/**
* Minimum secret requirements per DoD/MilSpec standards
*/
interface SecretRequirements {
minLength: number
requireUppercase: boolean
requireLowercase: boolean
requireNumbers: boolean
requireSpecialChars: boolean
maxAge?: number // in days
}
const DEFAULT_REQUIREMENTS: SecretRequirements = {
minLength: 32, // NIST SP 800-63B recommends minimum 32 characters for secrets
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
}
/**
* Validates that a secret meets DoD/MilSpec requirements
*/
export class SecretValidationError extends Error {
constructor(
message: string,
public code: string,
public requirements?: SecretRequirements
) {
super(message)
this.name = 'SecretValidationError'
}
}
/**
* Validates a secret against security requirements
*/
export function validateSecret(
secret: string | undefined,
name: string,
requirements: Partial<SecretRequirements> = {}
): void {
const req = { ...DEFAULT_REQUIREMENTS, ...requirements }
// Check if secret is provided
if (!secret) {
throw new SecretValidationError(
`Secret '${name}' is required but not provided`,
'MISSING_SECRET',
req
)
}
// Check for insecure defaults
if (INSECURE_SECRETS.includes(secret.toLowerCase().trim())) {
throw new SecretValidationError(
`Secret '${name}' uses an insecure default value. This is not allowed in production.`,
'INSECURE_DEFAULT',
req
)
}
// Check minimum length
if (secret.length < req.minLength) {
throw new SecretValidationError(
`Secret '${name}' must be at least ${req.minLength} characters long (current: ${secret.length})`,
'INSUFFICIENT_LENGTH',
req
)
}
// Check complexity requirements
if (req.requireUppercase && !/[A-Z]/.test(secret)) {
throw new SecretValidationError(
`Secret '${name}' must contain at least one uppercase letter`,
'MISSING_UPPERCASE',
req
)
}
if (req.requireLowercase && !/[a-z]/.test(secret)) {
throw new SecretValidationError(
`Secret '${name}' must contain at least one lowercase letter`,
'MISSING_LOWERCASE',
req
)
}
if (req.requireNumbers && !/[0-9]/.test(secret)) {
throw new SecretValidationError(
`Secret '${name}' must contain at least one number`,
'MISSING_NUMBER',
req
)
}
if (req.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(secret)) {
throw new SecretValidationError(
`Secret '${name}' must contain at least one special character`,
'MISSING_SPECIAL_CHAR',
req
)
}
// Check for common patterns (optional but recommended)
if (isCommonPattern(secret)) {
logger.warn(`Secret '${name}' matches a common pattern and may be insecure`)
}
}
/**
* Checks if a secret matches common insecure patterns
*/
function isCommonPattern(secret: string): boolean {
const patterns = [
/^[a-z]+$/i, // All same case
/^[0-9]+$/, // All numbers
/^(.)\1+$/, // All same character
/^12345/, // Sequential numbers
/^abcde/i, // Sequential letters
]
return patterns.some(pattern => pattern.test(secret))
}
/**
* Validates a secret and returns it, or throws if invalid
* This is the main function to use for secret validation
*/
export function requireSecret(
secret: string | undefined,
name: string,
requirements?: Partial<SecretRequirements>
): string {
validateSecret(secret, name, requirements)
return secret!
}
/**
* Validates a secret in production environment
* Fails fast if secret is insecure in production
*/
export function requireProductionSecret(
secret: string | undefined,
name: string,
requirements?: Partial<SecretRequirements>
): string {
const isProduction = process.env.NODE_ENV === 'production' ||
process.env.ENVIRONMENT === 'production' ||
process.env.PRODUCTION === 'true'
if (isProduction) {
// Stricter requirements for production
const prodRequirements: SecretRequirements = {
...DEFAULT_REQUIREMENTS,
minLength: 64, // Longer secrets for production
...requirements,
}
validateSecret(secret, name, prodRequirements)
} else {
validateSecret(secret, name, requirements)
}
return secret!
}
/**
* Validates JWT secret specifically
*/
export function requireJWTSecret(): string {
return requireProductionSecret(
process.env.JWT_SECRET,
'JWT_SECRET',
{
minLength: 64, // JWT secrets should be longer
}
)
}
/**
* Validates database password specifically
* Relaxed requirements for development mode
*/
export function requireDatabasePassword(): string {
const isProduction = process.env.NODE_ENV === 'production' ||
process.env.ENVIRONMENT === 'production' ||
process.env.PRODUCTION === 'true'
if (isProduction) {
return requireProductionSecret(
process.env.DB_PASSWORD,
'DB_PASSWORD',
{
minLength: 32,
}
)
} else {
// Development mode: relaxed requirements
// Still validate but allow shorter passwords for local development
const password = process.env.DB_PASSWORD
if (!password) {
throw new SecretValidationError(
'DB_PASSWORD is required but not provided. Please set it in your .env file.',
'MISSING_SECRET',
{ minLength: 8, requireUppercase: false, requireLowercase: false, requireNumbers: false, requireSpecialChars: false }
)
}
// Basic validation for dev (just check it's not empty and not insecure)
if (password.length < 8) {
throw new SecretValidationError(
'DB_PASSWORD must be at least 8 characters long for development',
'INSUFFICIENT_LENGTH',
{ minLength: 8 }
)
}
if (INSECURE_SECRETS.includes(password.toLowerCase().trim())) {
throw new SecretValidationError(
'DB_PASSWORD uses an insecure default value',
'INSECURE_DEFAULT'
)
}
return password
}
}
/**
* Validates all required secrets at application startup
* Call this during application initialization
*/
export function validateAllSecrets(): void {
const isProduction = process.env.NODE_ENV === 'production' ||
process.env.ENVIRONMENT === 'production' ||
process.env.PRODUCTION === 'true'
if (!isProduction) {
logger.warn('Not in production environment - secret validation may be relaxed')
return
}
logger.info('Validating all required secrets for production...')
const requiredSecrets = [
{ env: 'JWT_SECRET', name: 'JWT_SECRET', minLength: 64 },
{ env: 'DB_PASSWORD', name: 'DB_PASSWORD', minLength: 32 },
{ env: 'KEYCLOAK_CLIENT_SECRET', name: 'KEYCLOAK_CLIENT_SECRET', minLength: 32 },
]
const missing: string[] = []
const invalid: Array<{ name: string; error: string }> = []
for (const secret of requiredSecrets) {
const value = process.env[secret.env]
if (!value) {
missing.push(secret.name)
continue
}
try {
requireProductionSecret(value, secret.name, { minLength: secret.minLength })
} catch (error) {
if (error instanceof SecretValidationError) {
invalid.push({ name: secret.name, error: error.message })
} else {
invalid.push({ name: secret.name, error: String(error) })
}
}
}
if (missing.length > 0) {
throw new Error(
`Missing required secrets in production: ${missing.join(', ')}\n` +
'Please set all required environment variables before starting the application.'
)
}
if (invalid.length > 0) {
const errors = invalid.map(i => ` - ${i.name}: ${i.error}`).join('\n')
throw new Error(
`Invalid secrets in production:\n${errors}\n` +
'Please ensure all secrets meet security requirements.'
)
}
logger.info('All required secrets validated successfully')
}