179 lines
5.6 KiB
Python
179 lines
5.6 KiB
Python
"""
|
|
InnotexBoard - Interface d'administration Debian
|
|
Backend FastAPI
|
|
"""
|
|
|
|
from fastapi import FastAPI, Request, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
import uvicorn
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from app.core.config import settings
|
|
from app.api.routes import api_router
|
|
from app.api.websocket import router as ws_router
|
|
|
|
# Configuration du répertoire de logs
|
|
log_dir = Path(os.getenv('LOG_DIR', '/var/log/innotexboard'))
|
|
if not log_dir.exists():
|
|
try:
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
except PermissionError:
|
|
# Fallback sur un répertoire local si pas de permissions
|
|
log_dir = Path('./logs')
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
log_file = log_dir / 'security.log'
|
|
|
|
# Configuration du logging de sécurité
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler(str(log_file)),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger("innotexboard.security")
|
|
|
|
# Réduire le bruit des logs watchfiles en développement
|
|
logging.getLogger("watchfiles.main").setLevel(logging.WARNING)
|
|
logging.getLogger("watchfiles").setLevel(logging.WARNING)
|
|
|
|
# Rate limiting pour prévenir les attaques brute force et DDoS
|
|
limiter = Limiter(key_func=get_remote_address, default_limits=["200/minute"])
|
|
|
|
# Initialiser l'application FastAPI
|
|
app = FastAPI(
|
|
title=settings.API_TITLE,
|
|
description=settings.API_DESCRIPTION,
|
|
version=settings.API_VERSION,
|
|
docs_url="/docs" if settings.DEBUG else None, # Désactiver docs en production
|
|
redoc_url="/redoc" if settings.DEBUG else None,
|
|
openapi_url="/openapi.json" if settings.DEBUG else None,
|
|
)
|
|
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
# Middleware de sécurité CORS - Configuration stricte
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.ALLOWED_ORIGINS, # Liste blanche uniquement
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"], # Méthodes explicites
|
|
allow_headers=["Authorization", "Content-Type", "X-Requested-With"],
|
|
expose_headers=["X-RateLimit-Limit", "X-RateLimit-Remaining"],
|
|
max_age=3600, # Cache preflight 1h
|
|
)
|
|
|
|
# Middleware pour les hôtes de confiance
|
|
app.add_middleware(
|
|
TrustedHostMiddleware,
|
|
allowed_hosts=settings.ALLOWED_HOSTS,
|
|
)
|
|
|
|
|
|
@app.middleware("http")
|
|
async def security_headers_middleware(request: Request, call_next):
|
|
"""Ajoute les headers de sécurité HTTP à toutes les réponses"""
|
|
response = await call_next(request)
|
|
|
|
# Protection contre le clickjacking
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
|
|
# Force HTTPS et HSTS (HTTP Strict Transport Security)
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
|
|
# Prévention du MIME-sniffing
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
|
|
# Protection XSS intégrée au navigateur
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
|
|
# Content Security Policy - Politique stricte
|
|
response.headers["Content-Security-Policy"] = (
|
|
"default-src 'self'; "
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
|
"style-src 'self' 'unsafe-inline'; "
|
|
"img-src 'self' data: https:; "
|
|
"font-src 'self' data:; "
|
|
"connect-src 'self'; "
|
|
"frame-ancestors 'none'; "
|
|
"base-uri 'self'; "
|
|
"form-action 'self';"
|
|
)
|
|
|
|
# Contrôle des permissions
|
|
response.headers["Permissions-Policy"] = (
|
|
"geolocation=(), microphone=(), camera=(), payment=()"
|
|
)
|
|
|
|
# Désactiver la divulgation de la version du serveur
|
|
response.headers["Server"] = "InnotexBoard"
|
|
|
|
return response
|
|
|
|
|
|
@app.middleware("http")
|
|
async def log_security_events(request: Request, call_next):
|
|
"""Log les événements de sécurité suspects"""
|
|
client_ip = get_remote_address(request)
|
|
|
|
# Logger les tentatives d'accès aux endpoints sensibles
|
|
if "/api/v1/auth" in request.url.path:
|
|
logger.info(f"Auth attempt from {client_ip} to {request.url.path}")
|
|
|
|
response = await call_next(request)
|
|
|
|
# Logger les erreurs d'authentification
|
|
if response.status_code == 401:
|
|
logger.warning(f"Failed authentication from {client_ip} to {request.url.path}")
|
|
|
|
# Logger les tentatives d'accès non autorisé
|
|
if response.status_code == 403:
|
|
logger.warning(f"Forbidden access attempt from {client_ip} to {request.url.path}")
|
|
|
|
return response
|
|
|
|
|
|
# Inclure les routes API
|
|
app.include_router(api_router, prefix="/api/v1")
|
|
app.include_router(ws_router, prefix="/api/v1")
|
|
|
|
|
|
@app.get("/")
|
|
@limiter.limit("10/minute")
|
|
async def root(request: Request):
|
|
"""Endpoint racine"""
|
|
return {
|
|
"message": "Bienvenue sur InnotexBoard",
|
|
"version": settings.API_VERSION,
|
|
"docs": "/docs" if settings.DEBUG else "Documentation disabled in production",
|
|
"openapi": "/openapi.json" if settings.DEBUG else "OpenAPI disabled in production"
|
|
}
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Vérification de la santé de l'application"""
|
|
return {
|
|
"status": "healthy",
|
|
"service": "InnotexBoard API"
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Configuration pour le développement
|
|
uvicorn.run(
|
|
"main:app",
|
|
host="0.0.0.0",
|
|
port=8000,
|
|
reload=True,
|
|
log_level="info",
|
|
)
|