diff --git a/ARCHIVAGE retour GEMINY.txt b/ARCHIVAGE retour GEMINY.txt index b454b87..9ab4e75 100644 --- a/ARCHIVAGE retour GEMINY.txt +++ b/ARCHIVAGE retour GEMINY.txt @@ -6434,3 +6434,234 @@ git push origin main Une fois le package recréé et téléchargé sur Dockge, le conteneur passera au vert ! Dites-moi dès que c'est bon. ça me brise les burnes mais je lâche pas +pfffffff j'ai un vpn obligatoire de supervision. MERCI MESH et BESZEL + +EDIT DE VOTRE QUESTION : + +Désactive le contrôle du VPN pour les VIP/ADMIN. J'utilise un VPN obligatoire pour ma supervision avec MeshCentral et Beszel. + +Je comprends tout à fait la frustration ! C'est le grand classique de la cybersécurité : on verrouille tellement bien la porte d'entrée qu'on finit par s'enfermer dehors soi-même. 😅 L'utilisation d'un VPN pour MeshCentral et Beszel est une excellente pratique, il ne faut surtout pas l'enlever. + +Techniquement, le blocage actuel pose problème car le "Middleware" de sécurité qu'on a mis en place dans main.py intercepte la connexion avant même de savoir qui est l'utilisateur (s'il est VIP, Admin ou simple visiteur). + +Pour corriger cela proprement et autoriser votre VPN sans ouvrir les vannes à tout le monde, j'ai deux options à implémenter : + +Soit autoriser spécifiquement les adresses IP privées de votre VPN (Whitelist). + +Soit lire le cookie de session directement dans le Middleware pour vérifier si le rôle "Admin/VIP" est actif avant d'appliquer le blocage. + +Pour réaliser cette modification avec précision sans rien casser de ce qui fonctionne : + +👉 Veuillez me transmettre les contenus actuels de ces fichiers : + +main.py (Pour modifier la logique du SecurityMiddleware) + +modules/security.py (Pour y ajouter la logique de contournement / Whitelist) + +README.md (Pour que je prépare l'incrémentation indiquant cette exception de sécurité). + +Dès que j'ai ces fichiers, je vous donne le code exact pour créer ce passe-droit ! + +pfffffff j'ai un vpn obligatoire de supervision. MERCI MESH et BESZEL +pfffffff j'ai un vpn obligatoire de supervision. MERCI MESH et BESZEL + +EDIT DE VOTRE QUESTION : + +Désactive le contrôle du VPN pour les utilisateurs VIP et Admin (Xavier, Céline, Anaëlle, Renan, Audric). J'utilise un VPN obligatoire pour ma supervision avec MeshCentral et Beszel, ce qui bloque mes propres accès au serveur. + +C'est le grand classique de la cybersécurité : on verrouille tellement bien la porte d'entrée qu'on finit par s'enfermer dehors ! 😅 L'utilisation d'un VPN pour vos outils de supervision (MeshCentral et Beszel) est une excellente pratique. Il ne faut surtout pas la désactiver. + +Techniquement, le blocage posait problème car notre SecurityMiddleware dans main.py interceptait toutes les requêtes entrantes avant de vérifier l'identité de l'utilisateur. Pour régler cela sans ouvrir les vannes au public, nous allons modifier le système pour qu'il autorise systématiquement les IP locales (celles générées par vos tunnels de supervision) et qu'il lise un passe-droit pour les VIP que nous allons centraliser dans modules/security.py. +PY ++ 1 + +Voici les fichiers corrigés dans leur intégralité pour un copier-coller propre. + +1. Mise à jour de modules/security.py +Nous ajoutons la liste stricte de vos VIP et modifions la fonction de blocage pour autoriser les plages d'IP locales (192.168.x.x, 10.x.x.x, etc.) et les profils privilégiés. + +Python +import json +import os +import time +import requests +from datetime import datetime +from modules.logger import log_event + +LOG_FILE = "aethas_ip_logs.json" + +# --- DÉFINITION CENTRALISÉE DES PROFILS PRIVILÉGIÉS --- +UTILISATEURS_VIP = ["xavier", "céline", "anaëlle", "renan", "audric", "admin"] + +def verifier_et_bloquer_vpn(ip_address, utilisateur="visiteur"): + """ + Vérifie si une IP appartient à un VPN, un Proxy ou un Datacenter. + Intègre une liste blanche (Whitelist) pour les réseaux locaux et les VIP. + """ + # 1. Bypass matériel : On ne bloque jamais les requêtes locales (Serveur, MeshCentral, Beszel, Gluetun) + if ip_address in ["127.0.0.1", "localhost", "::1"] or ip_address.startswith(("192.168.", "10.", "172.")): + return False + + # 2. Bypass profil : Passe-droit pour les VIP et Administrateurs + if utilisateur.lower() in UTILISATEURS_VIP: + log_event("INFO", "SECURITY", f"Passe-droit VPN accordé à l'utilisateur VIP : {utilisateur} ({ip_address})") + return False + + try: + # Requête ultra-légère, timeout court pour ne pas ralentir le site + reponse = requests.get(f"https://blackbox.ipinfo.app/lookup/{ip_address}", timeout=3) + if reponse.text.strip() == "Y": + log_event("WARNING", "SECURITY", f"Tentative de connexion VPN bloquée pour l'IP : {ip_address}") + return True # True = C'est un VPN public/datacenter, il faut bloquer + except requests.RequestException as e: + # En cas de panne de l'API de vérification, on laisse passer pour ne pas bloquer les utilisateurs légitimes + log_event("ERROR", "SECURITY", f"Erreur lors de la vérification VPN pour {ip_address}: {e}") + + return False + +def journaliser_ip(ip_address, action="Connexion"): + """ + Enregistre l'IP pour la conformité légale (RGPD). + Purge automatique des logs vieux de plus de 365 jours. + """ + logs = [] + + # 1. Chargement de l'historique existant + if os.path.exists(LOG_FILE): + with open(LOG_FILE, 'r', encoding='utf-8') as f: + try: + logs = json.load(f) + except json.JSONDecodeError: + logs = [] + + # 2. Nettoyage RGPD : on supprime les logs de plus d'un an (365 jours = 31 536 000 secondes) + un_an_en_arriere = time.time() - 31536000 + logs = [log for log in logs if log.get("timestamp", 0) > un_an_en_arriere] + + # 3. Ajout de la nouvelle entrée + logs.append({ + "ip": ip_address, + "action": action, + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "timestamp": time.time() + }) + + # 4. Sauvegarde + with open(LOG_FILE, 'w', encoding='utf-8') as f: + json.dump(logs, f, indent=4) + + log_event("INFO", "SECURITY", f"IP {ip_address} journalisée pour raison légale ({action}).") +2. Mise à jour de main.py +Nous modifions le SecurityMiddleware pour qu'il extraie l'information du cookie utilisateur et la transmette à la nouvelle fonction de sécurité. Cela prépare parfaitement le terrain pour notre futur module de connexion complet. +PY + +Python +import uuid +from fasthtml.common import * +from starlette.staticfiles import StaticFiles +from starlette.responses import FileResponse +from starlette.middleware.base import BaseHTTPMiddleware + +# Imports ultra-propres depuis les modules dédiés +from modules.theme import get_theme_hdrs +from modules.database import nettoyer_vieilles_discussions, HISTORY_FILE, load_history, save_history +from modules.ui_components import render_main_page, render_sidebar +from modules.chat_handler import handle_chat_request +from modules.security import verifier_et_bloquer_vpn, journaliser_ip + +# --- MIDDLEWARE DE SÉCURITÉ --- +class SecurityMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request, call_next): + # Récupération de l'IP du client (gère le proxy NPM si configuré avec X-Forwarded-For) + ip = request.headers.get("X-Forwarded-For", request.client.host).split(",")[0].strip() + + # Simulation du rôle de l'utilisateur via cookie (en attendant le module de connexion) + utilisateur = request.cookies.get("utilisateur", "visiteur") + + # On exclut les requêtes statiques (comme le logo) pour ne pas spammer les vérifications API + if not request.url.path.startswith("/Logo"): + # 1. Vérification VPN (inclut désormais la Whitelist VIP/Admin) + if verifier_et_bloquer_vpn(ip, utilisateur): + return HTMLResponse("

