Skip to content

[FEATURE][SECURITY]: 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 security

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions