Changements: - Ajouter ensure_dev_config() qui génère des clés temporaires en dev - Ajouter paramètre strict=True/False à validate_production_security() - En développement: génère auto ENCRYPTION_PASSWORD et SECRET_KEY - En production: comportement inchangé (bloque si config invalide) server/api_upload.py: - Utilise strict=is_production_environment() - En dev: warning seulement, continue le démarrage - En prod: sys.exit(1) si config invalide Résout les problèmes de démarrage en développement sans config manuelle. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
347 lines
11 KiB
Python
347 lines
11 KiB
Python
"""
|
|
Security Configuration Management
|
|
|
|
Gère la configuration sécurisée du système, particulièrement en production.
|
|
Exigence 7.1: Forcer la configuration sécurisée en production
|
|
Exigence 7.5: Refus de démarrage avec configuration insécurisée
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import secrets
|
|
import hashlib
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional, Set
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class SecurityConfig:
|
|
"""Configuration de sécurité du système."""
|
|
|
|
# Environnement
|
|
environment: str = "development"
|
|
|
|
# Chiffrement
|
|
encryption_password: Optional[str] = None
|
|
encryption_key_file: Optional[str] = None
|
|
|
|
# Clés secrètes
|
|
secret_key: Optional[str] = None
|
|
jwt_secret: Optional[str] = None
|
|
|
|
# Base de données
|
|
db_encryption_enabled: bool = False
|
|
db_password: Optional[str] = None
|
|
|
|
# API
|
|
api_key_required: bool = False
|
|
api_keys: List[str] = None
|
|
|
|
# Logging
|
|
log_sensitive_data: bool = True # False en production
|
|
|
|
# Validation
|
|
strict_input_validation: bool = False # True en production
|
|
|
|
def __post_init__(self):
|
|
"""Post-initialisation pour valider la configuration."""
|
|
if self.api_keys is None:
|
|
self.api_keys = []
|
|
|
|
|
|
class SecurityConfigError(Exception):
|
|
"""Erreur de configuration de sécurité."""
|
|
pass
|
|
|
|
|
|
class ProductionSecurityError(SecurityConfigError):
|
|
"""Erreur de sécurité critique en production."""
|
|
pass
|
|
|
|
|
|
def is_production_environment() -> bool:
|
|
"""
|
|
Détermine si nous sommes en environnement de production.
|
|
|
|
Returns:
|
|
True si en production
|
|
"""
|
|
env = os.getenv("ENVIRONMENT", "development").lower()
|
|
return env in ["production", "prod"]
|
|
|
|
|
|
def get_security_config() -> SecurityConfig:
|
|
"""
|
|
Charge la configuration de sécurité depuis les variables d'environnement.
|
|
|
|
Returns:
|
|
Configuration de sécurité
|
|
"""
|
|
config = SecurityConfig(
|
|
environment=os.getenv("ENVIRONMENT", "development").lower(),
|
|
encryption_password=os.getenv("ENCRYPTION_PASSWORD"),
|
|
encryption_key_file=os.getenv("ENCRYPTION_KEY_FILE"),
|
|
secret_key=os.getenv("SECRET_KEY"),
|
|
jwt_secret=os.getenv("JWT_SECRET"),
|
|
db_encryption_enabled=os.getenv("DB_ENCRYPTION_ENABLED", "false").lower() == "true",
|
|
db_password=os.getenv("DB_PASSWORD"),
|
|
api_key_required=os.getenv("API_KEY_REQUIRED", "false").lower() == "true",
|
|
api_keys=os.getenv("API_KEYS", "").split(",") if os.getenv("API_KEYS") else [],
|
|
log_sensitive_data=os.getenv("LOG_SENSITIVE_DATA", "true").lower() == "true",
|
|
strict_input_validation=os.getenv("STRICT_INPUT_VALIDATION", "false").lower() == "true"
|
|
)
|
|
|
|
return config
|
|
|
|
|
|
def _is_default_key(key: str, known_defaults: Set[str]) -> bool:
|
|
"""
|
|
Vérifie si une clé est une clé par défaut connue.
|
|
|
|
Args:
|
|
key: Clé à vérifier
|
|
known_defaults: Ensemble des clés par défaut connues
|
|
|
|
Returns:
|
|
True si c'est une clé par défaut
|
|
"""
|
|
if not key:
|
|
return True
|
|
|
|
return key in known_defaults
|
|
|
|
|
|
def _is_weak_key(key: str, min_length: int = 32) -> bool:
|
|
"""
|
|
Vérifie si une clé est faible.
|
|
|
|
Args:
|
|
key: Clé à vérifier
|
|
min_length: Longueur minimale requise
|
|
|
|
Returns:
|
|
True si la clé est faible
|
|
"""
|
|
if not key or len(key) < min_length:
|
|
return True
|
|
|
|
# Si la clé semble être générée automatiquement (base64-like), être moins strict
|
|
if len(key) >= min_length and all(c.isalnum() or c in "+-_=" for c in key):
|
|
# Clé générée automatiquement, probablement sécurisée
|
|
return False
|
|
|
|
# Vérifier la complexité pour les clés manuelles
|
|
has_upper = any(c.isupper() for c in key)
|
|
has_lower = any(c.islower() for c in key)
|
|
has_digit = any(c.isdigit() for c in key)
|
|
has_special = any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in key)
|
|
|
|
complexity_score = sum([has_upper, has_lower, has_digit, has_special])
|
|
|
|
return complexity_score < 2 # Au moins 2 types de caractères (moins strict)
|
|
|
|
|
|
def ensure_dev_config(config: SecurityConfig) -> SecurityConfig:
|
|
"""
|
|
S'assure que la config de développement a des valeurs par défaut sécurisées.
|
|
Génère automatiquement des clés temporaires si non définies.
|
|
|
|
Args:
|
|
config: Configuration à compléter
|
|
|
|
Returns:
|
|
Configuration avec valeurs par défaut
|
|
"""
|
|
if is_production_environment():
|
|
return config
|
|
|
|
# Générer des clés temporaires pour le développement si non définies
|
|
if not config.encryption_password:
|
|
config.encryption_password = f"dev_encryption_{secrets.token_hex(16)}"
|
|
logger.info("Generated temporary ENCRYPTION_PASSWORD for development")
|
|
os.environ["ENCRYPTION_PASSWORD"] = config.encryption_password
|
|
|
|
if not config.secret_key:
|
|
config.secret_key = f"dev_secret_{secrets.token_hex(16)}"
|
|
logger.info("Generated temporary SECRET_KEY for development")
|
|
os.environ["SECRET_KEY"] = config.secret_key
|
|
|
|
return config
|
|
|
|
|
|
def validate_production_security(config: SecurityConfig, strict: bool = True) -> None:
|
|
"""
|
|
Valide la configuration de sécurité pour la production.
|
|
|
|
Args:
|
|
config: Configuration à valider
|
|
strict: Si False, ne lève pas d'exception (warnings seulement)
|
|
|
|
Raises:
|
|
ProductionSecurityError: Si la configuration n'est pas sécurisée (mode strict + production)
|
|
"""
|
|
if not is_production_environment():
|
|
logger.info("Development environment - applying permissive security validation")
|
|
# En dev, on génère des clés temporaires si nécessaire
|
|
ensure_dev_config(config)
|
|
return
|
|
|
|
logger.info("Validating production security configuration...")
|
|
|
|
errors = []
|
|
warnings = []
|
|
|
|
# Clés par défaut connues
|
|
default_encryption_keys = {
|
|
"rpa_vision_v3_default_key",
|
|
"default_password",
|
|
"changeme",
|
|
"password",
|
|
"secret"
|
|
}
|
|
|
|
default_secret_keys = {
|
|
"dev-key-change-in-production",
|
|
"development-secret",
|
|
"default-secret",
|
|
"changeme"
|
|
}
|
|
|
|
# 1. Validation du mot de passe de chiffrement
|
|
if not config.encryption_password:
|
|
errors.append("ENCRYPTION_PASSWORD must be set in production")
|
|
elif _is_default_key(config.encryption_password, default_encryption_keys):
|
|
errors.append("ENCRYPTION_PASSWORD cannot use default value in production")
|
|
elif _is_weak_key(config.encryption_password, 32):
|
|
errors.append("ENCRYPTION_PASSWORD is too weak (minimum 32 characters with complexity required)")
|
|
|
|
# 2. Validation de la clé secrète
|
|
if not config.secret_key:
|
|
errors.append("SECRET_KEY must be set in production")
|
|
elif _is_default_key(config.secret_key, default_secret_keys):
|
|
errors.append("SECRET_KEY cannot use default value in production")
|
|
elif _is_weak_key(config.secret_key, 32):
|
|
errors.append("SECRET_KEY is too weak (minimum 32 characters with complexity required)")
|
|
|
|
# 3. Validation JWT si utilisé
|
|
if config.jwt_secret:
|
|
if _is_default_key(config.jwt_secret, default_secret_keys):
|
|
errors.append("JWT_SECRET cannot use default value in production")
|
|
elif _is_weak_key(config.jwt_secret, 32):
|
|
errors.append("JWT_SECRET is too weak")
|
|
|
|
# 4. Validation base de données
|
|
if config.db_password and _is_weak_key(config.db_password, 16):
|
|
warnings.append("DB_PASSWORD appears to be weak")
|
|
|
|
# 5. Configuration de sécurité
|
|
if config.log_sensitive_data:
|
|
warnings.append("LOG_SENSITIVE_DATA should be false in production")
|
|
|
|
if not config.strict_input_validation:
|
|
warnings.append("STRICT_INPUT_VALIDATION should be true in production")
|
|
|
|
# 6. Validation des clés API
|
|
if config.api_key_required and not config.api_keys:
|
|
errors.append("API_KEY_REQUIRED is true but no API_KEYS provided")
|
|
|
|
for api_key in config.api_keys:
|
|
if _is_weak_key(api_key, 24):
|
|
warnings.append(f"API key appears to be weak: {api_key[:8]}...")
|
|
|
|
# 7. Validation des fichiers de clés
|
|
if config.encryption_key_file:
|
|
key_path = Path(config.encryption_key_file)
|
|
if not key_path.exists():
|
|
errors.append(f"Encryption key file not found: {config.encryption_key_file}")
|
|
elif key_path.stat().st_mode & 0o077:
|
|
warnings.append(f"Encryption key file has overly permissive permissions: {config.encryption_key_file}")
|
|
|
|
# Afficher les warnings
|
|
for warning in warnings:
|
|
logger.warning(f"Security warning: {warning}")
|
|
|
|
# Lever une erreur si des erreurs critiques (mode strict uniquement)
|
|
if errors:
|
|
error_msg = "Production security validation failed:\n" + "\n".join(f" - {error}" for error in errors)
|
|
logger.error(error_msg)
|
|
if strict:
|
|
raise ProductionSecurityError(error_msg)
|
|
else:
|
|
logger.warning("Strict mode disabled - continuing despite security errors")
|
|
return
|
|
|
|
logger.info("Production security validation passed")
|
|
|
|
|
|
def generate_secure_key(length: int = 32) -> str:
|
|
"""
|
|
Génère une clé sécurisée.
|
|
|
|
Args:
|
|
length: Longueur de la clé
|
|
|
|
Returns:
|
|
Clé sécurisée
|
|
"""
|
|
return secrets.token_urlsafe(length)
|
|
|
|
|
|
def hash_sensitive_value(value: str) -> str:
|
|
"""
|
|
Hash une valeur sensible pour le logging sécurisé.
|
|
|
|
Args:
|
|
value: Valeur à hasher
|
|
|
|
Returns:
|
|
Hash de la valeur
|
|
"""
|
|
return hashlib.sha256(value.encode()).hexdigest()[:16]
|
|
|
|
|
|
def validate_and_get_config() -> SecurityConfig:
|
|
"""
|
|
Valide et retourne la configuration de sécurité.
|
|
|
|
Returns:
|
|
Configuration validée
|
|
|
|
Raises:
|
|
ProductionSecurityError: Si la validation échoue en production
|
|
"""
|
|
config = get_security_config()
|
|
validate_production_security(config)
|
|
return config
|
|
|
|
|
|
def check_security_requirements() -> Dict[str, bool]:
|
|
"""
|
|
Vérifie les exigences de sécurité.
|
|
|
|
Returns:
|
|
Dict avec le statut de chaque exigence
|
|
"""
|
|
config = get_security_config()
|
|
is_prod = is_production_environment()
|
|
|
|
requirements = {
|
|
"production_environment": is_prod,
|
|
"encryption_password_set": bool(config.encryption_password),
|
|
"encryption_password_secure": not _is_default_key(
|
|
config.encryption_password or "",
|
|
{"rpa_vision_v3_default_key", "default_password", "changeme"}
|
|
),
|
|
"secret_key_set": bool(config.secret_key),
|
|
"secret_key_secure": not _is_default_key(
|
|
config.secret_key or "",
|
|
{"dev-key-change-in-production", "development-secret"}
|
|
),
|
|
"logging_secure": not config.log_sensitive_data if is_prod else True,
|
|
"input_validation_strict": config.strict_input_validation if is_prod else True
|
|
}
|
|
|
|
return requirements |