Accès refusé

Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme pour votre profil.

", status_code=403) + # 2. Journalisation RGPD + journaliser_ip(ip, f"Accès à {request.url.path}") + + return await call_next(request) + +# On exécute le nettoyage au démarrage +nettoyer_vieilles_discussions() + +# Initialisation du serveur FastHTML avec le Middleware de sécurité +app, rt = fast_app( + hdrs=get_theme_hdrs(), + secret_key="super_secret_aethas", + middleware=[Middleware(SecurityMiddleware)] +) + +# Montage du dossier statique pour rendre accessible le logo +app.mount("/Logo", StaticFiles(directory="Logo"), name="Logo") + +@rt('/') +def get(session): + if 'current_chat_id' not in session or not session['current_chat_id']: + session['current_chat_id'] = str(uuid.uuid4()) + return render_main_page(session['current_chat_id']) + +@rt('/chat') +async def post(request, session: dict): + return await handle_chat_request(request, session) + +@rt('/favicon.ico') +def get(): return HTTPResponse(status_code=204) + +@rt('/new_chat') +def get(session): session['current_chat_id'] = str(uuid.uuid4()); return RedirectResponse('/', status_code=303) + +@rt('/load_chat/{cid}') +def get(cid: str, session): + if cid and cid != "null": session['current_chat_id'] = cid + return RedirectResponse('/', status_code=303) + +@rt('/rename/{cid}') +def post(cid: str, req): + h = load_history() + if cid in h and (nom := req.headers.get("HX-Prompt")): h[cid]['title'] = nom; save_history(h) + return render_sidebar() + +@rt('/pin/{cid}') +def post(cid: str): + h = load_history() + if cid in h: h[cid]['pinned'] = not h[cid].get('pinned', False); save_history(h) + return render_sidebar() + +@rt('/delete/{cid}') +def post(cid: str): + h, cid_str = load_history(), str(cid) + if cid_str in h: del h[cid_str]; save_history(h) + return render_sidebar() + +@rt('/export_json') +def get(): return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.json') + +if __name__ == '__main__': + serve(port=5001) +3. Mise à jour incrémentielle du README.md +Voici la ligne exacte à remplacer dans la Phase 4 de votre fichier README.md actuel pour notifier ce changement transparent : +MD + +Markdown +### Phase 4 : Gestion des Utilisateurs & Sécurité Légale +- [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur. +- [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur. +- [ ] Système de connexion sécurisé avec configuration du premier lancement. +- [x] Système de cryptage avancé (Fernet) et de Hashing (Bcrypt) mis en place. +- [x] Sécurité : Protection Bruteforce. **Blocage des accès VPN/Proxy (Passe-droit exclusif configuré pour les profils VIP/Admin et réseaux locaux).** +- [x] Conformité légale (RGPD) : Journalisation stricte des IP par client conservée 1 an. +- [ ] Multilinguisme : Adaptation automatique de la langue du site via IP. +Avant de tester sur le serveur, exécutez ces commandes dans le terminal de votre PC pour figer l'étape : + +Bash +git add . +git commit -m "feat: Whitelist VPN pour les réseaux locaux de supervision et passe-droit VIP" +git push origin main +pfffffff j'ai un vpn obligatoire de supervision. MERCI MESH et BESZEL \ No newline at end of file diff --git a/README.md b/README.md index d962656..88dc2e4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim - [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur. - [ ] Système de connexion sécurisé avec configuration du premier lancement. - [x] Système de cryptage avancé (Fernet) et de Hashing (Bcrypt) mis en place. -- [x] Sécurité : Protection Bruteforce. **Blocage total des accès VPN/Proxy**. +- [x] Sécurité : Protection Bruteforce. **Blocage des accès VPN/Proxy (Passe-droit exclusif configuré pour les profils VIP/Admin et réseaux locaux).** - [x] Conformité légale (RGPD) : Journalisation stricte des IP par client conservée 1 an. - [ ] Multilinguisme : Adaptation automatique de la langue du site via IP. diff --git a/main.py b/main.py index b8d105a..70dfad6 100644 --- a/main.py +++ b/main.py @@ -17,11 +17,14 @@ class SecurityMiddleware(BaseHTTPMiddleware): # Récupération de l'IP du client (gère le proxy NPM si configuré avec X-Forwarded-For) ip = request.headers.get("X-Forwarded-For", request.client.host).split(",")[0].strip() - # On exclut les requêtes internes et statiques pour ne pas spammer les logs - if ip not in ["127.0.0.1", "localhost", "::1"] and not request.url.path.startswith("/Logo"): - # 1. Vérification VPN - if verifier_et_bloquer_vpn(ip): - return HTMLResponse("

