feat: Whitelist VPN pour les réseaux locaux de supervision et passe-droit VIP
Build and Push Docker Image / build-and-push (push) Successful in 33s

This commit is contained in:
xavier committed 2026-08-19 00:44:24 +02:00
1 parent 34163a630c
commit 5a4e7dd094
4 files changed
+253 -11

No files matched your search

+231
View File
@@ -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. 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 ç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("<h1 style='color:red; text-align:center;'>Accès refusé</h1><p style='text-align:center;'>Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme pour votre profil.</p>", 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
+1 -1
View File
@@ -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. - [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur.
- [ ] Système de connexion sécurisé avec configuration du premier lancement. - [ ] 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] 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. - [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. - [ ] Multilinguisme : Adaptation automatique de la langue du site via IP.
+8 -5
View File
@@ -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) # 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() 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 # Simulation du rôle de l'utilisateur via cookie (en attendant le module de connexion)
if ip not in ["127.0.0.1", "localhost", "::1"] and not request.url.path.startswith("/Logo"): utilisateur = request.cookies.get("utilisateur", "visiteur")
# 1. Vérification VPN
if verifier_et_bloquer_vpn(ip): # On exclut les requêtes statiques (comme le logo) pour ne pas spammer les vérifications API
return HTMLResponse("<h1 style='color:red; text-align:center;'>Accès refusé</h1><p style='text-align:center;'>Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme.</p>", status_code=403) 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("<h1 style='color:red; text-align:center;'>Accès refusé</h1><p style='text-align:center;'>Les connexions via VPN, Proxy ou Datacenter sont strictement interdites sur cette plateforme pour votre profil.</p>", status_code=403)
# 2. Journalisation RGPD # 2. Journalisation RGPD
journaliser_ip(ip, f"Accès à {request.url.path}") journaliser_ip(ip, f"Accès à {request.url.path}")
+13 -5
View File
@@ -7,13 +7,21 @@ from modules.logger import log_event
LOG_FILE = "aethas_ip_logs.json" 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. 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) # 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"]: 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 return False
try: try:
@@ -21,7 +29,7 @@ def verifier_et_bloquer_vpn(ip_address):
reponse = requests.get(f"https://blackbox.ipinfo.app/lookup/{ip_address}", timeout=3) reponse = requests.get(f"https://blackbox.ipinfo.app/lookup/{ip_address}", timeout=3)
if reponse.text.strip() == "Y": if reponse.text.strip() == "Y":
log_event("WARNING", "SECURITY", f"Tentative de connexion VPN bloquée pour l'IP : {ip_address}") 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: 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 # 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}") log_event("ERROR", "SECURITY", f"Erreur lors de la vérification VPN pour {ip_address}: {e}")