Public Access
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
Build and Push Docker Image / build-and-push (push) Successful in 33s
This commit is contained in:
1 parent
34163a630c
commit
5a4e7dd094
4 files changed
+253
-11
No files matched your search
@@ -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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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}")
|
||||||
|
|||||||
Reference in new issue
Block a user