Accès refusé

Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme.

", status_code=403) + # Simulation du rôle de l'utilisateur via cookie (en attendant le module de connexion) + utilisateur = request.cookies.get("utilisateur", "visiteur") + + # On exclut les requêtes statiques (comme le logo) pour ne pas spammer les vérifications API + if not request.url.path.startswith("/Logo"): + # 1. Vérification VPN (inclut désormais la Whitelist VIP/Admin) + if verifier_et_bloquer_vpn(ip, utilisateur): + return HTMLResponse("

Accès refusé

Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme pour votre profil.

", status_code=403) # 2. Journalisation RGPD journaliser_ip(ip, f"Accès à {request.url.path}") diff --git a/modules/security.py b/modules/security.py index 8e6db4e..89ca89d 100644 --- a/modules/security.py +++ b/modules/security.py @@ -7,21 +7,29 @@ from modules.logger import log_event LOG_FILE = "aethas_ip_logs.json" -def verifier_et_bloquer_vpn(ip_address): +# --- DÉFINITION CENTRALISÉE DES PROFILS PRIVILÉGIÉS --- +UTILISATEURS_VIP = ["xavier", "céline", "anaëlle", "renan", "audric", "admin"] + +def verifier_et_bloquer_vpn(ip_address, utilisateur="visiteur"): """ Vérifie si une IP appartient à un VPN, un Proxy ou un Datacenter. - Utilise l'API publique gratuite blackbox.ipinfo.app (Renvoie 'Y' si VPN, 'N' si clean). + Intègre une liste blanche (Whitelist) pour les réseaux locaux et les VIP. """ - # On ne bloque jamais les requêtes locales du serveur (localhost) - if ip_address in ["127.0.0.1", "localhost", "::1"]: + # 1. Bypass matériel : On ne bloque jamais les requêtes locales (Serveur, MeshCentral, Beszel, Gluetun) + if ip_address in ["127.0.0.1", "localhost", "::1"] or ip_address.startswith(("192.168.", "10.", "172.")): return False + # 2. Bypass profil : Passe-droit pour les VIP et Administrateurs + if utilisateur.lower() in UTILISATEURS_VIP: + log_event("INFO", "SECURITY", f"Passe-droit VPN accordé à l'utilisateur VIP : {utilisateur} ({ip_address})") + return False + try: # Requête ultra-légère, timeout court pour ne pas ralentir le site reponse = requests.get(f"https://blackbox.ipinfo.app/lookup/{ip_address}", timeout=3) if reponse.text.strip() == "Y": log_event("WARNING", "SECURITY", f"Tentative de connexion VPN bloquée pour l'IP : {ip_address}") - return True # True = C'est un VPN, il faut bloquer + return True # True = C'est un VPN public/datacenter, il faut bloquer except requests.RequestException as e: # En cas de panne de l'API de vérification, on laisse passer pour ne pas bloquer les utilisateurs légitimes log_event("ERROR", "SECURITY", f"Erreur lors de la vérification VPN pour {ip_address}: {e}")