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:
136
core/security/__init__.py
Normal file
136
core/security/__init__.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Core Security Module
|
||||
|
||||
Modules pour la sécurité du système :
|
||||
- Configuration sécurisée
|
||||
- Validation des inputs
|
||||
- Chiffrement
|
||||
- Authentification
|
||||
- API Security & Governance (Fiche #23)
|
||||
"""
|
||||
|
||||
from .security_config import (
|
||||
SecurityConfig,
|
||||
validate_production_security,
|
||||
get_security_config,
|
||||
is_production_environment,
|
||||
ensure_dev_config,
|
||||
check_security_requirements,
|
||||
generate_secure_key,
|
||||
ProductionSecurityError,
|
||||
SecurityConfigError
|
||||
)
|
||||
|
||||
# API Security & Governance imports (Fiche #23)
|
||||
from .api_tokens import (
|
||||
TokenManager,
|
||||
TokenRole,
|
||||
TokenValidationError,
|
||||
validate_token,
|
||||
generate_api_token
|
||||
)
|
||||
|
||||
from .ip_allowlist import (
|
||||
IPAllowlist,
|
||||
IPValidationError,
|
||||
is_ip_allowed,
|
||||
get_client_ip
|
||||
)
|
||||
|
||||
from .rate_limiter import (
|
||||
RateLimiter,
|
||||
TokenBucket,
|
||||
RateLimitExceeded,
|
||||
check_rate_limit
|
||||
)
|
||||
|
||||
from .audit_log import (
|
||||
AuditLogger,
|
||||
AuditEvent,
|
||||
log_security_event,
|
||||
log_api_access
|
||||
)
|
||||
|
||||
# FastAPI Security (optional import)
|
||||
try:
|
||||
from .fastapi_security import (
|
||||
FastAPISecurityMiddleware,
|
||||
require_admin_token,
|
||||
require_any_token,
|
||||
get_current_user_role
|
||||
)
|
||||
_FASTAPI_AVAILABLE = True
|
||||
except ImportError:
|
||||
_FASTAPI_AVAILABLE = False
|
||||
FastAPISecurityMiddleware = None
|
||||
require_admin_token = None
|
||||
require_any_token = None
|
||||
get_current_user_role = None
|
||||
|
||||
# Flask Security (optional import)
|
||||
try:
|
||||
from .flask_security import (
|
||||
FlaskSecurityMiddleware,
|
||||
flask_require_admin,
|
||||
flask_require_any_token,
|
||||
init_flask_security
|
||||
)
|
||||
_FLASK_AVAILABLE = True
|
||||
except ImportError:
|
||||
_FLASK_AVAILABLE = False
|
||||
FlaskSecurityMiddleware = None
|
||||
flask_require_admin = None
|
||||
flask_require_any_token = None
|
||||
init_flask_security = None
|
||||
|
||||
# Input validation imports will be added after testing
|
||||
|
||||
__all__ = [
|
||||
# Security Config
|
||||
'SecurityConfig',
|
||||
'validate_production_security',
|
||||
'get_security_config',
|
||||
'is_production_environment',
|
||||
'ensure_dev_config',
|
||||
'check_security_requirements',
|
||||
'generate_secure_key',
|
||||
'ProductionSecurityError',
|
||||
'SecurityConfigError',
|
||||
|
||||
# API Tokens
|
||||
'TokenManager',
|
||||
'TokenRole',
|
||||
'TokenValidationError',
|
||||
'validate_token',
|
||||
'generate_api_token',
|
||||
|
||||
# IP Allowlist
|
||||
'IPAllowlist',
|
||||
'IPValidationError',
|
||||
'is_ip_allowed',
|
||||
'get_client_ip',
|
||||
|
||||
# Rate Limiting
|
||||
'RateLimiter',
|
||||
'TokenBucket',
|
||||
'RateLimitExceeded',
|
||||
'check_rate_limit',
|
||||
|
||||
# Audit Logging
|
||||
'AuditLogger',
|
||||
'AuditEvent',
|
||||
'log_security_event',
|
||||
'log_api_access',
|
||||
|
||||
# FastAPI Security
|
||||
'FastAPISecurityMiddleware',
|
||||
'require_admin_token',
|
||||
'require_any_token',
|
||||
'get_current_user_role',
|
||||
|
||||
# Flask Security (if available)
|
||||
'FlaskSecurityMiddleware',
|
||||
'flask_require_admin',
|
||||
'flask_require_any_token',
|
||||
'init_flask_security'
|
||||
]
|
||||
347
core/security/security_config.py
Normal file
347
core/security/security_config.py
Normal 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
|
||||
Reference in New Issue
Block a user