Skip to content

[SECURITY FEATURE]: Configurable Password and Secret Policy Engine #426

@crivetimihai

Description

@crivetimihai

Epic: Configurable Password and Secret Policy Engine

Depends on: #319 #313

🎯 Overview

Summary

Implement a configurable password policy engine that validates passwords and secrets across the system, including user passwords, BASIC_AUTH_PASSWORD, and JWT_SECRET_KEY.

Problem Statement

Currently, there's no systematic validation of password strength or secret complexity:

  • BASIC_AUTH_PASSWORD can be weak (e.g., "changeme")
  • JWT_SECRET_KEY can be insecure (e.g., "my-test-key")
  • No configurable policies for different security requirements
  • No feedback on password/secret strength during configuration

Solution

Create a reusable password policy engine that:

  • Validates passwords against configurable policies
  • Checks secrets meet cryptographic requirements
  • Provides clear feedback on policy violations
  • Can be applied to any password/secret in the system

👥 User Stories

Story 1: Core Policy Engine

As a developer
I want a reusable password policy validator
So that I can consistently validate passwords across the application

Acceptance Criteria:

Scenario: Validate password against policy
  Given a password policy requiring 12 chars, mixed case, numbers
  When I validate "Password123!"
  Then validation passes
  
  When I validate "password"
  Then validation fails with specific reasons:
    - "Password must be at least 12 characters"
    - "Password must contain uppercase letters"
    - "Password must contain numbers"

Scenario: Check common passwords
  Given the policy checks against common passwords
  When I validate "password123"
  Then validation fails with "Password is too common"

Story 2: Settings Validation at Startup

As a system administrator
I want validation of critical settings at startup
So that I'm warned about insecure configurations

Acceptance Criteria:

Scenario: Validate BASIC_AUTH_PASSWORD
  Given BASIC_AUTH_PASSWORD is set to "admin"
  When the application starts
  Then I see a warning:
    "⚠️ BASIC_AUTH_PASSWORD is weak: too short, too common"
  And the app continues but logs the security risk

Scenario: Validate JWT_SECRET_KEY  
  Given JWT_SECRET_KEY is set to "test"
  When the application starts
  Then I see an error:
    "❌ JWT_SECRET_KEY is critically insecure: must be at least 32 characters"
  And the app refuses to start in production mode

Story 3: Configurable Policy Levels

As a deployment engineer
I want different policy levels for different environments
So that I can balance security with development convenience

Acceptance Criteria:

Scenario: Development mode - relaxed policy
  Given PASSWORD_POLICY_LEVEL="development"
  Then passwords require only 8 characters
  And warnings are shown but not enforced

Scenario: Production mode - strict policy  
  Given PASSWORD_POLICY_LEVEL="production"
  Then passwords require 12+ chars with complexity
  And weak secrets prevent application startup

Scenario: Custom policy
  Given custom policy settings in environment
  When PASSWORD_MIN_LENGTH=16
  And PASSWORD_REQUIRE_SYMBOLS=true
  Then these override the default policy

📊 Architecture

flowchart TB
    subgraph "Policy Engine"
        PE[PasswordPolicyEngine] --> PL[Policy Loader]
        PL --> DP[Default Policies]
        PL --> CP[Custom Policies]
        PE --> VAL[Validators]
        VAL --> LEN[Length Check]
        VAL --> COMP[Complexity Check]
        VAL --> COMMON[Common Password Check]
        VAL --> ENT[Entropy Calculator]
    end
    
    subgraph "Application Integration"
        START[App Startup] --> SV[Settings Validator]
        SV --> PE
        SV -->|Check| BASIC[BASIC_AUTH_PASSWORD]
        SV -->|Check| JWT[JWT_SECRET_KEY]
        SV -->|Warnings/Errors| LOG[Console/Logs]
        
        USER[User Creation] --> PE
        API[API Token] --> PE
    end
    
    subgraph "Policy Levels"
        ENV[Environment] --> DEV[Development]
        ENV --> PROD[Production]
        ENV --> CUSTOM[Custom]
    end
    
    style PE fill:#90EE90
    style SV fill:#FFB6C1
    style LOG fill:#DDA0DD
