-
Notifications
You must be signed in to change notification settings - Fork 339
Description
Epic: Configurable Password and Secret Policy Engine
🎯 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
🏗️ 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
andJWT_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
- Required by: [Feature Request]: Epic: Secure JWT Token Catalog with Per-User Expiry and Revocation #87 - JWT Token Catalog (for user password validation)
- No external dependencies beyond Python standard library
📝 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