fix(security): Rendre validation permissive en développement

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>
This commit is contained in:
Dom
2026-01-19 17:55:06 +01:00
parent 35f0cb3461
commit 5a77b1a41e
3 changed files with 969 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
"""
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