Loading

🏗️ Technical Design

Policy Configuration

# settings.py
class PasswordPolicySettings(BaseSettings):
    # Policy Level
    password_policy_level: str = Field(
        default="production",
        description="Policy level: development, production, or custom"
    )
    
    # Length Requirements
    password_min_length: int = Field(default=12, ge=1)
    password_min_length_dev: int = Field(default=8, ge=1)
    secret_min_length: int = Field(default=32, ge=16)
    
    # Complexity Requirements
    password_require_uppercase: bool = Field(default=True)
    password_require_lowercase: bool = Field(default=True)
    password_require_numbers: bool = Field(default=True)
    password_require_symbols: bool = Field(default=True)
    
    # Security Settings
    password_check_common: bool = Field(default=True)
    password_min_entropy_bits: float = Field(default=50.0)
    
    # Enforcement
    password_enforce_on_startup: bool = Field(default=True)
    password_block_startup_on_weak_secrets: bool = Field(default=True)

Core Implementation

# password_policy.py
from enum import Enum
from typing import List, Optional, Dict
import math
import re

class PolicyLevel(Enum):
    DEVELOPMENT = "development"
    PRODUCTION = "production"
    CUSTOM = "custom"

class PasswordPolicyEngine:
    def __init__(self, settings: PasswordPolicySettings):
        self.settings = settings
        self.load_common_passwords()
        
    def validate_password(self, password: str, context: str = "password") -> PolicyResult:
        """Validate a password against the configured policy."""
        errors = []
        warnings = []
        
        # Get policy based on level
        policy = self.get_active_policy()
        
        # Length check
        if len(password) < policy.min_length:
            errors.append(f"{context} must be at least {policy.min_length} characters")
            
        # Complexity checks
        if policy.require_uppercase and not re.search(r'[A-Z]', password):
            errors.append(f"{context} must contain uppercase letters")
            
        if policy.require_lowercase and not re.search(r'[a-z]', password):
            errors.append(f"{context} must contain lowercase letters")
            
        if policy.require_numbers and not re.search(r'\d', password):
            errors.append(f"{context} must contain numbers")
            
        if policy.require_symbols and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            errors.append(f"{context} must contain special characters")
            
        # Common password check
        if policy.check_common and self.is_common_password(password):
            errors.append(f"{context} is too common")
            
        # Entropy check
        entropy = self.calculate_entropy(password)
        if entropy < policy.min_entropy_bits:
            warnings.append(f"{context} has low entropy ({entropy:.1f} bits, recommended: {policy.min_entropy_bits})")
            
        return PolicyResult(
            valid=len(errors) == 0,
            errors=errors,
            warnings=warnings,
            entropy=entropy,
            score=self.calculate_strength_score(password)
        )
    
    def validate_secret(self, secret: str, context: str = "Secret") -> PolicyResult:
        """Validate a cryptographic secret."""
        errors = []
        warnings = []
        
        # Secrets have different requirements
        if len(secret) < self.settings.secret_min_length:
            errors.append(f"{context} must be at least {self.settings.secret_min_length} characters")
            
        # Check for obvious patterns
        if secret.lower() in ['test', 'secret', 'key', 'password', 'changeme']:
            errors.append(f"{context} uses a default or weak value")
            
        # Entropy is critical for secrets
        entropy = self.calculate_entropy(secret)
        min_secret_entropy = 128  # 128 bits for cryptographic secrets
        if entropy < min_secret_entropy:
            errors.append(f"{context} has insufficient entropy ({entropy:.1f} bits, required: {min_secret_entropy})")
            
        return PolicyResult(
            valid=len(errors) == 0,
            errors=errors,
            warnings=warnings,
            entropy=entropy,
            score=self.calculate_strength_score(secret)
        )
    
    def calculate_entropy(self, password: str) -> float:
        """Calculate Shannon entropy of password."""
        if not password:
            return 0.0
            
        # Character space size
        char_space = 0
        if re.search(r'[a-z]', password):
            char_space += 26
        if re.search(r'[A-Z]', password):
            char_space += 26
        if re.search(r'\d', password):
            char_space += 10
        if re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            char_space += 32
            
        if char_space == 0:
            return 0.0
            
        return len(password) * math.log2(char_space)

Startup Validation

# startup_validator.py
class StartupSecurityValidator:
    def __init__(self, settings: Settings, policy_engine: PasswordPolicyEngine):
        self.settings = settings
        self.policy = policy_engine
        
    def validate_settings(self) -> bool:
        """Validate security-critical settings at startup."""
        all_valid = True
        
        # Validate BASIC_AUTH_PASSWORD
        result = self.policy.validate_password(
            self.settings.basic_auth_password,
            "BASIC_AUTH_PASSWORD"
        )
        
        if not result.valid:
            logger.error(f"🔴 BASIC_AUTH_PASSWORD validation failed:")
            for error in result.errors:
                logger.error(f"   - {error}")
            all_valid = False
        elif result.warnings:
            logger.warning(f"⚠️  BASIC_AUTH_PASSWORD has warnings:")
            for warning in result.warnings:
                logger.warning(f"   - {warning}")
                
        # Validate JWT_SECRET_KEY
        result = self.policy.validate_secret(
            self.settings.jwt_secret_key,
            "JWT_SECRET_KEY"
        )
        
        if not result.valid:
            logger.critical(f"❌ JWT_SECRET_KEY validation failed:")
            for error in result.errors:
                logger.critical(f"   - {error}")
            
            if self.settings.password_policy_level == PolicyLevel.PRODUCTION:
                logger.critical("Cannot start with insecure JWT_SECRET_KEY in production!")
                return False
                
        # Show entropy information
        logger.info(f"📊 Security metrics:")
        logger.info(f"   - BASIC_AUTH_PASSWORD entropy: {result.entropy:.1f} bits")
        logger.info(f"   - JWT_SECRET_KEY entropy: {result.entropy:.1f} bits")
        
        return all_valid or self.settings.password_policy_level == PolicyLevel.DEVELOPMENT

Common Passwords Dataset

# data/common_passwords.py
COMMON_PASSWORDS_TOP_1000 = [
    "password", "123456", "password123", "admin", "letmein",
    "welcome", "monkey", "dragon", "123456789", "qwerty",
    # ... loaded from file
]

def load_common_passwords() -> Set[str]:
    """Load common passwords from dataset."""
    # Could load from:
    # - Embedded list for top 1000
    # - File for larger lists
    # - Download from SecLists on first run
    return set(COMMON_PASSWORDS_TOP_1000)

🛠️ Implementation Tasks

Phase 1: Core Engine

  • Create password_policy.py with PolicyEngine class
  • Implement policy configuration in settings
  • Add length and complexity validators
  • Add entropy calculator
  • Create common passwords dataset

Phase 2: Startup Integration

  • Create startup_validator.py
  • Integrate with application startup
  • Add logging with clear formatting
  • Implement development vs production modes
  • Add environment variable support

Phase 3: Testing

  • Unit tests for policy engine
  • Tests for each validator
  • Integration tests for startup
  • Performance tests for common password checking

Phase 4: Documentation

  • Document policy configuration options
  • Create security best practices guide
  • Add examples for different environments
  • Migration guide for existing deployments

📋 Acceptance Criteria

  • Policy engine validates passwords with configurable rules
  • Startup validation checks BASIC_AUTH_PASSWORD and JWT_SECRET_KEY
  • Development mode shows warnings but allows weak passwords
  • Production mode blocks startup with weak secrets
  • Clear error messages explain policy violations
  • Entropy calculation provides security metrics
  • Common password detection works efficiently
  • All settings configurable via environment variables

📊 Success Metrics

  • < 10ms overhead for password validation
  • Clear actionable feedback for policy violations

🔗 Dependencies

📝 Notes

  • Start with embedded common passwords list
  • Consider integration with haveibeenpwned API later
  • Default to strict policies with opt-out for development
  • Policy engine designed for reuse across the application

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestsecurityImproves securitytriageIssues / Features awaiting triage

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions