- JavaScript 72.9%
- Go 20.1%
- HTML 5.1%
- CSS 1.2%
- Dockerfile 0.4%
- Other 0.3%
| .dev | ||
| .forgejo/workflows | ||
| Docs | ||
| nginx-agent | ||
| nginx-analyzer | ||
| nginx-analyzer-go | ||
| nginx-challenge | ||
| nginx-dashboard | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| ngx-dash-current-version | ||
| README.md | ||
| sample.env | ||
Nginx Reverse Proxy Stack
Stack Docker complète pour un reverse proxy Nginx avec dashboard de monitoring intégré, authentification, API REST, déploiement Git, sauvegardes, générateur de vhost, et intégrations CrowdSec + GoAccess.
Fonctionnalités
Nginx
- Image officielle Nginx (trixie) avec modules compilés
- Génération dynamique de
nginx.confvia variables d'environnement - Modules : GeoIP2, headers-more, subs-filter, VTS, stream TCP/UDP
Dashboard
- Interface web sombre, temps réel, bilingue EN/FR
- Authentification login/mot de passe avec système de rôles
- API REST complète avec Bearer token
- Webhooks HTTP sur événements
- Zéro port supplémentaire à exposer (proxy interne)
Intégrations optionnelles
- GeoIP2 — filtrage géographique MaxMind (profil
geo) - Certbot — SSL wildcard via DNS Cloudflare (profil
ssl) - CrowdSec — dashboard Prometheus (décisions locales vs CAPI, alertes, parser)
- Blocklists IP — agrégation périodique de listes de blocage IP externes (une ou plusieurs sources) et/ou générées depuis les règles d'Analyse (mode « à la CrowdSec » configurable par règle : seuil, fenêtre en minutes, remédiation et durée de blocage), liste blanche IP/CIDR globale, opt-out de remédiation par vhost, blocage par vhost via snippet
- GoAccess — analyse des logs par vhost, rapport HTML temps réel intégré
- Auto-config Docker — publication automatique d'un conteneur comme vhost à partir de ses labels, façon Traefik (étape 1 : HTTP, approbation manuelle par défaut)
Modules Nginx inclus
| Module | Usage |
|---|---|
ngx_http_geoip2_module |
Filtrage par pays (MaxMind GeoLite2) |
ngx_http_headers_more_filter_module |
Suppression/modification des headers HTTP |
ngx_http_subs_filter_module |
Substitution de contenu à la volée |
ngx_http_vhost_traffic_status_module (VTS) |
Métriques temps réel par virtual host |
stream + stream_ssl_module |
Proxy TCP/UDP |
Structure des répertoires
.
├── conf/ → /etc/nginx/conf.d/*.conf
├── sites/ → /etc/nginx/sites/*.conf
├── snippets/ → /etc/nginx/snippets/*.conf
├── streams/ → /etc/nginx/streams/*.conf
├── ssl/ → certificats manuels (.cer, .key)
├── certs/ → Let's Encrypt / Certbot
├── geoip_data/ → bases MaxMind
├── logs/ → logs nginx (access, error, vhosts, par vhost)
├── backups/ → sauvegardes ZIP locales
├── goaccess/
│ ├── conf/ → goaccess-combined.conf, goaccess-vhost.conf (générés)
│ ├── db/ → persistance GoAccess par vhost
│ └── reports/ → rapports HTML générés
├── modules/ → modules .so nginx externes
├── cache/ → /var/cache/nginx/
├── cloudflare/ → credentials DNS (certbot)
├── nginx-dashboard/
│ ├── Dockerfile
│ ├── server.js
│ ├── public/index.html
│ ├── public/assets/style.css → CSS extrait de index.html (servi via GET /assets/*)
│ ├── public/assets/lang/en.json → dictionnaire de traduction EN (idem)
│ ├── public/assets/lang/fr.json → dictionnaire de traduction FR (idem)
│ ├── public/assets/js/config-editor.js → JS de la page Configuration, extrait de index.html
│ ├── public/assets/js/notify-scheduler.js → JS de la page Notifications (+ onglet Notifications du Scheduler)
│ ├── public/assets/js/scheduler.js → JS de la page Scheduler (liste, formulaire visuel, import/export)
│ ├── public/assets/js/notify-form.js → JS des formulaires SMTP / alertes (page Notifications)
│ ├── public/assets/js/crowdsec.js → JS de la page CrowdSec, extrait de index.html (v12.41.0)
│ ├── public/assets/js/vhost-generator.js → JS du Générateur de VHost (idem, v12.41.0)
│ ├── public/assets/js/api-doc.js → JS de la page API REST (idem, v12.42.0)
│ ├── public/assets/js/alerting.js → JS du Message important (alertes distantes) (idem, v12.45.0)
│ ├── public/assets/js/graph.js → JS de la page Schéma (idem, v12.47.0)
│ ├── public/assets/js/monitoring.js → JS de la page Monitoring (idem, v12.47.0)
│ ├── public/assets/js/diagnostic.js → JS du panneau Diagnostic (idem, v12.47.0)
│ ├── public/assets/js/ssl.js → JS de la page Certificats SSL (idem, v12.47.0)
│ ├── public/assets/js/control.js → JS de la page Contrôle (idem, v12.48.0)
│ ├── public/assets/js/events.js → JS du journal des événements (idem, v12.48.0)
│ ├── public/assets/js/webhooks.js → JS de la page Webhooks (idem, v12.48.0)
│ ├── public/assets/js/log-viewer.js → JS du visualiseur de logs Nginx (idem, v12.48.0)
│ ├── public/assets/js/auth.js → JS auth/session + visibilité de menu (idem, v12.48.0)
│ ├── public/assets/js/admin.js → JS de la page Administration (idem, v12.48.0)
│ ├── public/assets/js/backup.js → JS de la page Sauvegardes (idem, v12.48.0)
│ ├── public/assets/js/goaccess.js → JS de la page GoAccess (idem, v12.48.0)
│ ├── public/assets/js/version-check.js → JS du badge de version / mises à jour (idem, v12.48.0)
│ ├── public/assets/js/certbot.js → JS des pages Certbot (HTTP + défi DNS) (idem, v12.48.0)
│ ├── public/assets/js/nginx-cache.js → JS de la page Cache Nginx (idem, v12.48.0)
│ ├── public/assets/js/sync-files.js → JS de la page Sync (idem, v12.48.0)
│ ├── public/assets/js/notify.js → JS du centre de notification + page Notifications (idem, v12.48.0)
│ ├── public/assets/js/configs.js → JS de la page Configs (Zones/Upstreams/fichiers) (idem, v12.48.0)
│ ├── public/assets/js/overview-charts.js → JS des graphiques de la Vue d'ensemble (idem, v12.48.0)
│ ├── public/assets/js/nav.js → JS des sections repliables du menu (idem, v12.48.0)
│ ├── lib/feature-flags.js → interrupteurs marche/arrêt transversaux (ENV > YAML > défaut), v12.45.0
│ ├── features/alerting.js → récupération/analyse du fichier d'alertes distant, v12.45.0
│ ├── config/menu.yml → exemple commenté des réglages de visibilité de menu (v12.42.0)
│ ├── config/features.yml → interrupteurs marche/arrêt transversaux, ex. alerting_enable (v12.45.0)
│ └── config/users.yml
├── docker-compose.yml
└── sample.env
nginx-agent/ (à la racine de cette livraison, à côté de nginx-dashboard/
et nginx-analyzer/) n'apparaît pas dans cet arbre : ce n'est pas un
composant qui tourne aux côtés de nginx, mais le binaire à déployer sur
chaque hôte Docker distant (voir « Hôtes Docker distants (agents) »
plus bas) — son propre README explique sa compilation et son usage.
Format de log nginx — configuration requise
Le dashboard, GoAccess et CrowdSec se basent sur des formats de log précis. Deux formats sont supportés.
Format combined_vhost (recommandé — log global)
Inclut le nom du vhost en préfixe. Utilisé dans logging-config.conf :
log_format combined_vhost '$server_name $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
Exemple de ligne générée :
example.com 1.2.3.4 - - [19/May/2026:10:00:00 +0000] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
Déclaration dans un virtualhost (log global) :
access_log /var/log/nginx/vhosts_access.log combined_vhost if=$is_not_static;
Format combined (standard — log dédié par vhost)
Format nginx standard, sans préfixe vhost. À utiliser quand chaque vhost a son propre fichier de log :
# Pas besoin de déclarer log_format combined — c'est le format par défaut nginx
access_log /var/log/nginx/example.com.access.log combined if=$is_not_static;
Exemple de ligne générée :
1.2.3.4 - - [19/May/2026:10:00:00 +0000] "GET / HTTP/1.1" 200 1234 "-" "Mozilla/5.0"
Détection automatique par GoAccess
Le dashboard détecte automatiquement le format en lisant la première ligne du fichier de log :
- Premier token = IP (
1.2.3.4) → formatcombined - Premier token = hostname (
example.com) → formatcombined_vhost
Aucune configuration manuelle nécessaire côté GoAccess.
Variable $is_not_static (optionnel)
Pour ne logger que les requêtes dynamiques (exclure JS/CSS/images) :
# Dans conf/logging-config.conf
map $request_uri $is_not_static {
~*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|otf|map)(\?|$) 0;
~*\/robots\.txt 0;
default 1;
}
Installation
1. Prérequis
- Docker + Docker Compose v2
- Compte MaxMind (optionnel — GeoIP)
- Compte Cloudflare (optionnel — certbot DNS)
2. Configurer l'environnement
cp sample.env .env
Aucun secret n'est obligatoire à cette étape (depuis la v12.32.0) :
NGINX_DASHBOARD_SESSION_SECRET est auto-générée et persistée au premier
démarrage si elle est absente (voir plus bas), et NGINX_DASHBOARD_API_TOKEN
/ NGINX_DASHBOARD_WEBHOOK_SECRET ne sont utiles que pour des fonctions
optionnelles (API Bearer, signature des webhooks sortants) — le dashboard
fonctionne normalement sans elles. Si vous en avez besoin, le plus simple
(depuis la v12.33.0) est de les générer depuis la page Système du
dashboard une fois connecté (bouton "Générer", valeur affichée une seule
fois) plutôt que d'éditer .env — voir la section dédiée plus bas. Si vous
préférez malgré tout les fixer vous-même dès maintenant dans .env
(recommandé en production, pour ne pas dépendre d'une valeur générée) :
echo "NGINX_DASHBOARD_API_TOKEN=$(openssl rand -hex 32)" >> .env
echo "NGINX_DASHBOARD_SESSION_SECRET=$(openssl rand -hex 32)" >> .env
echo "NGINX_DASHBOARD_WEBHOOK_SECRET=$(openssl rand -hex 32)" >> .env
Important — ne pas mettre de guillemets autour des valeurs dans
.env. Une valeur définie ici a toujours priorité sur une valeur auto-générée.
3. Créer la structure de dossiers
mkdir -p conf sites snippets streams ssl certs geoip_data logs \
backups goaccess/{conf,db,reports} modules cache cloudflare
4. Configurer les utilisateurs (optionnel)
Rien à faire ici pour démarrer : nginx-dashboard/config/users.yml est
fourni avec un compte admin/admin, et depuis la v12.32.0 le dashboard
détecte ce mot de passe d'exemple encore en place au tout premier démarrage
et le remplace automatiquement par un mot de passe aléatoire, affiché une
seule fois dans les logs du conteneur (docker compose logs nginx-dashboard) :
[auth] SECURITE: le compte admin utilise encore le mot de passe d'exemple "admin" livre avec le projet.
[auth] Generation automatique d'un mot de passe aleatoire (affiche UNE SEULE FOIS ci-dessous) :
[auth] username: admin password: <valeur générée>
Si vous préférez choisir vous-même le mot de passe initial, éditez
nginx-dashboard/config/users.yml avant le premier démarrage — les mots
de passe en clair que vous y mettez sont hashés automatiquement, et une
valeur différente de admin n'est jamais touchée par la génération
automatique :
users:
- username: admin
password: mon-mot-de-passe
role: admin
name: Administrateur
enabled: true
5. Activer les modules nginx
# VTS (requis pour le dashboard)
cp conf/vts-settings_conf.DISABLE conf/vts-settings.conf
cp nginx-dashboard/vts-status.conf sites/vts-status.conf
# GeoIP (si compte MaxMind)
cp conf/geoip2-load-database_conf.DISABLE conf/geoip2-load-database.conf
# Filtrage géographique (optionnel)
cp snippets/allow-visit-001_conf.DISABLE snippets/allow-visit-001.conf
6. Build et lancement
docker compose build nginx-dashboard
docker compose up -d
Dashboard : http://serveur:3000 — connectez-vous avec admin et le mot
de passe généré affiché dans les logs (étape 4), ou celui que vous avez
choisi vous-même dans users.yml.
Dashboard — Pages et fonctionnalités
Monitoring
| Page | Contenu |
|---|---|
| Overview | Stat cards (requêtes, connexions, erreurs, trafic, monitoring), graphique historique — grille responsive, s'adapte au nombre de cards |
| Virtual Hosts | Stats VTS par vhost — requêtes, codes HTTP, trafic |
| Upstreams | Backends avec temps de réponse |
Configuration
| Page | Contenu |
|---|---|
| Config files | Explorateur sites/conf/snippets/streams avec coloration syntaxique nginx |
| SSL Certificates | Analyse certs manuels + Let's Encrypt — expiration, SANs, fingerprint |
| Live logs | Stream SSE par fichier .log, filtres, pause, auto-scroll |
Déploiement et sauvegardes
| Page | Contenu |
|---|---|
| Git Deployment | Pull, diff, test éphémère, pipeline complet, gestion branche backup |
| Backups | ZIP locaux (configs + certs LE), téléchargement, restauration, backup manuel |
Configuration
| Page | Contenu |
|---|---|
| VHost Generator | Générateur de config reverse proxy avec brouillons localStorage |
Contrôle
| Page | Contenu |
|---|---|
| NGX Ops | Test config via conteneur éphémère, reload, statut |
| Events | Journal chronologique de toutes les actions |
Diagnostic
| Page | Contenu |
|---|---|
| Diagnostic | Grille de cards (une par vhost) + panneau glissant : Vue d'ensemble, Backends, Sécurité (audit statique + analyse live des en-têtes), Monitoring, Schéma, Conf |
| Monitoring | Cards groupées par vhost, sparkline par cible, historique des pannes — vue globale continue |
| Schéma | Vue graphique SVG de tous les vhosts et de leurs cibles résolues |
Intégrations
| Page | Contenu |
|---|---|
| REST API | Documentation interactive avec test en direct |
| Webhooks | CRUD webhooks, test manuel |
| CrowdSec | Décisions locales vs CAPI, alertes, sources, santé parser (si configuré) |
| Blocklists IP | Sources agrégées, IP uniques bloquées, rafraîchissement manuel |
| GoAccess | Analyse logs par vhost, rapport HTML temps réel (si configuré) |
Administration
| Page | Contenu |
|---|---|
| Users | Gestion utilisateurs, matrice permissions, sessions actives |
| Système | Registre des paramètres du dashboard lui-même — valeur actuelle, valeur par défaut, comment la surcharger, explication (admin uniquement, voir plus bas) |
Interface — thème, en-tête personnalisé, menu
-
Thème de couleur : vert (défaut, historique) ou bleu, au choix dans le sélecteur de l'en-tête. N'affecte que le "chrome" de marque (logo, badges, item de menu actif, boutons primaires, focus des champs) — les couleurs de statut (vert = up/succès, rouge = down, etc.) restent identiques dans les deux thèmes, ce n'est pas ce qui change. Choix mémorisé par navigateur (
localStorage), appliqué immédiatement sans rechargement. -
Texte personnalisé dans l'en-tête :
BRANDING_HEADER_TEXT(variable d'environnement, voirsample.env) affiche une chaîne libre entre le nom "NGX Ops" et les badges de version — nom de client, environnement (STAGING,PROD-EU…), etc. Absente : rien n'est affiché. -
Menu repliable : chaque section du menu latéral (Monitoring, Configuration, Contrôle, Intégrations, Administration) se replie/déplie d'un clic sur son en-tête, pour garder le menu lisible même avec beaucoup d'intégrations activées. État mémorisé par navigateur, par section.
-
Visibilité des éléments de menu optionnels (WAF, GoDNS, API REST, Webhooks, Auto-config Docker, Hôtes distants) (v12.39.0 puis v12.41.0, retours utilisateur) : l'item WAF n'a de sens que si l'image nginx en cours d'exécution embarque réellement un WAF (image dont le tag se termine par
-wafou-coraza) ; l'item GoDNS n'a d'intérêt que si le DNS dynamique est utilisé ; API REST et Webhooks n'ont d'intérêt que si un jeton/secret est configuré ; Auto-config Docker et Hôtes distants n'ont d'intérêt que si la fonctionnalité correspondante est activée. Tous sont réglables, avec priorité ENV >config/menu.yml> défaut :Variable config/menu.ymlDéfaut Valeurs MENU_WAFwaf_menuautoauto(détecte le suffixe-waf/-corazade l'image nginx en cours),show,hideMENU_GODNSgodns_menushowshow,hide(pas de modeauto: rien dans une image de conteneur n'indique un usage de DNS dynamique)MENU_APIapi_menuautoauto(masqué siAPI_TOKENabsent),show,hideMENU_WEBHOOKSwebhooks_menuautoauto(masqué siWEBHOOK_SECRETabsent),show,hideMENU_DOCKER_AUTOCONFIGdocker_autoconfig_menuautoauto(masqué sienable: falsedansdocker-autoconfig.yml),show,hideMENU_AGENTSagents_menuautoauto(masqué sienable: falsedansagents.yml),show,hide# config/menu.yml waf_menu: hide # cette instance n'utilise pas d'image WAF godns_menu: hide # DNS dynamique jamais utilisé ici api_menu: hide # API REST jamais utilisée ici webhooks_menu: hide # Webhooks jamais utilisés ici docker_autoconfig_menu: hide # auto-config Docker désactivée agents_menu: hide # hôtes distants désactivésL'état résolu (mode, source ENV/YAML/défaut, image détectée pour WAF) est visible sur la page Système, catégorie « Menu ». Nouvelle route :
GET /api/menu-visibility(VIEW_CONFIGS, donc accessible à tous les rôles connectés — le menu doit s'afficher correctement pour un viewer comme pour un admin).v12.72.0 — formulaire Menu : sur la page Système, le bouton Menu (admin) ouvre un formulaire (liste déroulante par entrée) qui écrit
config/menu.ymlen conservant ses commentaires ; l'édition manuelle du fichier reste valable. Entrées supplémentaires masquables (défautshow) :sync_menu,vhostgen_menu,graph_menu,deploy_menu,blocklists_menu,wafemu_menu,geomap_menu,digest_menu,cache_menu,notifications_menu(variablesMENU_CACHE,MENU_NOTIFICATIONS,MENU_SYNC,MENU_VHOSTGEN,MENU_GRAPH,MENU_DEPLOY,MENU_BLOCKLISTS,MENU_WAFEMU,MENU_GEOMAP,MENU_DIGEST) etgoaccess_menu(auto= affiché si des sources sont configurées, ouhide;MENU_GOACCESS). Une variableMENU_*définie verrouille la ligne dans le formulaire. Routes :GET /api/menu-config,POST /api/menu-config/save(admin). Les pages Blocklists IP, Diagnostic et Monitoring ont désormais un bouton Aide.Un
config/menu.ymld'exemple, commenté, avec les six réglages déjà à leur valeur par défaut, est livré avec le projet (v12.42.0) — il suffit de changer les valeurs qui doivent différer du défaut plutôt que de recréer le fichier de zéro.
Authentification et rôles
| Permission | admin | operator | viewer |
|---|---|---|---|
| Métriques / VH / Upstreams | ✓ | ✓ | ✓ |
| Configs / SSL / Logs | ✓ | ✓ | ✓ |
| CrowdSec / GoAccess | ✓ | ✓ | ✓ |
| Reload / Test nginx | ✓ | ✓ | — |
| Déploiement Git / Sauvegardes | ✓ | ✓ | — |
| Webhooks | ✓ | — | — |
| Gestion utilisateurs | ✓ | — | — |
Snippets — métadonnées
Les snippets peuvent déclarer des métadonnées dans leur en-tête pour contrôler leur utilisation dans le générateur de vhost :
# name: proxy-common
# description: Headers proxy standard
# emplacement: server,location
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
| Champ | Valeurs | Défaut si absent |
|---|---|---|
name |
Libellé affiché | Nom du fichier sans .conf |
description |
Description courte | (vide) |
emplacement |
server, location, ou les deux |
server,location (universel) |
Les snippets ssl-*.conf sont automatiquement considérés comme emplacement: server uniquement.
Générateur de VHost
Le générateur produit une config reverse proxy nginx complète.
Sources SSL disponibles :
- Snippets
ssl-*.confdu dossiersnippets/— génèreinclude snippets/ssl-xxx.conf - Certificats manuels du dossier
ssl/— pré-remplitssl_certificateetssl_certificate_key - Certificats Let's Encrypt dans
certs/live/— pré-remplit avec les chemins Certbot
Mode Docker : coche "Docker container" pour générer :
set $backend http://mon-conteneur:3000;
resolver 127.0.0.11 valid=30s;
Brouillons : stockés dans localStorage, plusieurs nommés, téléchargement direct en .conf.
Déploiement : télécharger le fichier généré et le placer dans sites/ (manuellement ou via le déploiement Git).
Diagnostic (fusion Backends + Audit, page Diagnostic)
Les anciennes pages Backends et Audit étaient deux listes verticales
quasi identiques (une par vhost) et devenaient illisibles avec une
cinquantaine de vhosts. Elles ont été fusionnées en une seule page
Diagnostic : une grille de cards responsive (3 à 5 par ligne selon la
largeur d'écran, grid-template-columns:repeat(auto-fill,minmax(230px,1fr)),
sans breakpoint JS), une par vhost, résumant sa posture d'un coup d'œil —
HTTP/HTTPS, pire niveau d'audit statique (OK/INFO/WARNING), état de
monitoring (UP/DOWN/non surveillé), désactivé ou non. Cliquer une card
ouvre un panneau glissant (slide-over, pas une modale ni une nouvelle
page) avec 6 onglets pour ce vhost précis :
| Onglet | Contenu |
|---|---|
| Vue d'ensemble | Fichier, blocs server{} avec leurs listen, badge SSL et pire niveau d'audit par bloc, état de monitoring |
| Backends | Résolution des cibles (proxy_pass) et test à la demande — reprend l'ancienne page Backends |
| Sécurité | Remarques statiques (posture SSL/HTTPS) + bouton Analyser les en-têtes par bloc nommé (sonde live) |
| Monitoring | Cibles surveillées de ce vhost uniquement, avec sparkline et historique — filtre de la page Monitoring globale |
| Schéma | Le même schéma SVG que la page Schéma, agrandi, pour ce vhost seul |
| Conf | Contenu brut du fichier, coloration syntaxique — lecteur de la page Configuration |
La page Monitoring globale et la page Schéma globale restent disponibles séparément dans le menu (rien n'a été retiré) — le panneau Diagnostic n'en est qu'une vue filtrée, pratique, sur un seul vhost à la fois.
Résolution des cibles (onglet Backends)
Pour chaque vhost de sites/, lib/vhost-targets.js + features/backends.js
(GET /api/backends) résolvent et affichent la cible réelle de chaque
location avec proxy_pass, puis permettent de la tester à la demande
(équivalent d'un curl -k <url> -H "Host: ...", sans quitter le dashboard) :
code de statut, temps de réponse, ou l'erreur si le backend ne répond pas.
C'est un résolveur au mieux-effort, pas un parseur nginx complet — il reconnaît
les formes que ce projet produit ou utilise réellement lui-même, et se replie
sur unresolved (texte brut affiché tel quel) plutôt que de deviner :
| Badge | Signifie | Reconnu quand |
|---|---|---|
DIRECT |
IP ou nom d'hôte litéral | proxy_pass http://host:port; |
UPSTREAM (LB/HA) |
Un pool upstream{} — toutes les cibles sont testables individuellement |
proxy_pass http://nom; avec un bloc upstream nom { server a; server b; } défini quelque part dans conf/ ou sites/ |
DOCKER |
Conteneur résolu via le DNS intégré de Docker (127.0.0.11) | Le motif que génère le Générateur de VHost ci-dessus (set $var ...; resolver 127.0.0.11; proxy_pass ...;) — reconnu sous ses deux formes (variable = URL complète, ou variable = nom d'hôte seul avec scheme/port dans le proxy_pass, comme dans global-error.conf/00-default.conf) |
VARIABLE |
Résolu via une variable, mais sans directive resolver trouvée dans le fichier — probablement pas Docker, affiché quand même |
idem sans resolver |
NON RÉSOLU |
Variable référencée sans set correspondant, ou valeur qui ne ressemble à aucune URL |
tout le reste |
Sécurité : POST /api/backends/check ne reçoit jamais un host/port du
client — il ne reçoit qu'une référence (file, blockIndex, locationIndex,
targetIndex, et optionnellement serverNameIndex — voir plus bas), et
refait lui-même la résolution côté serveur à partir du fichier vhost avant de
contacter quoi que ce soit. Impossible d'en faire un scanner de ports
interne. Accessible avec la permission VIEW_CONFIGS (donc au rôle viewer
aussi, comme le reste de la page Configuration) puisque c'est une lecture +
un test, jamais une écriture.
Redirection HTTP → HTTPS détectée côté backend : si la cible est en
http:// et répond par une redirection (301/302/303/307/308) vers une URL
https://, le test l'indique explicitement (badge ↪ HTTPS + l'URL de
redirection) au lieu d'afficher un code de statut brut qui pourrait passer
pour un problème — c'est le backend qui impose lui-même le HTTPS, un
comportement normal.
-k (ignorer les erreurs TLS du backend) reste toujours actif — la
plupart des backends internes utilisent un certificat auto-signé, et cette
requête ne sort jamais vers l'Internet public, seulement vers les cibles que
l'opérateur a lui-même configurées.
Mode verbeux (-v) : la case à cocher « Verbeux » en haut de l'onglet
Backends ajoute à chaque test les en-têtes de la requête envoyée et de la
réponse reçue, et — pour une cible HTTPS — un résumé du certificat présenté
par le backend (sujet, émetteur, validité, auto-signé ou non), affiché même
si, comme ci-dessus, il n'est jamais vérifié.
Plusieurs server_name sur le même backend : quand un bloc server{}
déclare plusieurs domaines pour une même cible (alias, www + apex, domaines
historiques…), un routage par nom côté backend peut répondre différemment
selon le domaine — l'un peut avoir cessé d'être servi sans que les autres
s'en aperçoivent. Dans ce cas, l'onglet affiche un bouton « Tester » par
domaine plutôt qu'un seul test générique ; chaque test précise dans sa
requête l'index du server_name à utiliser (jamais le nom lui-même — le
serveur ne fait confiance qu'à ce qu'il retrouve dans le fichier), avec repli
automatique sur le premier nom si l'index est absent ou invalide.
Vhosts de redirection pure masqués : un bloc server{} en listen 80
qui ne fait que rediriger vers son pendant HTTPS (return 301 https://...;,
sans aucune location avec proxy_pass — généralement le même
server_name que le bloc 443 du même fichier) n'a aucun backend à tester et
n'apparaît pas dans l'onglet Backends. L'onglet Sécurité, qui juge la
posture SSL et non les cibles à tester, continue lui de signaler ce même
bloc — normalement, pas comme un problème.
Audit statique et analyse live des en-têtes (onglet Sécurité)
GET /api/audit (lib/vhost-audit.js + features/audit.js) relit la même
résolution que l'onglet Backends (lib/vhost-targets.js) et l'annote de
remarques statiques — aucun appel réseau n'est fait pour générer la
liste, elle est donc instantanée.
Chaque remarque porte un niveau :
| Niveau | Signifie |
|---|---|
OK |
Conforme — rien à faire |
INFO |
À savoir, pas un problème en soi |
WARNING |
À corriger |
Règles statiques appliquées, vhost et cible confondus :
- Vhost en HTTP sans redirection vers HTTPS →
WARNING: aucune directivereturn 301 https://...nirewrite ... https://...trouvée dans le bloc — suggestion : forcer la redirection, ou terminer directement en HTTPS. - Vhost en HTTP dont le seul rôle est justement cette redirection →
INFO, pasWARNING: c'est le comportement attendu, pas une remarque à corriger. - Décharge SSL (vhost en HTTPS, mais connexion vers le backend en clair,
HTTP) →
INFO: un cas courant et pas automatiquement fautif, mais la bonne pratique minimale rappelée explicitement est de chiffrer aussi cette seconde moitié du trajet — passer le backend en HTTPS (proxy_pass https://...) avecproxy_ssl_verify off;si le certificat backend est auto-signé, plutôt que du HTTP en clair côté interne. - Chiffré de bout en bout (vhost HTTPS, backend HTTPS) →
OK; siproxy_ssl_verify off;est présent sur la location, la remarque le mentionne (vérification du certificat backend désactivée — attendu avec un certificat auto-signé). - Vhost en HTTP mais backend contacté en HTTPS →
INFO, cas rare mais possible, signalé pour information. - Cible non résolue (
NON RÉSOLUdans l'onglet Backends) →WARNING: impossible d'auditer ce chemin, signalé plutôt que silencieusement ignoré.
Analyse live des en-têtes (POST /api/audit/headers, bouton « Analyser
les en-têtes » — à la demande, par bloc server{} nommé, jamais automatique)
sonde les deux côtés indépendamment — le reverse proxy lui-même (même
Host/SNI qu'un vrai visiteur) et le backend directement — pour distinguer
un en-tête ajouté (ou retiré) par nginx d'un en-tête envoyé par le backend
lui-même :
| Code | Niveau | Signifie |
|---|---|---|
proxy-unreachable |
WARNING |
Le reverse proxy ne répond pas — aucune autre remarque n'est déduite (évite un faux "en-tête absent") |
missing-hsts |
WARNING |
HTTPS sans Strict-Transport-Security |
hsts-short |
INFO |
HSTS présent mais max-age < ~6 mois |
hsts-ok |
OK |
HSTS présent avec une durée suffisante |
server-version-leak |
WARNING |
En-tête Server avec un numéro de version (ex. nginx/1.24.0) |
server-generic |
OK |
Server présent mais générique (sans version) |
x-powered-by-leak |
WARNING |
En-tête X-Powered-By présent |
backend-header-passthrough |
WARNING |
Le Server du backend (avec version) ressort identique côté proxy — non filtré |
missing-nosniff / missing-frame-options / missing-referrer-policy / missing-csp |
INFO |
X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy absents |
Même garantie de sécurité et même permission que l'onglet Backends
(VIEW_CONFIGS, lecture seule) : POST /api/audit/headers ne reçoit que
{file, blockIndex}, jamais un host/port, et refait toute la résolution
(port d'écoute, server_name, cible backend) côté serveur.
Backend « sans cible » ≠ échec : un bloc qui ne fait que rediriger HTTP →
HTTPS (return 301 https://...;, comme ci-dessus) n'a par construction
aucune location avec proxy_pass, donc rien à sonder côté backend. Ce cas
affiche un badge neutre (N/A) plutôt que le badge rouge ÉCHEC — ce
dernier reste réservé à une cible qui existe réellement mais n'a pas
répondu. Avant cette distinction, un vhost de redirection pure affichait à
tort « Backend : ÉCHEC — no resolvable backend target on this server block »,
alors qu'il n'y avait simplement rien à comparer.
Rechercher et filtrer les vhosts (grille Diagnostic)
Au-dessus de la grille de cards, un champ de recherche filtre par nom de
fichier ou par server_name (insensible à la casse), combiné à un filtre
Tous / Actif / Désactivé sur le même état enabled que la page Config files
(suffixe .conf.DISABLE). Purement côté client — pas de rafraîchissement
serveur à chaque frappe — et se combine avec le filtre d'état (ET logique,
pas l'un ou l'autre).
Exclure un fichier ou un bloc du Diagnostic
Sur le même principe que le monitoring continu (commentaire, jamais une vraie directive nginx — sans effet sur la configuration réellement servie), deux flags permettent d'exclure un vhost de la page Diagnostic quand ses remarques n'ont pas de sens pour lui (un cas particulier connu, un fichier surtout fait de snippets/includes, etc.) :
# nginx-control-diagnostic: off
server {
server_name interne.exemple.com;
...
}
# nginx-control-diagnostic: off, sur sa propre ligne de commentaire, avant le premier blocserver{}du fichier — exclut le fichier entier : aucune card Diagnostic pour lui.# nginx-control-diagnostic-vhost: off, à l'intérieur d'un blocserver{}— exclut seulement ce bloc-là ; les autres blocs du même fichier (et donc sa card) restent audités normalement.
Les deux sont scopés à la page Diagnostic uniquement : les pages Backends,
Monitoring et Schéma (qui partagent le même résolveur,
lib/vhost-targets.js) ne sont pas affectées — un vhost exclu du Diagnostic
reste par exemple surveillable via # nginx-control-monitoring: on.
Monitoring continu (page Monitoring)
Le monitoring d'un vhost est opt-in, activé par un commentaire dans le
bloc server{} — jamais une vraie directive nginx, donc ceci n'a strictement
aucun effet sur la configuration réellement servie :
server {
# nginx-control-monitoring: on
# nginx-control-monitoring-interval: 60s
# nginx-control-monitoring-valid-http-code: 2xx, 3xx
listen 443 ssl;
server_name exemple.com;
location / { proxy_pass http://backend:8080; }
location /api {
# nginx-control-monitoring-ignore-location: on
proxy_pass http://backend:8080; # meme cible que ci-dessus — inutile de la sonder deux fois
}
}
# nginx-control-monitoring: on(ouoff, ou absent — équivalent àoff) active ou non le monitoring pour ce bloc ; chaque blocserver{}d'un fichier a son propre flag, indépendamment des autres.# nginx-control-monitoring-interval: 60sfixe l'intervalle entre deux sondes pour ce bloc ; par défaut 60 s, plancher forcé à 10 s (une faute de frappe du type1sne peut pas transformer ceci en bombardement du backend).# nginx-control-monitoring-valid-http-code: 2xx, 3xxsurcharge la règle par défaut décidant si une cible est UP ou DOWN. Par défaut (flag absent), une réponse2xx/3xx/4xxcompte comme UP et seul un5xx(ou l'absence totale de réponse — timeout, connexion refusée) compte comme DOWN. Ça ne suffit pas pour un backend exposé derrière un reverse proxy supplémentaire type Traefik : un conteneur arrêté y répond quand même, en général par un404normal — sans surcharge, ce404reste lu comme UP indéfiniment. La valeur est une liste séparée par des virgules de motifs, chacun étant soit une classe entière (2xx…5xx), soit un code exact (404), soit une plage (200-299) ; le tableau ci-dessous liste ceux réellement en usage. Documenté et testé danslib/monitor-status.js/test/monitor-status.test.js.# nginx-control-monitoring-ignore-location: on(ouoff/absent — équivalent àoff), posé à l'intérieur d'unelocation, exclut uniquement cette location du monitoring sans toucher au flag du bloc ni aux autres locations. Utile quand plusieurslocationd'un même vhost proxifient la même cible (des chemins différents vers le même backend) : sans ce flag, chacune serait surveillée indépendamment — des sondes en double sur exactement la même chose, pour rien.- Une fois actif, chaque cible résolue du bloc (une seule en
DIRECT, plusieurs enUPSTREAM (LB/HA)— chaque membre du pool suivi individuellement), hors locations marquéesignore-location, est sondée sur son propre intervalle, avec exactement la même sonde que le test à la demande de l'onglet Backends (même fonction réutilisée,curl -kimplicite, jamais de vérification TLS du backend) — seule la classification UP/DOWN du résultat diffère (lib/monitor-status.js), pas la sonde elle-même.
Ce qui est suivi : le code de statut et le temps de réponse de chaque
sonde, et un historique des pannes — le moment où une cible est passée en
échec et celui où elle a répondu de nouveau (un échec continu ne crée qu'un
seul incident, mis à jour ; le premier succès qui suit le referme). Rétention
par défaut 30 jours (MONITOR_RETENTION_DAYS), au-delà desquels les
checks et les incidents clos sont purgés — un incident encore en cours n'est
jamais purgé, quel que soit son âge.
Stockage : lib/monitor-store.js, SQLite via node:sqlite (intégré à
Node 22, donc toujours zéro dépendance npm), dans monitoring.db à côté de
users.yml/events.db — aucun nouveau volume à monter. Si SQLite est
indisponible, dégradation automatique vers un historique en mémoire
uniquement (perdu au redémarrage, le monitoring continue de fonctionner).
Page Monitoring : les cibles sont groupées par vhost (un fichier
peut avoir plusieurs cibles — plusieurs location, ou plusieurs membres
d'un pool UPSTREAM (LB/HA) — elles partagent une seule card plutôt que
d'en occuper une chacune) dans une grille responsive
(repeat(auto-fill,minmax(300px,1fr)), plusieurs cards par ligne selon la
largeur d'écran). Chaque cible affiche son état courant (UP/DOWN), un
sparkline (mini-graphe en barres, vert/rouge, sur les 20 derniers checks
— sparkline dans la réponse de GET /api/monitor, dérivé de
monitorStore.getHistory(key, 20)) pour repérer une panne récente d'un coup
d'œil, sa disponibilité sur les dernières 24h, l'heure du dernier check, et
— si elle est actuellement en panne — depuis quand ; une card avec beaucoup
de cibles défile indépendamment (max-height + overflow-y:auto) plutôt que
d'étirer toute la grille. Un panneau « Historique » par cible, ouvert à la
demande, détaille les pannes passées (début, fin, dernière erreur) et les 20
derniers checks bruts. Le bouton « Vérifier maintenant »
(POST /api/monitor/check-now) force une sonde immédiate de toutes les
cibles surveillées sans attendre leur intervalle — utile après avoir modifié
un vhost ou juste activé le flag. Le même filtre par vhost, sans le
regroupement (puisqu'il n'y a alors qu'un seul vhost), alimente l'onglet
Monitoring du panneau Diagnostic.
Fonctionnement interne : une seule boucle réexamine les vhosts toutes les 30 secondes (une modification, ou le flag qu'on vient d'activer/désactiver, est donc pris en compte en moins de 30 s sans redémarrer le conteneur), et une seconde boucle, toutes les 5 secondes, sonde toute cible dont l'intervalle est écoulé — plutôt qu'un timer par cible, plus simple à raisonner avec potentiellement des dizaines de cibles actives.
Accessible avec la permission VIEW_CONFIGS (lecture + sonde, jamais
d'écriture), comme la page Diagnostic.
NGX Ops Intelligence — envoi des alertes (v12.69.0)
Client émetteur uniquement : la plateforme centrale n'existe pas encore. Fonction désactivée par défaut, pilotée par config/intelligence.yml
(Éditeur de configuration). Statut, aperçu du prochain lot et « envoyer maintenant » sur la page Analyse (admin).
Modes
anonymous: l'instance se présente seulement par sonINSTANCE_ID(unique, persistant, page Système). Confiance faible côté serveur.identified:INSTANCE_ID+user(e-mail) + jeton (Authorization: Bearer). Jeton : ENVINTEL_TOKEN(prioritaire) sinontoken:du YAML.
URL : indéfinie par défaut. ARG/ENV INTEL_API_URL du Dockerfile (et docker-compose.yml / sample.env) ; la clé api_url: du YAML la surcharge. HTTPS obligatoire, sans identifiants dans l'URL.
Alertes envoyées : toutes les règles sauf exclude_rules (défaut volumetric, country_traffic) ; include_rules restreint (ex. ua_attack_tools, probe_sensitive_files, attack_injection_patterns, scan) ; min_severity.
Confidentialité : share_user_agents, share_paths, share_vhost (faux par défaut). backfill_hours rejoue le passé au premier envoi (0 = futur seulement).
Livraison par curseur d'ID d'alerte, lots idempotents, aucun doublon après redémarrage.
Requête
POST {api_url}
Content-Type: application/json
Accept: application/json
User-Agent: NGXOps
X-NC-Schema: 1
X-NC-Instance-Id: <INSTANCE_ID>
X-NC-Client-Version: <version>
Idempotency-Key: <batch_id>
Authorization: Bearer <token> (identified seulement)
X-NC-User: <user> (identified seulement)
{
"schema": 1,
"batch_id": "uuid",
"sent_at": "2026-10-02T14:50:00Z",
"mode": "anonymous | identified",
"instance": { "id": "<INSTANCE_ID>", "version": "12.69.0" },
"user": "romain@example.org",
"alerts": [{
"id": 1234, "ts": "2026-10-02T14:49:12Z",
"rule": { "key": "custom_ua_attack_tools", "kind": "builtin|custom", "id": 7, "name": "ua_attack_tools" },
"severity": "low|medium|high",
"scope": "ip",
"ip": "203.0.113.5", "ips": ["..."],
"window_minutes": 60, "requests": 120, "distinct_paths": 37, "matches": 12,
"not_found": 80, "auth_failures": 0, "distinct_ips": 1,
"first_seen": "...", "last_seen": "...", "statuses": {"404": 80},
"user_agents": ["sqlmap/1.7"], "paths": ["/.env", "/wp-config.php"], "vhost": "example.com"
}]
}
Champs optionnels omis si absents ou non partagés. user présent seulement en mode identified.
Réponses attendues
| Code | Sens | Comportement du client |
|---|---|---|
2xx {"accepted": n, "rejected":[{"id":1,"reason":"..."}]} |
lot traité | curseur avancé |
429 + Retry-After (s ou date) |
protection | pause jusqu'à Retry-After (sinon backoff), lot rejoué à l'identique |
| 401 / 403 | identité refusée | envoi suspendu jusqu'à modification de la configuration |
| 413 | lot trop gros | taille de lot divisée par 2 |
| autre 4xx | lot invalide | lot abandonné (journalisé), curseur avancé |
| 5xx / réseau / timeout | indisponible | backoff exponentiel, lot rejoué (même batch_id) |
Le serveur doit dédupliquer sur Idempotency-Key / alerts[].id + instance.id.
Action par liste (v12.69.0)
Une source url accepte action: deny_403 | drop_444 | challenge qui remplace block_action pour cette liste (challenge exige challenge_enable: true, sinon ses IP ne sont pas appliquées et le fichier généré l'indique). IP présente dans plusieurs listes : l'action la plus sévère l'emporte (444 > 403 > challenge). Technique : valeurs de la table geo (1 = 403, 2 = 444) ; blocklist-enforce.conf ajoute if ($blocklist_ip = 2) { return 444; } seulement si nécessaire.
Désactivé = fichiers vidés, jamais supprimés (v12.69.0)
Les fichiers générés (blocklist-ips.conf, blocklist-enforce.conf, blocklist-challenge.conf, snippets de gate) sont créés au démarrage s'ils manquent et ne sont jamais supprimés : nginx les inclut. Désactiver la blocklist (enable: false) ou le challenge (challenge_enable: false) les réécrit vides/commentés (tick de 30 s, nginx -t puis reload, rollback si échec, nouvel essai après 10 min).
Diagnostic et événements (v12.70.5)
La clé d'activation est enable (l'alias enabled est accepté) ; toute autre clé inconnue est signalée dans les erreurs de configuration. La page Événements journalise intelligence.sent, intelligence.error, intelligence.recovered, intelligence.initialized, intelligence.config_error et intelligence.config_ok. Au premier cycle seules les alertes à venir sont envoyées (backfill_hours pour rejouer l'historique).
Ping de connexion (v12.71.0)
POST /api/v1/ping (mêmes en-têtes que l'envoi, sans alerte ni effet sur la réputation). Réglages de intelligence.yml : ping_enable (défaut true), ping_interval_minutes (défaut 5, min 3, max 60), ping_url / url_ping (défaut dérivé de api_url, même hôte obligatoire). Le ping part au démarrage et après toute modification du fichier, puis périodiquement ; il est sauté si un lot vient de réussir, suspendu après un 401/403 tant que la configuration ne change pas, et la limite max_alerts_per_batch annoncée par le serveur plafonne la taille des lots. Le bouton Tester la connexion de la page Système (admin, visible si l'URL est configurée) affiche latence, serveur, authentification, limites et avertissements.
WAF émulé (v12.70.0)
WAF émulé : un conteneur ngx-waf (même image que l'analyseur Go, second binaire /usr/local/bin/waf) relit les journaux d'accès nginx et rejoue chaque ligne comme une requête synthétique dans Coraza v3 (mode DetectionOnly) avec le jeu de règles OWASP CRS (volume /waf-rules). Page WAF émulé (/waf-emu), conteneur géré comme celui de l'Analyse (démarrer, arrêter, mettre à jour l'image).
Limites (affichées sur la page et à lire avant d'activer) : ce n'est pas un WAF en ligne. Analyse a posteriori : il ne bloque jamais lui-même, ne voit pas les corps POST ni les en-têtes absents du journal, et la remédiation (listes) ne protège que les requêtes suivantes de l'IP. accept_limits: true est obligatoire dans config/waf.yml pour démarrer.
Activation : config/waf.yml (enable, accept_limits, host_data_path, host_rules_path, host_logs_path, container_image, crs_version, …), variable WAF_DEFAULT_IMAGE (défaut : image de l'analyseur) et WAF_TOKEN (généré et conservé dans .generated-secrets.json, jamais envoyé au navigateur). Au premier démarrage le CRS (crs_version, depuis github.com uniquement) est téléchargé dans le volume ; « Mettre à jour le CRS » depuis la page ; règles personnalisées dans custom/.
Réglages (page WAF émulé, enregistrés dans le conteneur) : mode observation (défaut) ou remédiation, global ou par vhost ; niveau de paranoïa (1-4) global ou par vhost ; choix des vhosts analysés (tous / sélection) ; règles désactivées globalement ou par vhost ; exceptions ciblées (règle + vhost + préfixe de chemin / User-Agent / IP) créables en un clic depuis une détection ; statuts ignorés, IP exclues (IP privées toujours ignorées) ; onglet Exceptions (exceptions, règles désactivées globalement / par vhost, retrait en un clic) ; simulateur (score, règles retenues et supprimées avec la raison).
Score et listes : chaque règle CRS ajoute son score (CRITICAL 5, ERROR 4, WARNING 3, NOTICE 2) ; somme par IP sur la fenêtre (60 min par défaut), uniquement pour les vhosts en mode remédiation. Seuil challenge → liste waf-challenge ; seuil block → liste waf-block (durées 24 h / 72 h). Les listes sont consommées par les Blocklists avec des sources type: waf (voir config/blocklists.yml, list: block|challenge, action par liste, liste blanche appliquée). waf-challenge exige challenge_enable: true.
Formats de journal : combined et combined_vhost fonctionnent directement (sans préfixe vhost, un fichier par vhost est nécessaire). Pour plus de précision, l'onglet Conseils fournit le format waf_json (en-têtes Accept, Content-Type, Origin…) à écrire dans un fichier séparé *.wafemu.log : aucun conflit avec GoAccess ni l'analyseur. Avec un journal combined, les règles CRS qui jugent des en-têtes absents du journal (920280, 920300, 920320…) sont désactivées automatiquement (« règles système ») pour éviter les faux positifs, et des en-têtes neutres sont injectés. Les requêtes statiques exclues par $is_not_static ne sont pas analysées. Les deux journaux peuvent être actifs en même temps (combined + waf_json) : les doublons sont écartés automatiquement (la ligne waf_json est prioritaire), il n'y a pas de double détection. Dans les Conseils, « lignes illisibles » (format inattendu, échantillons affichés), « saturation » (file pleine) et « moteur non prêt » sont distingués.
Conseils (diagnostic) : format de journal, vhosts sans journal ou sans trafic, CRS absent / règles ignorées, saturation de la file, remédiation sans blocklist ou sans challenge actif.
Alertes et NGX Ops Intelligence : une alerte WAF (type waf) est créée par IP et par fenêtre quand le score atteint alertMinScore. À la lecture, les règles désormais désactivées (globalement, par vhost, exceptions, système) sont retirées et l'alerte disparaît si le score restant est insuffisant : une règle désactivée n'envoie jamais d'alerte. Le client Intelligence les envoie avec un curseur propre (rule.kind: "waf", objet waf: {score, mode, rules[{id,count,severity}], tags, categories}) ; exclude_rules: [waf] les retire.
Performances mesurées (CRS 4.8, PL1, 2 cœurs) : ~2 400 lignes/s (~0,8 ms par requête) ; trafic normal : 0 détection sur 38 requêtes légitimes variées en PL1 ; en PL2, quelques faux positifs possibles (d'où l'observation par défaut).
API (dashboard, VIEW_CONFIGS en lecture, DEPLOY en écriture) : /api/waf/status|settings|vhosts|rules|hits|hits/stats|simulate|lists|alerts|rules/update|custom-rules|diagnostics|container/*. Contrat complet conteneur ↔ dashboard : document du projet claude/design-waf-emule.md.
Onglet Listes : état Blocklist et Preuves (v12.70.3)
Chaque IP des listes block / challenge affiche si les Blocklists l'appliquent (colonne Blocklist) ; si une IP est listée mais absente de la source type: waf, la colonne indique la cause (source manquante ou désactivée, liste blanche, challenge désactivé, rafraîchissement en attente, erreur). Le bouton Preuves ouvre le détail du blocage : règles déclenchées, dernières requêtes, dernière alerte.
Blocklist « analyzer » : cadence et historique (v12.53.0)
La source type: analyzer (liste générée depuis les règles d'Analyse) est recalculée chaque
minute (analyzer_interval_cron dans config/blocklists.yml), sans téléchargement ; les
listes url restent au cron général interval_cron. nginx n'est rechargé que si la liste
change. La page Blocklists IP affiche un historique filtrable et paginé des ajouts,
retraits et détections « observation seule » (export CSV, GET /api/blocklists/history).
Détails dans le CHANGELOG (v12.53.0).
nginx-analyzer en Go (v12.52.0) — −40 % de RAM, remplacement direct
nginx-analyzer-go/ est une réécriture en Go de nginx-analyzer : même API, mêmes
variables d'environnement, même base SQLite, donc aucun changement côté dashboard. Sur des
logs identiques, 82 Mo au lieu de 131 Mo (20 000 lignes) et 176 Mo au lieu de 297 Mo
(300 000 lignes, 50 000 IP suivies), avec une ingestion plus rapide.
Bascule : construire l'image depuis nginx-analyzer-go/, puis changer container_image
dans nginx-dashboard/config/analyzer.yml (ou le build.context du bloc commenté de
docker-compose.yml). Le volume de données est repris tel quel (historique, baseline
apprise, exceptions, règles). Retour arrière : remettre l'image Node — la base écrite par
le Go se rouvre sans perte. Détails, réglages (GOGC, GOMEMLIMIT, TZ), architecture et
procédure de tests dans nginx-analyzer-go/README.md.
Gouvernance des règles d'analyse (page Analyse)
La page Analyse s'appuie sur nginx-analyzer, un service séparé qui
détecte des comportements suspects dans les journaux nginx (brute-force,
scan, flood, scraping, anomalies volumétriques par vhost et par pays).
Depuis la v12.5.0, ces règles ne sont plus figées dans le code : elles se
consultent, s'activent/désactivent et se personnalisent entièrement depuis
le dashboard, sans jamais redémarrer le conteneur nginx-analyzer.
La modale « Règles »
Un bouton ⚙ Règles, à côté du bouton de rafraîchissement de la page Analyse, ouvre une modale listant :
- Les 6 règles intégrées, chacune avec son identifiant numérique
(
1brute-force,2scan,3flood,4scraping,5anomalie volumétrique par vhost,6anomalie de trafic par pays), son niveau de sévérité, une explication en quatre parties (ce qu'elle détecte, pourquoi c'est suspect, les cas légitimes qui peuvent la déclencher à tort, et l'action recommandée), et sa configuration réelle (les seuils effectivement actifs — ex. « Fenêtre glissante : 5 min », « Seuil de requêtes : 600 » — reflétant les variables d'environnement en place, pas une description figée). Chaque règle a une case à cocher activer/désactiver : le changement est appliqué immédiatement par l'agent (aucun redémarrage), et persiste au reboot du conteneur. - Une section repliable « Comment le moteur traite ces règles » : agrégation par IP sur la fenêtre glissante, déclenchement par transition (une seule alerte par épisode continu, pas une rafale), et le fonctionnement précis de l'opt-out par vhost quand une même IP touche plusieurs vhosts dans la fenêtre (voir « Exclure un vhost de l'analyse, ou lui faire ignorer certaines règles » plus bas).
- La liste des règles personnalisées déjà définies (voir ci-dessous), avec leurs propres explications.
- Un éditeur YAML pour créer ou modifier ces règles personnalisées, avec validation immédiate et un bouton « Exemple » qui insère un modèle de départ.
Accessible en lecture avec VIEW_CONFIGS ; les actions qui modifient
quoi que ce soit (bascule d'une règle, sauvegarde du YAML) exigent la
permission DEPLOY.
La modale « Hôtes & directives »
Un second bouton, 📋 Hôtes & directives, ouvre une modale complémentaire
en lecture seule : un tableau, une ligne par bloc server{} de chaque
vhost sous sites/, avec l'état réel de chaque commentaire magique
# nginx-control-... — diagnostic actif/coupé, analyse activée/coupée,
règles ignorées pour ce vhost (avec leur nom, pas seulement leur numéro), et
monitoring (actif/inactif, intervalle, codes HTTP valides s'ils sont
surchargés). Jusqu'à cette modale, la seule façon de savoir si un vhost avait
une exception (analyze.enable=false, ignore-rules, diagnostic coupé...)
était d'ouvrir son fichier de configuration un par un.
Aucune configuration n'est modifiable depuis cette modale — ces réglages se changent toujours dans le fichier lui-même (page Configs/Éditeur) ; elle comble un angle mort de visibilité, sans devenir un second endroit où éditer la même chose.
Règles personnalisées au format YAML
Sur le même principe qu'un scénario CrowdSec, une règle personnalisée est
une entrée de la liste rules: :
rules:
- id: 101
name: wp_admin_probe
description: "Sonde répétée de /wp-admin sur un site qui n'est pas du WordPress"
severity: high
min_matches: 8
window_minutes: 5
path_hint: "wp-admin|wp-login"
status_in: [401, 403, 404]
enable: true
id(obligatoire) : entier ≥ 100 — les identifiants 1 à 6 sont réservés aux règles intégrées. Doit être unique parmi les règles personnalisées.name(obligatoire) : identifiant court, lettres/chiffres/_/-uniquement.description: texte libre, affiché comme explication dans la modale et dans les alertes générées.severity:low,mediumouhigh(défautmedium).min_matches(obligatoire) : nombre de correspondances, pour une même IP, à atteindre dans la fenêtre glissante pour déclencher l'alerte.scope(v12.60.0) :ip(défaut, comptage par IP) ouglobal— les correspondances de toutes les IP sont additionnées dans la fenêtre. Pour les campagnes distribuées (botnet résidentiel, une ou deux requêtes par IP) que le comptage par IP ne voit jamais. La règle se déclenche quand le total atteintmin_matcheset quemin_ipsIP distinctes (défaut 5) ont correspondu. Une seule alerte « campagne » par règle (v12.62.0, plus une par IP) : sa preuve contient le nombre d'IP et de requêtes, le débit, les chemins et user-agents les plus fréquents, la répartition des codes (ex. 444 = déjà refusé par nginx), 10 requêtes d'exemple et la liste des IP (jusqu'à 3000, copiable depuis l'interface). L'alerte est ré-émise si le nombre d'IP augmente de 50 % ou après une fenêtre complète. Exceptions et opt-out par vhost s'appliquent IP par IP avant le comptage ; les IP qui ne correspondent pas au motif (ex. runner en IP privée) ne sont jamais comptées. La blocklist du dashboard lit la liste d'IP de la campagne ; une campagne touchant un vhost « sans remédiation » n'alimente jamais la blocklist (les IP ne peuvent pas être départagées par vhost). Chaque campagne compte pour une alerte par IP : unblocklist_threshold> 1 n'est atteint qu'avec des ré-émissions. Exemple :scope: global,min_ips: 20,min_matches: 60,window_minutes: 10.min_ips(v12.60.0) : avecscope: global, nombre minimum d'IP distinctes (entier ≥ 1, défaut 5).blocklist_remediation_type(v12.63.0) :block(défaut, refus 403/444) ouchallenge(vérification navigateur, voir « Remédiation par challenge navigateur »).window_minutes: taille de la fenêtre glissante en minutes (défaut 5). Une fenêtre plus large que celle des règles intégrées élargit automatiquement la fenêtre de rétention interne de l'agent, pour ne jamais priver la règle de son historique.path_hint/ua_hint: expression régulière optionnelle appliquée respectivement au chemin de la requête et au user-agent.status_in/method_in: listes optionnelles de codes HTTP ou de méthodes à filtrer ([401, 403],["POST"], etc.).enable:true/false(défauttrue) — permet de garder une règle définie mais coupée, sans la supprimer du YAML.
Toute règle invalide (id hors plage, id dupliqué, regex invalide, champ
manquant, etc.) est rejetée à la sauvegarde avec un message précis, ligne
par ligne, sans jamais casser les autres règles déjà valides. Le YAML est
persisté par nginx-analyzer (base SQLite interne) et rechargé par le
moteur de détection dès la sauvegarde, immédiatement.
Formulaire, modèles et mode avancé (v12.61.0)
Bouton Règles de la page Analyse → fenêtre à quatre onglets : Règles intégrées, Mes règles
(formulaire : une carte par règle, phrase en langage clair, testeur de requête), Modèles par application
(Forgejo/Gitea, GitLab, WordPress, Nextcloud, user-agents, Web générique : « Ajouter » une règle ou un pack) et
Mode avancé (YAML). Le YAML reste le format de stockage : le formulaire le génère (POST /api/analyzer/rules/to-yaml valide et produit le texte, l'enregistrement passe par POST /api/analyzer/rules/custom, permission deploy). Enregistrer depuis le formulaire ne conserve pas les
commentaires écrits à la main en mode avancé. Ajouter un modèle : un pack dans lib/rule-templates.js.
Voir nginx-analyzer/lib/rules-yaml.js (analyse/validation du YAML) et
nginx-analyzer/lib/detect.js (intégration au moteur), avec leurs tests
associés nginx-analyzer/test/rules-yaml.test.js et
nginx-analyzer/test/detect.test.js.
Exclure un vhost de l'analyse, ou lui faire ignorer certaines règles
Sur le même principe que les flags # nginx-control-monitoring-*, deux
commentaires placés dans un bloc server{ ... } permettent de garder le
contrôle fin, vhost par vhost, sans toucher au code ni à la configuration
de l'agent :
server {
server_name exemple.com;
# Désactive complètement l'analyse de ce vhost (aucune règle,
# intégrée ou personnalisée, n'y sera jamais appliquée)
# nginx-control-analyze: off
# Ou, plus fin : laisser l'analyse active mais ignorer certaines
# règles précises sur ce vhost (ici : brute-force, scan et scraping)
# nginx-control-analyze-ignore-rules: 1, 2, 4
...
}
-
# nginx-control-analyze: off: désactive toute analyse (règles intégrées et personnalisées) pour ce vhost. Absent, ou mis àon, l'analyse reste active (comportement par défaut). -
# nginx-control-analyze-ignore-rules: 1, 2, 4: liste d'identifiants de règles (voir la numérotation ci-dessus) à ignorer spécifiquement sur ce vhost ; les autres règles restent actives normalement. Les deux flags sont indépendants et peuvent coexister avec les flags de monitoring/diagnostic déjà documentés plus haut.Cas d'une IP qui touche plusieurs vhosts dans la même fenêtre de détection (proxy partagé, CDN, un même client interrogeant deux façades apparentées) : l'ignore-rule ne s'applique que si tous les vhosts touchés par cette IP l'ont configuré — un seul vhost n'ayant pas opté pour l'ignore-rule suffit à garder l'alerte active, pour ne jamais masquer une attaque réelle visant ce vhost-là au prétexte qu'un autre a choisi de l'ignorer.
Fonctionnement interne : nginx-analyzer tourne dans un conteneur
séparé et n'a pas accès aux fichiers de configuration nginx. C'est donc le
dashboard qui relit les vhosts (nginx-dashboard/lib/vhost-targets.js),
calcule la table des exclusions par nom de vhost, et la pousse à l'agent
via POST /api/vhost-rules — au démarrage du dashboard, puis toutes les
60 secondes (même boucle que la remontée des alertes), de sorte qu'une
modification d'un commentaire est prise en compte en moins d'une minute
sans redémarrer quoi que ce soit. Voir
nginx-dashboard/test/analyzer-vhost-rules.test.js et le bloc de tests
« opt-out par vhost » dans nginx-analyzer/test/detect.test.js /
nginx-analyzer/test/e2e.test.js.
Centre de notification (cloche d'en-tête)
Un fil d'actualité partagé et modulable — placé dans l'en-tête, entre le sélecteur de langue et l'utilisateur courant — qui remonte tout ce qu'un opérateur devrait remarquer sans avoir à aller cliquer page par page :
- Certificat arrivant à expiration / déjà expiré
- Certificat obtenu ou renouvelé par Certbot / échec Certbot
- Cible sous monitoring continu qui tombe (et qui remonte)
- Comportement anormal détecté par l'analyseur de journaux
- Reload / redémarrage nginx planifié (succès ou échec)
Chaque entrée a un niveau (info / success / warning / error, badge de
couleur assorti) et un état lu/non lu ; un badge sur la cloche affiche le
nombre de non-lues. Rafraîchi par le même cycle de sondage que le reste du
dashboard (5 s), pas de flux SSE dédié pour rester cohérent avec le reste
du projet. Le fil est partagé entre toutes les sessions (même principe
que le journal d'événements) : pas d'état de lecture par utilisateur.
Accessible avec la permission VIEW_METRICS — accordée aux trois rôles,
y compris viewer : c'est l'état de lecture de l'opérateur sur un fil
partagé, pas une modification de l'infrastructure elle-même.
Modulaire : ajouter un nouveau type de notification
Aucune fonctionnalité n'a besoin d'importer ce module pour l'utiliser — un
require() direct suffit, exactement comme lib/events.js ou lib/notify.js
sont déjà appelés depuis n'importe quelle feature :
const { pushNotification } = require('../lib/notifications');
pushNotification({
type: 'mon_nouveau_type', // identifiant libre, aucun registre à mettre à jour ailleurs
level: 'warning', // 'info' | 'success' | 'warning' | 'error' (repli sur 'info' si invalide)
message: 'Texte affiché tel quel dans le dropdown',
data: { /* optionnel — informations structurées, non affichées mais renvoyées par l API */ },
});
Rien d'autre à faire : pas de setDeps(), pas de route à ajouter, pas de
modification du frontend — la nouvelle entrée apparaît dans la cloche au
prochain rafraîchissement (5 s au plus).
Différence avec les deux modules voisins (souvent utilisés en même temps pour le même événement, chacun sa propre décision d'appel) :
lib/notify.jsenvoie un email, avec ses propres règles d'activation par type (config/notifications.yml) — aucune notion de lu/non-lu.lib/events.jsest le journal d'audit général (toute action, y compris administrative), sans niveau de sévérité ni état de lecture, et déclenche les webhooks sortants.lib/notifications.js(ce module) est plus étroit : seulement ce qui mérite l'attention d'un opérateur, avec un niveau et un état lu/non-lu que la cloche d'en-tête affiche directement.
Stockage SQLite (node:sqlite, comme le reste du projet), sa propre base
notifications-center.db dans CONFIG_DIR, dégradation silencieuse vers un
tampon en mémoire (300 entrées) si node:sqlite est indisponible. Rétention
configurable via NOTIF_CENTER_RETENTION_DAYS (défaut 90 jours).
Schéma des vhosts (page Schéma)
Une vue graphique de ce que chaque vhost proxifie réellement. Aucune route
dédiée — c'est une seconde présentation de GET /api/backends (même donnée
que l'onglet Backends du panneau Diagnostic), en SVG dessiné à la main plutôt
qu'en liste (zéro dépendance, cohérent avec le reste du projet). Le même
rendu (renderVhostGraphSvg, agrandi) est repris tel quel dans l'onglet
Schéma du panneau Diagnostic, pour un seul vhost à la fois.
Pour chaque vhost : [VHOST] --chemin--> [CIBLE], en éventail quand une
location a plusieurs cibles (pool upstream{}, LB/HA) — chaque flèche colorée
selon le type de cible (mêmes couleurs que les badges DIRECT/UPSTREAM/DOCKER/
VARIABLE/NON RÉSOLU de la page Backends). Un vhost de redirection pure
(voir plus haut) n'a rien à schématiser et n'apparaît pas.
Le sélecteur en haut de page bascule entre tous les vhosts (schémas compacts empilés, pour un balayage rapide même avec un grand nombre de vhosts) et un vhost précis (même schéma, agrandi, plus lisible pour étudier une répartition de charge en détail).
Les boîtes (vhost et cibles) sont dimensionnées à partir du texte réel (police monospace, largeur calculée par caractère), plutôt qu'à une largeur fixe : un nom de domaine ou une cible un peu longs ne sont ni tronqués ni débordants (ce qui pouvait auparavant chevaucher le libellé du chemin sur la flèche). Chaque schéma garde sa taille naturelle et défile horizontalement au besoin (le conteneur qui l'entoure scrolle) plutôt que de rétrécir le texte jusqu'à le rendre illisible ; un survol affiche le texte complet en infobulle dans tous les cas.
Déploiement Git
Configuration
GIT_REPO_URL=https://github.com/user/nginx-configs.git
GIT_BRANCH=main
GIT_TOKEN=ghp_xxxxx
NGINX_IMAGE=forge.rdr-it.com/dockerfiles/nginx-reverse-proxy:1.30.1
HOST_LOGS=/containers/nginx-rproxy/logs
HOST_GOACCESS=/containers/nginx-rproxy/goaccess
Structure du dépôt attendue
nginx-configs/
├── conf/
├── sites/
├── snippets/
├── streams/
└── ssl/
Pipeline de déploiement
Pull Git → Test conteneur éphémère → Backup ZIP + Git → Copie configs → Reload nginx
Le test éphémère copie les certs Let's Encrypt pour éviter les erreurs sur les vhosts SSL.
Première utilisation
Cliquer "Initialize backup branch" dans la page Deployment (crée une branche orpheline backup sur le dépôt distant).
Déclencher depuis un pipeline CI/CD (Forgejo, GitHub, GitLab, ...)
Les mêmes actions que les boutons de la page Déploiement (Pull / Test /
Sauvegarde / Déploiement complet) sont déclenchables via l'API, avec une
authentification dédiée : un jeton de déploiement
(config/deploy-tokens.yml), à créer et gérer depuis Configurations →
Éditeur → Jetons de déploiement (CI/CD).
Contrairement au jeton API admin global (API_TOKEN), un jeton de
déploiement ne donne accès qu'aux cinq routes suivantes — jamais au
reste du dashboard, quoi qu'il arrive par ailleurs dans l'application :
POST /api/git/pull, POST /api/git/test, POST /api/git/deploy,
GET /api/git/status, POST /api/backups. Chaque jeton peut en plus
restreindre ses propres actions autorisées (actions: [test] pour un
pipeline de vérification qui ne doit jamais pouvoir déployer, par exemple).
# config/deploy-tokens.yml
tokens:
- name: forgejo-ci
token: "un-secret-aleatoire-d-au-moins-16-caracteres"
enable: true
actions: [pull, test, deploy, backup] # ou un sous-ensemble
Chaque appel répond par un code HTTP reflétant directement le résultat
(succès = 2xx, échec = non-2xx — nginx -t invalide donne 422, une erreur
serveur 500), suffisant pour qu'un curl en pipeline fasse échouer l'étape
sans logique supplémentaire :
curl -sS -f -X POST \
-H "Authorization: Bearer ${DEPLOY_TOKEN}" \
"${DASHBOARD_URL}/api/git/deploy"
Trois exemples complets et prêts à adapter (job de test limité + job de
déploiement complet, chacun avec son propre jeton scoped) sont fournis dans
examples/ci/ : github-actions-deploy.yml, gitlab-ci-deploy.yml,
forgejo-deploy.yml.
Sauvegardes
Les ZIP incluent sites/ conf/ snippets/ streams/ ssl/ et les certs Let's Encrypt (certs/live/).
| Mode | ZIP local | Push Git |
|---|---|---|
| Local uniquement | ✓ | — |
| Git uniquement | — | ✓ |
| Local + Git | ✓ | ✓ |
Rotation automatique configurable via BACKUP_KEEP (défaut : 20).
CrowdSec
Mode Prometheus (recommandé)
Métriques moteur complètes, distinction local vs CAPI :
# Générer une clé API (nécessaire pour la liste des IPs)
cscli bouncers add nginx-dashboard
CROWDSEC_PROMETHEUS_URL=http://crowdsec:6060
CROWDSEC_LOCAL_ONLY=true # masquer les décisions CAPI
# Optionnel — pour la liste des IPs bannies avec détails
CROWDSEC_URL=http://crowdsec:8080
CROWDSEC_API_KEY=votre-cle-api
Le mode Prometheus expose :
- Décisions actives par origine (local
crowdsecvs communautéCAPI) - Top scénarios déclenchés localement (
cs_alerts) - Sources de logs surveillées (
cs_filesource_hits_total) - Santé du parser (
cs_parser_hits_ok_total/cs_parser_hits_ko_total)
Filtre des décisions
La liste des décisions actives est filtrable par origine via les tabs All / crowdsec / CAPI.
L'onglet CrowdSec est masqué si aucune variable n'est configurée.
Bouncers connectés — fonctionnalité retirée (v12.42.0)
Une carte Bouncers connectés avait été ajoutée en v12.41.0, avec un
message « non disponible » — la LAPI de CrowdSec n'expose aucun endpoint
HTTP pour lister les bouncers ou machines enregistrés (cscli bouncers list lit directement la base locale de CrowdSec, en dehors de toute API
HTTP accessible au dashboard). Cette limitation étant permanente et non un
état transitoire, la carte a été retirée entièrement plutôt que de
justifier son absence en permanence. Pour consulter cette liste, cscli bouncers list (ou cscli machines list) directement sur l'hôte CrowdSec
reste le seul moyen.
Blocklists IP
Agrège une ou plusieurs listes d'IP à bloquer, publiées en clair sur une URL
(ex. https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/refs/heads/main/prod_data-shield_ipv4_blocklist.txt),
avec récupération périodique configurable, fusion/déduplication si plusieurs
sources, et deux snippets Nginx à inclure pour bloquer ces IP au niveau vhost.
Configuration (config/blocklists.yml)
enable: true
interval_cron: "0 */6 * * *" # syntaxe cron 5 champs, ex. toutes les 6h
block_action: deny_403 # deny_403 (HTTP 403) ou drop_444 (ferme la connexion)
sources:
- name: datashield
url: "https://raw.githubusercontent.com/duggytuxy/Data-Shield_IPv4_Blocklist/refs/heads/main/prod_data-shield_ipv4_blocklist.txt"
enable: true
- name: autre-liste
url: "https://exemple.com/liste-ip.txt"
enable: true
Éditable directement depuis Configurations → Éditeur → Blocklists IP, ou
en modifiant le fichier sur l'hôte (name limité à [A-Za-z0-9_-]+,
unique ; url obligatoirement http:// ou https://).
Source « analyzer » : liste générée depuis les règles d'Analyse
Une source peut, au lieu de récupérer une liste tierce par URL, être générée localement depuis les propres règles de détection de nginx-analyzer (bruteforce, scan, flood, scraping, écart volumétrique, règles personnalisées) — un mode « à la CrowdSec » fait maison.
sources:
- name: analyzer-auto
type: analyzer # au lieu de fournir une url
enable: true
Depuis la v12.50.0, le seuil de répétition, la fenêtre glissante et la décision de bloquer réellement (remédiation) se configurent au niveau de CHAQUE RÈGLE d'Analyse, pas sur cette source (retour utilisateur : « je vois ça au niveau des règles existantes (1-99) intégré et sur les règles perso (100+) »), pour ne jamais mélanger un flood très bruyant et un scraping plus tolérant sous le même seuil global :
- Règles intégrées (bruteforce/scan/flood/scraping/volumétrique/pays) : mini-formulaire dans la modale « Règles d'analyse » du dashboard — Seuil, Fenêtre (minutes), case « Bloquer réellement », Durée (minutes).
- Règles personnalisées (id ≥ 100, YAML) : quatre champs optionnels à côté des champs de détection existants :
rules:
- id: 101
name: admin_probe
# ... champs de détection habituels (min_matches, path_hint, ...) ...
blocklist_threshold: 5 # null/absent = cette règle ne participe jamais (défaut)
blocklist_window_minutes: 1440 # fenêtre glissante de comptage, en MINUTES (défaut 1440 = 24h, plafond 14 j)
blocklist_remediation: false # voir ci-dessous — false par défaut, volontairement
blocklist_remediation_minutes: null # voir "Durée de blocage" ci-dessous
blocklist_remediation: false par défaut, et ce délibérément, exactement
comme pour l'ensemble du mécanisme. Tant que cette valeur n'est pas
explicitement passée à true sur une règle, celle-ci alimente la détection
— visible sur la page Blocklists IP (compteur « détectées », détail par
règle) — mais ne bloque rien : elle ne rejoint jamais la table geo
appliquée par Nginx. Sur un déploiement déjà en production, activer la
remédiation d'une règle pour la première fois ne doit jamais bloquer une IP
par surprise — l'opérateur observe d'abord ce que la détection produirait
réellement (une notification apparaît dès qu'une nouvelle IP franchit le
seuil), et n'active blocklist_remediation: true qu'une fois confiant dans
le seuil choisi.
Durée de blocage (blocklist_remediation_minutes) : une fois la
remédiation appliquée sur une IP, elle reste bloquée ce nombre de minutes
(renouvelé tant qu'elle continue de déclencher la règle), indépendamment de
la fenêtre de comptage — utile pour éviter qu'une IP soit débloquée dès
qu'elle cesse un instant son activité. Laissé absent/null, l'IP reste
bloquée aussi longtemps qu'elle dépasse le seuil sur sa fenêtre glissante
(comportement historique).
Coexiste sans problème avec des sources url classiques dans le même
fichier : les deux types sont fusionnés et dédupliqués ensemble dans la même
table geo.
Liste blanche (whitelist:)
Filet de sécurité global, appliqué après la fusion de toutes les
sources (url ou analyzer) — une IP ou un bloc CIDR listé ici n'est
jamais écrit dans la table geo appliquée par Nginx, quelle que soit la
source qui l'aurait sinon inclus :
whitelist:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "203.0.113.10"
Cas d'usage explicite (retour utilisateur : « gérer une liste blanche par exemple par d'ip privée donc soit CIDR ou juste une ip ») : vos propres plages privées, pour qu'un faux positif de la détection « Blocklist à la CrowdSec » ne puisse jamais bloquer du trafic interne.
Opt-out de remédiation par vhost (garde les alertes, coupe le blocage)
Nouveau commentaire, dans le fichier de conf d'un vhost :
# nginx-control-analyze-no-remediation: on
Distinct de # nginx-control-analyze: off (qui coupe l'alerte elle-même) :
ce commentaire ne change rien à la détection — les alertes de ce vhost
restent visibles (page Analyse, /api/alerts) — il retire seulement les
occurrences de ce vhost du comptage utilisé par le mécanisme « Blocklist à
la CrowdSec » pour décider de bloquer une IP. Utile pour garder la
visibilité sur un vhost sensible (une sonde interne, un partenaire
turbulent) sans jamais le laisser déclencher un blocage automatique. Visible
dans la modale « Hôtes & directives » (colonne « Blocklist IP »), et pris en
compte par l'agent dans la minute qui suit une modification du fichier.
Test en conteneur éphémère (page NGX Ops, v12.55.0)
La carte « Tester en conteneur éphémère » lance un nginx jetable sur une copie de la configuration
active et affiche sa sortie complète sous la grille (code de sortie, image, résumé du montage,
sortie de nginx -t). C'est le même bac à sable que le test Git et que l'ajout/édition de configuration,
donc son verdict est celui qu'un déploiement obtiendrait ; le serveur en production n'est pas touché.
Contrairement à « Tester la config » (nginx -t dans le conteneur de production), il valide aussi les
montages et l'image utilisés par le pipeline. API : POST /api/nginx/test-ephemeral (permission
nginx_control) → { ok, valid, exitCode, image, output, durationMs }.
Ignorer des chemins pour une règle (paths-ignore, v12.54.0)
Un endpoint légitime qui répond 403 ou 404 à des visiteurs normaux (ex.
WordPress GET /wp-json/wpa/v1/verify-session, qui répond 403 tant que
l'utilisateur n'est pas connecté) peut faire monter une règle. Dans le bloc
server{} du vhost concerné :
# nginx-control-analyze-rule-1-paths-ignore: /wp-json/wpa/v1/verify-session,/autre/chemin
# nginx-control-analyze-rule-2-paths-ignore: /wp-content/*
{ID}est l'identifiant de la règle (modale « Règles ») : 1 brute force, 2 scan, 3 flood, 4 scraping, ou une règle personnalisée (≥ 100). Les règles 5 et 6 (volumétrie, pays) ne sont pas concernées.- Motif : chemin exact (la query string est ignorée), ou préfixe s'il se
termine par
*. Doit commencer par/; 50 motifs max par règle. - Portée : ce vhost et cette règle uniquement. Les autres règles continuent de voir ces requêtes. Plusieurs lignes pour une même règle s'additionnent. Pris en compte en moins d'une minute, pour le trafic suivant (pas rétroactif). Valable pour l'analyzer Node comme pour l'analyzer Go.
- Visible dans « Hôtes & directives ».
Remédiation par challenge navigateur (v12.63.0)
Plutôt que de refuser une IP suspecte (utile contre un botnet résidentiel, où des humains
se cachent parmi les adresses), une règle peut lui imposer une vérification navigateur :
blocklist_remediation_type: challenge (menu « Type de remédiation » du formulaire des règles
et des règles intégrées). Un navigateur normal passe en moins d'une seconde et reçoit un cookie ;
un robot qui n'exécute pas de JavaScript reste bloqué. Le blocage l'emporte : une IP présente
dans les deux listes est refusée.
Mise en place
challenge_enable: truedansconfig/blocklists.yml(autres cléschallenge_*documentées dans ce fichier).- Démarrer le service :
docker compose --profile challenge up -d nginx-challenge(définirNC_SECRETdans.env, ≥ 32 caractères, pour que les cookies survivent aux redémarrages). - Dans chaque vhost protégé, après
include snippets/blocklist-enforce.conf;:include snippets/challenge-gate.conf;(une seule fois parserver{}; le snippet est un fichier vide de directives tant que le challenge est désactivé, donc l'inclusion ne peut jamais cassernginx -t).
Fonctionnement : le dashboard génère conf/blocklist-challenge.conf (table $blocklist_challenge)
et snippets/challenge-gate.conf (mêmes garde-fous que la liste de blocage : nginx -t avant rechargement,
retour arrière en cas d'échec). Une requête d'une IP listée, sans cookie valide, est redirigée vers
/.nc-challenge/go<URL d'origine> (servi par le conteneur, sous le même vhost, donc cookie « first-party ») ;
après la preuve de travail, le navigateur est renvoyé vers l'URL d'origine. Les autres IP ne paient
qu'une sous-requête interne qui répond 204 immédiatement.
Cookie : nc_chal, HttpOnly; Secure; SameSite=Lax, signé HMAC-SHA-256, lié à l'IP (préfixe /64 en IPv6)
et au nom d'hôte, 24 h par défaut. Sans cookie, chaque requête serait re-challengée : il est indispensable.
Exemptions : challenge_exempt_path_regex et challenge_exempt_ua_regex (jamais soumis au challenge,
par défaut /.well-known/ pour Let's Encrypt). Les clients qui ne sont pas des navigateurs (git en HTTP,
API, webhooks, applications mobiles) ne savent pas résoudre un challenge : exemptez-les (ex. user-agent ^git/,
chemins /api/) ou gardez la remédiation « refus » pour ces règles.
Moteur Anubis (challenge_engine: anubis) : le snippet utilise le mode auth_request d'Anubis
(vérification /.within.website/x/cmd/anubis/api/check, redirection /.within.website/?redir=). Ajoutez le
service Anubis (exemple commenté dans docker-compose.yml) avec REDIRECT_DOMAINS listant vos vhosts et un
ED25519_PRIVATE_KEY_HEX fixe. Ce mode suit la documentation d'Anubis mais n'a pas été exercé ici contre un
Anubis réel : vérifiez-le sur un vhost de test, et adaptez challenge_upstream si besoin.
Limites à connaître
- Une preuve de travail filtre les robots simples ; elle n'arrête pas un attaquant qui pilote un vrai navigateur.
auth_requesteterror_page 401sont posés au niveauserver{}: unelocationqui définit ses propreserror_pagen'hérite pas de la redirection (la requête recevrait alors un 401 brut).- Si le conteneur est arrêté, seules les IP listées reçoivent une erreur 502 ; les autres visiteurs ne sont pas touchés.
- Challenge désactivé (
challenge_enable: false) : les IP « challenge » restent détectées et affichées, mais rien n'est appliqué dans nginx (ni refus, ni challenge). - Le service fait confiance à
X-Real-IP/Hostposés par nginx : ne publiez jamais son port. - Détails du service :
nginx-challenge/README.md.
Stats du challenge, apprentissage visible (v12.68.0)
- Challenge HTTP → Efficacité : compteurs agrégés (challengés, réussis, échecs, sans JavaScript, taux de réussite). Route
GET /api/challenge/stats?hours=24|168|720. Le dashboard doit joindre le conteneur sur le réseau Docker ; reconstruire l'imagenginx-challengepour disposer de/stats. - Analyse → « Voir ce qui est appris » : résumé par vhost/pays et grille 7 × 24 de la baseline. Routes
GET /api/analyzer/baseline/keys|profile?type=vhost|country. Les créneaux exploitables ne comptent que les cibles observées au moins 24 h.
Challenge sur une location, identifiant d'instance (v12.67.1)
server {
include snippets/challenge-location-support.conf; # une fois (ou ...-allowbots.conf)
location = /wp-login.php {
include snippets/challenge-location.conf; # auth_request + error_page
# proxy_pass / fastcgi_pass habituel
}
}
Ne pas combiner avec challenge-gate/all/allowbots ni un profil dans le même server{}.
INSTANCE_ID : UUID unique et persistant de l'installation (page Système), généré au premier démarrage, surchargeable par ENV.
Recherche globale, page Conteneurs (v12.67.0)
- Recherche globale : champ de l'en-tête (
Ctrl+Kou/) — pages, fichiers de configuration nginx, fichiers de l'éditeur Configuration. - Administration → Conteneurs (admin) : vue unique de nginx, NGX Ops et des conteneurs gérés (label
managed-by=nginx-dashboard) : uptime, CPU/mémoire, démarrer / arrêter / redémarrer / mettre à jour l'image / forcer le rebuild, logs en direct. Routes :GET /api/containers,GET /api/containers/logs,GET /api/containers/logs/stream(SSE),POST /api/containers/action. Le rebuild recrée un conteneur géré depuis soninspect(pull, renommage, création, démarrage, retour arrière si échec) ; nginx et le dashboard ne sont que redémarrables (ils vivent dans votredocker-compose.yml). Le dashboard ne peut pas s'arrêter lui-même. - Page de challenge :
NC_LANG=auto|fr|en.
Volume de données et maintenance SQLite (v12.74.0)
- Analyse → Volume de données : même principe que « requêtes » et « par pays », mais sur les octets envoyés par vhost et par heure (baseline par créneau de la semaine, médiane + MAD). Une exfiltration après une faille, un téléchargement massif ou un scraping de gros fichiers déclenche une alerte
data_volume. Les 3 cartes (requêtes / pays / données) sont sur une seule ligne ; « Voir ce qui est appris » affiche le profil en Ko/Mo/Go. Règle n°7 désactivable dans « Règles ». Limite : seul le trafic sortant est mesuré (le formatcombinedne contient pas$request_length). - Réglages
analyzer.yml:data_sigma_threshold(6),data_min_bytes(52428800 = 50 Mo/h),rule_data_volume_enable,db_compact_on_start. - Compactage SQLite (analyzer Node/Go +
events.db,monitoring.db,notifications-center.db) : au démarrage,VACUUMsi l'espace récupérable dépasseDB_COMPACT_MIN_FREE_MB(16) etDB_COMPACT_MIN_FREE_PCT(20) ;DB_COMPACT_ON_START=auto|always|off. Ensuite, checkpoint WAL +incremental_vacuumtoutes les 6 h. Prévoir temporairement l'espace disque libre égal à la taille de la base.
IP en échec et blocage automatique du challenge (v12.73.0)
Le conteneur builtin suit, par IP et en mémoire, les visiteurs qui n'arrivent jamais à passer le challenge (redirections
vers la vérification, solutions fausses/expirées, limitations) — typiquement des robots sans JavaScript, qui font chuter le
taux de réussite. La page Challenge HTTP affiche un tableau « IP qui échouent au challenge » (période, seuil, filtre
« jamais résolu »). Avec autoblock: true dans config/challenge.yml, une IP qui cumule autoblock_threshold échecs
(défaut 30) dans autoblock_window_minutes (60) sans jamais avoir résolu le défi est bloquée autoblock_hours (24) heures :
refus 403 immédiat par le conteneur, puis blocage nginx via une source de blocklist :
# config/blocklists.yml
sources:
- name: challenge-bans
type: challenge # IP bloquées automatiquement par le challenge intégré
action: deny_403 # ou drop_444 (pas challenge)
Une IP qui a résolu le challenge au moins une fois n'est jamais bloquée (NAT/réseaux partagés) ; la liste blanche de
blocklists.yml s'applique ; le bouton Débloquer lève un blocage. Les réglages sont lus à la (re)création du conteneur.
Bons robots : Googlebot (et variantes Google), Bingbot, Applebot, Yandex, Baidu sont reconnus par DNS inverse confirmé
(extensible : goodbots_extra), jamais comptés ni bloqués. Alertes vers NGX Ops Intelligence : include_challenge: true
dans intelligence.yml (désactivé par défaut ; type d'alerte challenge, voir .notes/prompt-intelligence-challenge.md).
Page « Challenge HTTP » (v12.65.0)
Menu Intégrations → Challenge HTTP : le challenge s'utilise sans liste de blocage. La page affiche l'état
(actif, moteur, conteneur, IP signalées), pilote le conteneur, montre si les fichiers nginx générés sont à jour et les
régénère d'un clic (Régénérer et recharger nginx : nginx -t, retour arrière en cas d'échec), liste les snippets à
copier et les réglages effectifs. Les réglages challenge_* (challenge_enable, challenge_engine, challenge_upstream,
challenge_resolver, challenge_exempt_*) se posent dans config/challenge.yml — éditable dans « Éditeur de
configuration » — et l'emportent sur ceux de blocklists.yml (toujours lus, par compatibilité). Usage typique sans
blocklist : challenge_enable: true, « Régénérer », puis include snippets/challenge-all.conf; dans le vhost voulu.
Profils par vhost, difficulté, codes ignorés (v12.66.0)
Profils par vhost : challenge_exempt_* sont globaux ; pour un réglage propre à un vhost, déclarez un profil dans
config/challenge.yml :
profiles:
- name: forgejo # minuscules, chiffres, _ (30 max)
mode: allbots # listed | all | allbots (défaut all)
exempt_ua_regex: '^forgejo-runner/' # absent = hérite du global
Le dashboard génère snippets/challenge-forgejo.conf (à inclure dans ce server{} à la place des snippets globaux) et ses
variables nginx dédiées. Retirer un profil encore inclus par un vhost fait échouer nginx -t (retour arrière automatique) :
retirez d'abord l'include.
User-Agent de NGX Ops : toutes ses requêtes sortantes (git HTTP(S), listes de blocage, webhooks, CrowdSec, hôtes distants,
agent nginx-agent…) envoient NGXOps, sans version (variable HTTP_USER_AGENT pour le changer). Pour les exempter du
challenge : challenge_exempt_ua_regex: '^NGXOps$'. Un user-agent est falsifiable : à réserver aux vhosts peu exposés.
Difficulté : difficulty_bits / NC_DIFFICULTY_BITS accepte 8 à 64. Chaque bit double le travail : ~16 est instantané,
~24 prend quelques secondes, ~28 plusieurs minutes, au-delà de ~30 un navigateur n'aboutit plus (équivalent à un blocage).
Analyse — codes HTTP ignorés : ignore_status: "444" dans config/analyzer.yml (variable DETECT_IGNORE_STATUS) exclut ces
codes de toutes les règles et de la baseline : le trafic déjà refusé par la blocklist (drop_444) ne déclenche plus d'alertes.
Il reste compté dans les statistiques et les hits de blocklist. Vide par défaut.
Challenge pour tous les visiteurs, bons robots, conteneur géré (v12.64.0)
Trois snippets (générés dans snippets/, un seul par server{}, à la place l'un de l'autre) :
| Snippet | Qui est challengé |
|---|---|
challenge-gate.conf |
seulement les IP de la liste « challenge » des règles d'analyse |
challenge-all.conf |
tous les visiteurs du vhost (hors chemins/user-agents exemptés) |
challenge-all-allowbots.conf |
tous sauf les bons robots d'indexation |
La page Blocklists → « Challenge navigateur » liste les trois avec un bouton Copier. Ils sont vides
tant que challenge_enable: false.
Bons robots : un user-agent est falsifiable. Googlebot, Bingbot et consorts ne passent donc que si la
DNS inverse confirmée (FCrDNS : IP → nom → même IP) correspond au domaine officiel. Liste étendue par
goodbots_extra (nom|regex-UA|.suffixe[,.suffixe][;nom2|…]) ; désactivable (goodbots: false). L'en-tête
interne X-NC-Allow-Bots est toujours écrasé par nginx. Limite : les robots sans DNS inverse
(DuckDuckBot, facebookexternalhit…) ne sont pas reconnus. Avec Anubis, les bons robots se règlent dans sa politique.
Démarrer le conteneur depuis NGX Ops (même modèle que error-pages / geoipupdate) : boutons
Démarrer / Arrêter / Mettre à jour l'image dans la carte « Challenge navigateur », recréation au démarrage
du dashboard si enable: true dans config/challenge.yml (exemple commenté fourni). Le conteneur est durci
(aucun port publié, capacités retirées, rootfs en lecture seule pour le moteur maison, mémoire bornée) et
rejoint le réseau Docker du dashboard. Le secret est généré et conservé (jamais passé en --build-arg).
Utile pour les services en Docker externe : plus besoin d'un profil compose dédié.
Priorité des réglages : variable d'environnement (NC_SECRET, NC_DIFFICULTY_BITS, NC_COOKIE_HOURS,
NC_GOODBOTS, NC_GOODBOTS_EXTRA) > config/challenge.yml > valeur par défaut. sample.env les liste
commentées avec leur défaut (les décommenter fige la valeur). NC_SECRET absent : généré au premier
démarrage et conservé (.generated-secrets.json pour le conteneur géré, volume nc-data pour le service compose),
comme SESSION_SECRET. Tous apparaissent dans la page Système.
Images par défaut (surchargeables à l'exécution) : ARG/ENV CHALLENGE_DEFAULT_IMAGE et
ANUBIS_DEFAULT_IMAGE dans nginx-dashboard/Dockerfile ; container_image de config/challenge.yml reste prioritaire.
Le conteneur nginx-challenge expose ses réglages en ARG→ENV (NC_DIFFICULTY_BITS, NC_COOKIE_HOURS,
NC_GOODBOTS, NC_GOODBOTS_EXTRA…), surchargeables au docker run.
Débloquer une IP de la liste analyzer (v12.54.0)
Page Blocklists IP → « Rechercher une IP » : si l'adresse est bloquée par la
source analyzer, choisir Remettre le compteur à zéro (l'IP n'est rebloquée
que par de nouvelles alertes franchissant le seuil) ou Débloquer et exempter
1 h / 24 h / 7 j (aucun re-blocage automatique pendant ce temps), puis
« Débloquer ». Le bouton « Débloquer… » des lignes « Ajoutée » de l'historique
y mène directement. Le retrait est appliqué dans nginx immédiatement et
inscrit dans l'historique (« débloquée manuellement »). API :
POST /api/blocklists/unblock { "ip": "203.0.113.5", "hours": 0 }
(permission deploy).
Inclusion dans Nginx
Deux fichiers sont générés, dans deux dossiers différents selon leur rôle :
conf/blocklist-ips.conf— la tablegeo $blocklist_ip { ... }, agrégée et dédupliquée à partir de toutes les sources actives. Elle doit être chargée une seule fois, au niveau http{} — c'est justement ce que fait déjànginx.confpour toutconf/viainclude conf.d/*.conf;. Aucune action requise : dès que la fonctionnalité est activée, la table est automatiquement prise en compte au prochain reload.snippets/blocklist-enforce.conf— le blocage effectif (if ($blocklist_ip) { return 403; }ou444selonblock_action). Comme tous les fichiers desnippets/, il n'est jamais chargé automatiquement — c'est un opt-in par vhost, à ajouter dans chaqueserver{}à protéger :
include snippets/blocklist-enforce.conf;
blocklist-enforce.conf apparaît dans l'explorateur de snippets existant ;
blocklist-ips.conf, dans conf/, se consulte comme les autres fichiers de
ce dossier depuis l'explorateur de configuration.
Compatibilité avec le déploiement Git — ces fichiers sont générés
localement et ne doivent jamais être versionnés dans le dépôt Git de
configuration. Le déploiement Git complet (page Déploiement, bouton
« Déployer ») synchronise conf/ et snippets/ en miroir avec le dépôt
(y compris les suppressions) ; ces fichiers en sont explicitement
exclus — ils survivent à tout déploiement Git et sont pris en compte tels
quels par le test nginx -t préalable, qu'ils soient ou non présents dans
le dépôt.
Mesure d'efficacité : journalisation des hits (hit_logging_*)
Deux clés supplémentaires dans config/blocklists.yml, désactivées par
défaut :
hit_logging_enable: false
hit_logging_method: dedicated # dedicated | approx
dedicated(recommandée) : ajoute, dansblocklist-enforce.conf, une instructionaccess_log /var/log/nginx/blocklist-hits.log blocklist_hits if=$blocklist_ip;(chemin tel que le conteneur nginx le voit — jamais leDIR_LOGSinterne au dashboard, qui n'existe pas dans le conteneur nginx et ferait échouernginx -t) indépendante du blocif ($blocklist_ip) { return ...; }existant — pas imbriquée dedans, et sans toucher àlogging.confni au format de log partagé. Le paramètreif=rend la ligne conditionnelle nativement, côté nginx. Sonlog_formatest déclaré une seule fois au niveau http dans un troisième fichier généré,conf/blocklist-hitlog-format.conf(même logique d'emplacement queblocklist-ips.conf), toujours présent même quandhit_logging_enableest àfalse— l'activer plus tard ne nécessite donc jamais de créer de fichier manquant. Résultat : un fichier de log global (logs/blocklist-hits.log), une ligne par requête bloquée, tous vhosts confondus (le vhost s'y trouve comme champ, pas dans le nom du fichier), lu automatiquement par nginx-analyzer.approx: aucun log dédié, aucun fichier supplémentaire — depuis la v12.29.0, nginx-analyzer dérive directement les hits du (ou des) log(s) d'accès qu'il suit déjà, sans qu'aucune configuration nginx supplémentaire ne soit nécessaire côté agent. Le dashboard lui pousse périodiquement (même cadence que le reste de la synchronisation dashboard → analyzer, environ toutes les 30 s) la liste des IP/CIDR de chaque source activée ; l'analyzer compare chaque requête de son log principal à cette liste. Fonctionne correctement, avec les mêmes statistiques (hits, IP distinctes, répartition par source) qu'en modededicated— le compromis est une détection décalée d'au plus un cycle de synchronisation après un changement de blocklist, plutôt qu'un fichier de log supplémentaire à gérer.
Le choix est entièrement à l'opérateur. En mode dedicated, nginx-analyzer
suit le fichier blocklist-hits.log automatiquement dès qu'il existe (voir
son propre README) ; en mode approx, il n'a besoin d'aucun fichier
supplémentaire, seulement de la synchronisation de sources décrite
ci-dessus, qui se met en place automatiquement dès que l'agent est
configuré et joignable — aucune configuration à dupliquer côté agent.
Recherche d'IP dans les blocklists
GET /api/blocklists/check?ip=... (et le champ de recherche sur la page
Blocklists IP) indique si une adresse est bloquée et dans laquelle ou
lesquelles des sources configurées — une IP peut légitimement figurer
dans plusieurs listes.
Télécharger/visualiser une liste (GET /api/blocklists/export)
Retour utilisateur : « pour la blocklist basé sur les regle analyse, serait il possible d'avoir une url pour la télécharger et la visualiser ? ». Chaque ligne de la table « Sources configurées » (page Blocklists IP) porte désormais une colonne Export avec deux liens :
- Voir — ouvre
GET /api/blocklists/export?source=<nom>&format=txtdans un nouvel onglet : le navigateur affiche la liste brute (une IP/CIDR par ligne, triée, dédupliquée) directement. - Télécharger — le même lien avec
&download=1, qui force l'enregistrement (Content-Disposition: attachment) plutôt que l'affichage.
Pour une source type: analyzer ayant des IP détectées, un troisième lien
Détectées ajoute &detected=1 : expose toutes les IP ayant franchi le
seuil d'une règle, y compris celles dont la règle n'a pas encore
remediation: true — utile pour voir ce que la détection repère avant
d'activer un blocage effectif.
?source= accepte aussi all (par défaut) pour la liste fusionnée de
toutes les sources activées — le même ensemble que la table geo générée.
Sans format=txt, la réponse est structurée en JSON
({source, type, count, generatedAt, ips}). Route en lecture seule
(permission VIEW_CONFIGS, comme le reste de la page), ne fait aucun appel
réseau (lit le cache déjà peuplé), et accepte aussi bien la session du
navigateur que le jeton API global (Authorization: Bearer <API_TOKEN>) —
la même URL peut donc être collée dans un outil externe (curl, cron, une
autre instance nginx-dashboard configurée avec une source type: url
pointant ici).
Sécurité et fiabilité
- Aucune injection de configuration possible : chaque ligne récupérée est validée par une regex entièrement ancrée qui n'accepte qu'une IPv4, une IPv6, ou l'un ou l'autre avec un suffixe CIDR — tout le reste (commentaires, en-têtes, contenu inattendu ou malveillant) est rejeté intégralement, jamais tronqué ou partiellement accepté.
- Une source en panne ne fait jamais disparaître une protection : si une URL cesse de répondre (réseau, 404, changement de contenu), les IP précédemment récupérées pour cette source restent actives dans le blocage jusqu'à son prochain succès.
- Rechargement Nginx toujours testé avant application :
nginx -test exécuté sur la configuration générée avantnginx -s reload; en cas d'échec, les fichiers précédents sont immédiatement restaurés et Nginx n'est jamais rechargé sur une configuration invalide. Un reload n'est de toute façon déclenché que si la table d'IP ou l'action de blocage a réellement changé. - Sécurisé dès le tout premier démarrage : les deux fichiers (
conf/blocklist-ips.confetsnippets/blocklist-enforce.conf) sont créés de façon synchrone (table vide mais valide) avant même la première récupération réseau — un vhost qui inclut déjàblocklist-enforce.confne peut donc jamais faire échouernginx -tsimplement parce que le fichier n'existe pas encore.
Page « Blocklists IP »
État de chaque source (dernier succès/erreur, nombre d'IP, liens Export
pour voir/télécharger sa liste — voir ci-dessus), nombre total d'IP uniques
bloquées, configuration courante, chemins/tailles des fichiers générés, un
bouton Rafraîchir maintenant, une recherche d'IP (dans quelle(s)
liste(s) une adresse se trouve), et — si hit_logging_enable est actif —
un bloc Efficacité (hits) : nombre de hits et d'IP distinctes sur 24h,
répartis par blocklist source.
Digest
Quand la fonctionnalité est active, le résumé périodique (email +
/api/digest/*) affiche le nombre total d'IP bloquées (toujours
disponible) et, si hit_logging_enable est actif, le nombre de hits, d'IP
distinctes et la répartition par source sur la période du digest.
API
GET /api/blocklists/status— état courant (permissionVIEW_CONFIGS)POST /api/blocklists/refresh— rafraîchissement manuel immédiat (permissionDEPLOY)GET /api/blocklists/check?ip=...— dans quelle(s) liste(s) une IP se trouve (permissionVIEW_CONFIGS)GET /api/blocklists/hit-stats?hours=24— hits, IP distinctes, répartition par source (permissionVIEW_CONFIGS)GET /api/blocklists/export— télécharger/visualiser une liste (permissionVIEW_CONFIGS) :?source=<nom>(omis ouall= fusion de toutes les sources activées, l'ensemble exact écrit dans la tablegeo)?format=txt(une IP/CIDR par ligne,text/plain; sansformat: JSON structuré{source, type, count, generatedAt, ips})&download=1force le téléchargement (Content-Disposition: attachment) ; sans ce paramètre, le navigateur affiche la liste directement (page "Blocklists IP" → colonne « Export » de chaque source)&detected=1(sourceanalyzeruniquement) expose les IP détectées même sansremediation:true, plutôt que la liste effectivement bloquée- Accepte la session du navigateur ou le jeton API global (
Authorization: Bearer <API_TOKEN>), donc réutilisable depuis un outil externe (curl, une autre instance nginx-dashboard en sourcetype: url)
Auto-config Docker (labels façon Traefik)
Publie automatiquement un conteneur comme vhost Nginx à partir de ses propres labels Docker, à la manière de Traefik — sans passer par l'éditeur de configuration. Détection par sondage, test avant reload, rollback automatique en cas d'échec.
- Étape 1 (« socle ») : vhost HTTP simple (une ou plusieurs locations), resolver Docker.
- Étape 2 : modes SSL (
none/snippet/auto),http_to_https_auto, snippets par vhost (niveau serveur et niveau location), labels monitoring/diagnostic/analyze (mêmes commentaires magiques que ceux déjà compris par la page Diagnostic etfeatures/monitor.js— un vhost généré automatiquement se pilote exactement comme un vhost écrit à la main). upstream_group: plusieurs conteneurs (répliques d'un même service) partageant unserver_namesont agrégés en un seul vhost avec un blocupstream {}, au lieu d'être traités comme un conflit.- Détection réactive (Docker Events) : en plus du sondage périodique
(toujours actif, filet de sécurité), un conteneur qui démarre/s'arrête
déclenche un cycle en quelques secondes au lieu d'attendre le prochain
sondage — avec anti-rebond pour ne jamais déclencher un reload par
conteneur sur un
docker compose upd'une stack entière. ssl_certificate=certbot_http/certbot_dns: mode SSL basé sur Certbot/Certbot-DNS, avec vérification que le Certbot concerné est bien configuré et activé avant de s'appuyer dessus, et déclenchement actif d'une émission Certbot/Certbot-DNS si aucun certificat n'existe encore (voir plus bas).- Page dédiée (« Auto-config Docker » dans le menu) : réglages actifs, liste des conteneurs labellisés avec leur statut (badge coloré), approbation/rejet en un clic, aperçu en lecture seule du vhost généré, et bouton « Forcer un rescan » — plus besoin de lire les routes API en JSON brut pour savoir ce que le watcher a détecté.
- Suite : la gestion d'hôtes Docker distants (agent) est une étape ultérieure séparée, non couverte ici.
Labels reconnus
nginx-control.enable=true
nginx-control.network=<reseau-docker-partage-avec-nginx>
nginx-control.vhost.server_name=app.example.com
nginx-control.vhost.listen=80
nginx-control.vhost.location01=/
nginx-control.vhost.location01.proxy_pass=http://backend:8080
nginx-control.vhost.location02=/api
nginx-control.vhost.location02.proxy_pass=http://api:8080
Piège fréquent avec
docker-compose.yml— sous la forme liste, chaque label s'écrit"CLE=VALEUR"(signe=), jamais"CLE: VALEUR"(signe:, syntaxe YAML de mapping). Avec un:, Docker Compose ne voit aucune erreur mais le conteneur reçoit un label dont le NOM est la chaîne entière ("nginx-control.enable: true") et dont la VALEUR est vide — le dashboard ne verra jamaisnginx-control.enable, et rien ne remontera dans la page Auto-config Docker, sans message d'erreur nulle part. Deux écritures valides :labels: - "nginx-control.enable=true" # forme liste : "=" - "nginx-control.vhost.server_name=app.example.com" # OU, forme mapping ("truetype" doit alors être une chaine) : labels: nginx-control.enable: "true" nginx-control.vhost.server_name: "app.example.com"
server_nameaccepte plusieurs noms séparés par un espace ou une virgule.location<NN>/location<NN>.proxy_pass(oulocation<NN>.target, alias identique — pratique si vous réutilisez les mêmes labels que surnginx-agent, voir « Hôtes Docker distants » plus bas,.targetétant prioritaire si les deux sont posés) :NNest un numéro à deux chiffres ou plus (01,02, ...), une paire par location — au moins une requise.listen: port d'écoute. Si absent, le défaut dépend dessl_certificate:80en HTTP simple,443dès que du SSL est demandé (snippetouauto) — un label explicite l'emporte toujours sur ce défaut.network: requis, doit être un réseau réellement attaché au conteneur nginx (celui que le dashboard pilote), sinon le label est rejeté explicitement (jamais un vhost silencieusement inatteignable). Sans ce label,nginx-control.enable=trueseul ne suffit pas — rien ne remontera non plus.
Labels étape 2 (SSL, snippets, monitoring/diagnostic/analyze)
nginx-control.vhost.ssl_certificate=snippet # none (defaut) | snippet | auto | certbot_http | certbot_dns
nginx-control.vhost.ssl_certificate.snippet=ssl-wildcard.conf
nginx-control.vhost.http_to_https_auto=true
nginx-control.vhost.server.snippet01=security-headers.conf
nginx-control.vhost.location01.snippet01=cache-static.conf
nginx-control.vhost.monitor.enable=true
nginx-control.vhost.monitor.interval=60s
nginx-control.vhost.monitor.valid_http_code=2xx, 3xx
nginx-control.vhost.location01.monitor.ignore=true
nginx-control.vhost.diagnostic.enable=false
nginx-control.vhost.analyze.enable=false
nginx-control.vhost.analyze.ignore_rules=1, 2, 4
ssl_certificate=none(défaut) : vhost HTTP simple, comme à l'étape 1.ssl_certificate=snippet: nécessitessl_certificate.snippet, le nom d'un fichier existant danssnippets/(rejeté si absent ou introuvable) — inclus tel quel (include snippets/<fichier>;), même mécanisme que le générateur de vhost manuel.ssl_certificate=auto: le vhost cherche un certificat Let's Encrypt déjà émis pour leserver_name(correspondance exacte ou wildcard, même logique que/api/vhost/ssl-sources). Si aucun certificat ne correspond encore, le vhost reste en HTTP simple avec une note dans le fichier généré (# ... aucun certificat correspondant n'a encore ete trouve) — jamais de blocssl_certificatepointant vers un fichier qui n'existe pas ; il bascule automatiquement en HTTPS au cycle suivant dès qu'un certificat apparaît.ssl_certificate=certbot_http/ssl_certificate=certbot_dns: commeauto, mais le vhost s'appuie sur les certificats gérés par Certbot (défi HTTP-01) ou Certbot-DNS (défi DNS-01) plutôt que sur n'importe quel certificat déjà présent. Précondition vérifiée à chaque cycle : Certbot (config/certbot.yml) ou Certbot-DNS (config/certbot-dns.yml) doit être configuré et activé (enable: true) — sinon le vhost reste en HTTP simple avec une erreur explicite dans le fichier généré (# ERREUR ssl_certificate=certbot_http : Certbot n'est pas active...) et dans le statut de l'API (GET /api/docker-autoconfig/status→status: "ssl_error",ssl.error.code/ssl.error.message) — jamais un vhost qui reste silencieusement en attente sans qu'on sache pourquoi. Une fois Certbot actif :- Un certificat déjà émis (à la main depuis la page Certbot/Certbot-DNS,
ou par le renouvellement planifié) est détecté et utilisé
automatiquement, exactement comme
auto. - Sans certificat existant, une émission est déclenchée automatiquement
(
certbot certonly ...via le conteneur Certbot/Certbot-DNS déjà configuré) — le vhost reste en HTTP simple pendant ce temps (pending) et bascule en HTTPS dès que le certificat apparaît, sans action manuelle. L'émission tourne en tâche de fond (jusqu'à ~2 minutes) et ne bloque jamais l'application des autres vhosts. - Jamais deux émissions concurrentes pour le même vhost, et jamais de
boucle de nouvelles tentatives après un échec (identifiants invalides,
DNS non propagé, limite de débit Let's Encrypt...) : un délai minimum
avant de retenter est appliqué, réglable via
certbot_retry_minutesdansconfig/docker-autoconfig.yml(défaut 15 minutes). Pendant ce délai, l'échec réel (message Certbot + nombre de tentatives) est visible dans le fichier généré et dansGET /api/docker-autoconfig/status(status: "ssl_error",ssl.error.code: "certbot_issuance_failed") au lieu d'unpendingsilencieux indéfini.
- Un certificat déjà émis (à la main depuis la page Certbot/Certbot-DNS,
ou par le renouvellement planifié) est détecté et utilisé
automatiquement, exactement comme
http_to_https_auto=true: ajoute un second blocserver{}sur le port 80 qui redirige (301) vers HTTPS — uniquement une fois le certificat effectivement résolu (jamais de redirection vers un HTTPS qui n'existe pas encore en modeautoen attente).server.snippet<NN>/location<NN>.snippet<MM>:include snippets/<fichier>;au niveau du bloc serveur ou d'une location spécifique — mêmes contraintes de nom de fichier quessl_certificate.snippet.monitor.enable,monitor.interval,monitor.valid_http_code,location<NN>.monitor.ignore,diagnostic.enable,analyze.enable,analyze.ignore_rules: écrivent exactement les commentaires magiques déjà documentés plus haut (page Diagnostic / Analyse des logs / Monitoring —# nginx-control-monitoring: on,# nginx-control-diagnostic: off, etc.). Le monitoring reste opt-in (absent = désactivé) ; le diagnostic et l'analyse restent opt-out (absent = activé), exactement comme pour un vhost écrit à la main.
upstream_group — plusieurs conteneurs pour un même service
nginx-control.vhost.location01.upstream_group=api-pool
Quand plusieurs conteneurs posent le même server_name — des
répliques d'un même service — ce n'est normalement jamais résolu en silence
(voir la sécurité plus bas) : c'est traité comme un conflit. Sauf si chacun
d'eux nomme le même upstream_group sur la même location, auquel
cas ils sont agrégés en un seul vhost :
upstream api-pool {
server app1:8080;
server app2:8080;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://api-pool;
}
}
L'agrégation ne se fait que si tout correspond entre les conteneurs
candidats : même réseau Docker, même port d'écoute, même configuration SSL,
mêmes chemins de location, et le même nom de groupe (même schéma
http/https) sur chacun d'eux. Le moindre écart (un conteneur sans
upstream_group, deux noms de groupe différents, des schémas mélangés,
des locations qui ne correspondent pas...) fait retomber sur le
comportement normal : conflit de server_name, aucun des deux appliqué —
jamais une fusion devinée à moitié.
Exemple minimal dans un docker-compose.yml d'application :
services:
mon-app:
image: mon-app:latest
networks: [nginx-net]
labels:
nginx-control.enable: "true"
nginx-control.network: "nginx-net"
nginx-control.vhost.server_name: "app.example.com"
nginx-control.vhost.listen: "80"
nginx-control.vhost.location01: "/"
nginx-control.vhost.location01.proxy_pass: "http://mon-app:8080"
networks:
nginx-net:
external: true
Fichier généré
Chaque conteneur labellisé produit sites/docker_<server_name>.conf,
régénéré à chaque cycle de détection — même convention que le générateur de
vhost manuel (include snippets/<fichier>;, le chemin tel que nginx le
voit dans son propre conteneur, jamais un chemin interne au dashboard) :
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
resolver 127.0.0.11 valid=30s;
set $backend01 "http://mon-app:8080";
proxy_pass $backend01;
}
}
Toute modification manuelle de ce fichier est perdue au prochain cycle — pour changer sa configuration, modifier les labels du conteneur puis le recréer/mettre à jour.
Sécurité — approbation par défaut (config/docker-autoconfig.yml)
Un label Docker peut être posé par quiconque a le droit de lancer un conteneur sur l'hôte : traité avec la même méfiance qu'une liste IP externe (voir Blocklists IP ci-dessus).
enable: false # desactive par defaut (v12.42.0) — opt-in, voir l encadre ci-dessous
require_approval: true # actif par defaut — choix delibere de securite
poll_interval_sec: 15 # filet de securite, toujours actif
events_enable: true # detection reactive (Docker Events), en plus du sondage
events_debounce_ms: 3000 # regroupe une rafale de conteneurs en un seul cycle
certbot_retry_minutes: 15 # delai avant de retenter une emission certbot_http/certbot_dns apres un echec
allowed_server_name_patterns:
- "*.apps.example.com"
Désactivé par défaut (v12.42.0) : cette fonctionnalité surveille le socket Docker et peut transformer les labels d'un conteneur en vhost publié automatiquement — un comportement qu'une instance qui ne l'utilise pas ne devrait pas avoir activé implicitement. Mettre
enable: truedansconfig/docker-autoconfig.ymlpour l'activer explicitement (une installation existante qui en dépendait déjà doit ajouter cette ligne après une mise à jour vers v12.42.0).
enable: falsemasque aussi l'item de menu (v12.41.0) : une instance qui ne se sert pas de l'auto-config Docker n'a plus besoin d'afficher une page qui ne fait plus rien — l'item Auto-config Docker du menu se masque automatiquement dans ce cas (voirMENU_DOCKER_AUTOCONFIGdans la section « Interface — thème, en-tête personnalisé, menu » pour forcer son affichage malgré tout).
- Chaque valeur de label est validée par une regex entièrement ancrée avant
d'être écrite dans un fichier nginx (
server_name, port, chemin de location, cibleproxy_pass) — jamais de concaténation directe d'une valeur brute dans le fichier généré. - Conflit de
server_name(deux conteneurs, ou un conteneur contre un vhost écrit à la main) : jamais résolu en silence — aucun des deux n'est généré/appliqué tant que le conflit existe. require_approval: true(par défaut) : unserver_namequi ne correspond à aucun motif deallowed_server_name_patterns(wildcard*accepté) reste en attente d'approbation tant qu'un opérateur ne l'approuve pas explicitement — jamais appliqué automatiquement à partir d'un simple label posé sur un conteneur.- Test avant reload, comme partout ailleurs dans ce projet :
nginx -test exécuté avant toutnginx -s reload; en cas d'échec, les fichiers de ce cycle sont immédiatement restaurés à leur état précédent. - Compatibilité avec le déploiement Git : les fichiers
docker_*.confne sont jamais versionnés ni supprimés par le déploiement Git complet — protection par préfixe de nom de fichier, en plus (et indépendamment) de la liste des fichiers générés déjà utilisée pour Blocklists IP.
Décisions enregistrées : visibilité, pause et révocation
Une approbation (ou un rejet) est mémorisée indépendamment du cycle de vie
du conteneur, dans docker_autoconfig_state — elle est indexée par
server_name, pas par ID de conteneur. Avant la v12.35.0, cette persistance
n'avait aucune vue ni aucun contrôle associés : un conteneur arrêté
disparaissait purement et simplement de la page (le statut ne liste que les
conteneurs actifs), sans qu'il soit possible de voir, ni a fortiori de
révoquer, une approbation restée en place.
La page Auto-config Docker affiche maintenant un tableau « Décisions
enregistrées », distinct de la liste des conteneurs actifs, qui montre
toutes les décisions connues (approuvées ou rejetées), y compris celles
dont le conteneur est actuellement arrêté ou détruit — avec pour chacune :
les server_name concernés, la décision, si un conteneur vivant y correspond
actuellement, la date/l'auteur de la décision, et les actions disponibles.
Deux actions y sont proposées (permission DEPLOY), en plus de celles déjà
disponibles sur la liste des conteneurs actifs :
- Mettre en pause / Reprendre — une approbation mise en pause reste en mémoire (rien n'est perdu), mais le vhost généré est retiré tant que la pause est active ; la page affiche un message expliquant que la publication est en pause, depuis quand et par qui. Reprendre republie immédiatement, sans nouvelle approbation à donner.
- Révoquer l'approbation — supprime définitivement la décision
mémorisée (confirmation demandée avant toute suppression) ; si le
conteneur est toujours actif au prochain cycle, il repasse par le circuit
normal (
require_approval, etc.) comme s'il apparaissait pour la première fois.
Portée actuelle : auto-config Docker locale uniquement. Ce tableau et ces actions ne couvrent que les conteneurs de l'hôte local (
lib/docker-autoconfig.js). Les hôtes Docker distants (agents, section suivante) n'ont aujourd'hui qu'une approbation au niveau de l'hôte entier (lib/agents-store.js), pas par vhost individuel — il n'y a donc pas encore d'équivalent « pause d'une publication » ni de tableau de décisions par vhost côté agents distants. Une parité complète nécessiterait un magasin de décisions par vhost dédié aux agents, à concevoir séparément.
API
GET /api/docker-autoconfig/status— conteneurs détectés, leur statut (applied/pending_approval/rejected/invalid/paused), fichiers générés, et la listedecisions(toutes les décisions mémorisées, y compris sans conteneur vivant) (permissionVIEW_CONFIGS)POST /api/docker-autoconfig/rescan— force un cycle de détection immédiat (permissionDEPLOY)POST /api/docker-autoconfig/approve—{ "serverName": "..." }, approuve et applique (permissionDEPLOY)POST /api/docker-autoconfig/reject—{ "serverName": "..." }, rejette (permissionDEPLOY)POST /api/docker-autoconfig/pause—{ "serverName": "..." }, met en pause une approbation existante sans la perdre (permissionDEPLOY)POST /api/docker-autoconfig/resume—{ "serverName": "..." }, reprend une publication en pause (permissionDEPLOY)POST /api/docker-autoconfig/decisions/remove—{ "serverName": "..." }, révoque définitivement une décision mémorisée, avec ou sans conteneur vivant (permissionDEPLOY)
Éditable directement depuis Configurations → Éditeur → Auto-config Docker, ou en modifiant le fichier sur l'hôte.
Hôtes Docker distants (agents)
Partie 2 du document de conception : contrairement à l'auto-config Docker ci-dessus (qui ne voit que les conteneurs du même hôte, via le socket Docker local), un hôte distant peut annoncer ses vhosts au dashboard via un agent qui pousse un manifeste JSON par HTTPS — sans jamais donner à ce distant d'accès au socket Docker local ni aux fichiers du dashboard.
Cycle de vie d'un agent
pending ──approve──> approved ──revoke──> revoked
│
└──reject──> rejected
pending: créé parPOST /api/agent/enroll(seule route publique de cette fonctionnalité, désactivable viaconfig/agents.yml) — l'agent s'annonce avec un nom d'hôte proposé et, optionnellement, une empreinte libre (numéro de série, hash de clé publique, etc., affichée telle quelle pour aider un opérateur à vérifier). Aucun vhost n'est jamais généré à ce stade.approved: décision d'un opérateur, depuis la page Hôtes distants. Génère un jetonagt_<48 caractères hex>affiché une seule fois dans l'interface (comme un mot de passe) — seul son hash SHA-256 est conservé côté serveur. Un jeton perdu s'obtient par « Régénérer le jeton », qui invalide l'ancien.revoked: coupe le jeton et supprime tous les vhosts publiés par cet agent (avec le même pipeline test-avant-reload que partout ailleurs dans ce projet).rejected: l'agent reste visible (traçabilité) mais n'a jamais eu de jeton.
Sécurité du jeton
- Confinement strict : ce jeton n'authentifie que
POST /api/agent/manifest— rien d'autre. Toute autre route (y comprisGET /api/agents, qui liste tous les agents) répond 401 avec un jeton d'agent. - Comparaison en temps constant (
crypto.timingSafeEqual) sur l'ensemble du registre, sans sortie anticipée — même discipline que les jetons de déploiement CI/CD. - Le rôle
agentne porte aucune permission (PERMS) : sa portée est entièrement définie par la liste blanche de routes côtéserver.js, pas par le système de permissions habituel. - Le manifeste est poussé en HTTPS (TLS porté par nginx lui-même en frontal du dashboard) ; le jeton Bearer est la seule authentification de cette V1 — une authentification mutuelle (mTLS) est une extension possible, pas encore implémentée.
Manifeste JSON (POST /api/agent/manifest, Bearer token de l'agent)
{
"vhosts": [
{
"serverName": "app.distant.example.com",
"listen": 443,
"sslCertificate": "auto",
"httpToHttpsAuto": true,
"serverSnippets": ["security-headers.conf"],
"locations": [
{
"path": "/",
"target": "http://10.20.0.5:8080",
"snippets": ["proxy-common.conf"],
"monitorIgnore": false
}
],
"monitor": { "enable": true, "interval": "30s", "validHttpCode": "200,301,302" },
"diagnostic": { "enable": true },
"analyze": { "enable": true, "ignoreRules": [12] }
}
]
}
sslCertificate:none/snippet(avecsslCertificateSnippet, un fichier desnippets/) /auto(utilise un certificat déjà émis s'il en trouve un correspondant, sinon reste en HTTP simple en attendant — jamais un vhost SSL cassé).certbot_http/certbot_dnsne sont pas encore pris en charge pour un agent distant — scope volontairement restreint de cette première livraison (Partie 1 les a déjà pour les conteneurs locaux ; l'extension aux agents est un travail futur qui n'impose aucun changement de forme au manifeste).locations[].target: URL directescheme://host:portque l'hôte distant peut déjà joindre lui-même — jamais un nom de conteneur. Contrairement à la Partie 1 (même hôte Docker que nginx), le tour de passe-passeresolver 127.0.0.11 / set $backendNNn'a ici aucun sens : la cible est une adresse stable fournie à chaque manifeste, pas un nom résolu par le DNS interne de Docker.- Chaque champ est validé par une regex entièrement ancrée avant d'être écrit dans un fichier nginx — mêmes principes que l'auto-config Docker ci-dessus. Une entrée de vhost invalide dans un manifeste ne rejette que cette entrée-là ; les autres vhosts du même manifeste sont appliqués normalement.
- Conflit de
server_namevérifié contre les vhosts manuels, ceux de la Partie 1 (docker_*.conf) et ceux de tout autre agent — jamais résolu en silence. - Fichier généré :
sites/agent_<id>_<serverName>.conf, régénéré à chaque manifeste reçu (nginx -tpuisnginx -s reload, avec restauration complète en cas d'échec du test) ; protégé du déploiement Git complet par la même liste de préfixes (GENERATED_FILE_PREFIXES) que les fichiers de la Partie 1.
Versionnement du protocole
Le manifeste porte un protocolVersion optionnel — absent, il est traité
comme 1 (tous les manifestes v12.18.0, qui n'avaient pas ce champ,
restent valides tels quels). Une version que ce dashboard ne sait pas
encore traiter est rejetée en bloc, au niveau de l'enveloppe entière, avec
la liste des versions connues dans le message d'erreur — jamais une
tentative d'interpréter au mieux un format qui a changé de sens.
Métriques hôte
Un manifeste peut inclure un bloc metrics optionnel :
{ "vhosts": [ /* ... */ ],
"metrics": { "cpuPercent": 12.4, "memPercent": 38.2, "memTotalMb": 8192,
"uptimeSec": 431200, "netRxBytesPerSec": 15300, "netTxBytesPerSec": 4200 } }
Tous les champs sont facultatifs et bornés (une valeur hors bornes est simplement ignorée, jamais une erreur qui ferait échouer le manifeste pour un souci d'observabilité) — affichés sur la page Hôtes distants, à côté du statut habituel de l'agent.
Mode 3 — tunnel NAT sortant ("mode": "tunnel")
Un vhost du manifeste peut déclarer "mode": "tunnel" (au lieu de
"direct", la valeur par défaut). L'agent ouvre alors lui-même une
connexion WebSocket sortante vers ce dashboard
(GET /api/agent/tunnel, authentifiée par le même jeton Bearer) et y
reste connecté ; le vhost généré côté nginx fait un simple proxy_pass
vers ce dashboard lui-même (config/agents.yml#tunnel_target, même réseau
Docker que nginx), qui route ensuite chaque requête reçue vers le bon
agent par son en-tête Host, la relaie sur le tunnel, et renvoie la
réponse reçue en retour. Un hôte distant derrière NAT, sans IP publique
ni port entrant ouvert, peut ainsi publier un service — rien n'a besoin
d'être joignable depuis l'extérieur sur cet hôte, seul le sortant vers le
dashboard compte.
Portée assumée pour cette première livraison : une requête/réponse à la
fois par connexion, corps entièrement bufferisés (pas de streaming, pas de
SSE, pas de WebSocket relayé à travers le tunnel — en revanche le mode
relais supporte les WebSockets et SSE depuis v12.72.0), un agent = une seule
connexion tunnel active à la fois. Le serveur WebSocket est écrit à la
main (lib/ws-lite.js, RFC 6455) — aucune dépendance npm ajoutée.
Mode « relay » — port unique/double ("mode": "relay")
Réponse au cas intermédiaire entre direct et tunnel : l'hôte distant peut
recevoir des connexions entrantes, mais un port par vhost/container devient
vite pénible dès qu'il vit dans un VLAN séparé de nginx. En mode relay,
l'agent ouvre lui-même un seul port HTTP et/ou un seul port HTTPS fixes
et route en interne vers le bon backend local par Host + chemin (même
principe que le tunnel) ; le vhost nginx généré cible directement cette
adresse unique — une vraie connexion HTTP, streaming compris, sans passer
par le dashboard (contrairement au tunnel).
{ "serverName": "app.example.com", "mode": "relay", "relayScheme": "https",
"locations": [{ "path": "/", "target": "http://127.0.0.1:8080" }] }
accompagné, au niveau de l'enveloppe du manifeste, d'un champ relay donnant
l'adresse que nginx doit utiliser pour joindre l'agent :
{ "protocolVersion": 1, "relay": { "http": "http://203.0.113.44:8080", "https": "https://203.0.113.44:8443" }, "vhosts": [ /* ... */ ] }
Un vhost relay dont le schéma choisi n'a pas d'adresse correspondante dans
relay est rejeté seul, jamais tout le manifeste. Sans certificat fourni
à l'agent, le port HTTPS sert un certificat auto-signé généré en mémoire
(bibliothèque standard Go uniquement) ; le nginx généré pour ce schéma
ajoute alors automatiquement proxy_ssl_verify off;. Voir
nginx-agent/README.md, section « Mode relay », pour la configuration côté
agent (--relay-http-listen/--relay-http-advertise/etc.).
Configuration (config/agents.yml)
enable: false # desactive par defaut (v12.42.0) — opt-in, voir l encadre ci-dessous
offline_after_sec: 90 # seuil d'affichage "hors ligne" dans l'UI
max_vhosts_per_agent: 50 # garde-fou anti-abus, par manifeste
tunnel_enable: true # active le mode 3
tunnel_target: http://nginx-dashboard:3000 # adresse interne du dashboard, telle que nginx la joint
Désactivé par défaut (v12.42.0) : cette fonctionnalité expose un point d'enrôlement d'agent — un comportement qu'une instance qui n'utilise pas les hôtes distants ne devrait pas avoir activé implicitement. Mettre
enable: truedansconfig/agents.ymlpour l'activer explicitement (une installation existante qui en dépendait déjà doit ajouter cette ligne après une mise à jour vers v12.42.0).
enable: falsemasque aussi l'item de menu (v12.41.0) : une instance qui n'utilise pas les hôtes distants n'a plus besoin d'afficher une page qui ne fait plus rien — l'item Hôtes distants du menu se masque automatiquement dans ce cas (voirMENU_AGENTSdans la section « Interface — thème, en-tête personnalisé, menu » pour forcer son affichage malgré tout).
offline_after_sec ne fait que changer un badge dans l'interface —
jamais de suppression automatique des vhosts d'un agent sur un simple
flap réseau ; seule une révocation explicite les retire. Ce comportement
est volontairement plus strict que ce qu'un simple délai de grâce
configurable aurait permis : la page affiche la durée écoulée depuis le
dernier manifeste reçu (« Hors ligne depuis Xmin/Xh ») plutôt qu'un badge
statique, mais aucune purge automatique n'a été ajoutée — une suppression
n'intervient que sur une action explicite (agent révoqué/supprimé).
Page « Hôtes distants »
Liste des agents avec statut (pending/approved/rejected/revoked),
badge en ligne/hors ligne (« Hors ligne depuis Xmin » une fois le dernier
manifeste connu), badge « Tunnel actif » si une connexion mode 3 est
ouverte, empreinte optionnelle, nombre de vhosts publiés, version de
protocole, métriques hôte les plus récentes, dernière erreur de manifeste
le cas échéant, et une prévisualisation des vhosts publiés par un agent
approuvé (avec l'indication [tunnel] ou [relay -> adresse] selon le
mode), désormais avec pause/reprise par vhost (voir ci-dessous). Actions
selon le statut : approuver/rejeter (pending), régénérer le
jeton/révoquer (approved), supprimer (rejected/revoked).
Pause par vhost (v12.36.0)
Contrairement à l'auto-config Docker locale, un agent distant n'a aucune approbation par vhost : dès qu'un agent est approuvé (au niveau de l'hôte entier), tout vhost valide de son manifeste est publié automatiquement. Pour retirer un seul vhost de la publication sans révoquer l'agent entier ni attendre qu'il retire lui-même l'entrée correspondante de son côté (labels/configuration), la fenêtre « Vhosts publiés » de la page permet de mettre en pause un vhost précis (persistant, indépendant du cycle de vie de l'agent — survit à un redémarrage du dashboard, à une déconnexion de l'agent, ou à une révocation suivie d'une nouvelle approbation) et de reprendre la publication à tout moment, ce qui la réapplique immédiatement à partir du dernier manifeste valide reçu.
Un vhost en pause est exclu du fichier généré (retiré s'il existait déjà)
mais reste visible dans la fenêtre avec la date et l'auteur de la pause,
qu'un manifeste de l'agent le déclare encore ou non. Une pause peut aussi
être posée par avance sur un server_name que l'agent n'a encore jamais
publié — elle prend simplement effet dès qu'il apparaît dans un manifeste.
Portée : contrairement à la révocation d'un agent entier, la pause d'un vhost ne touche ni son jeton ni les autres vhosts de son manifeste. Il n'existe pas de notion d'« approbation » séparée à révoquer côté agent (voir plus haut) — mettre en pause est l'équivalent fonctionnel le plus proche pour retirer un vhost précis de la publication.
API
POST /api/agent/enroll— public,{ "hostname": "...", "fingerprint": "..." }GET /api/agents— liste (permissionVIEW_CONFIGS), chaque agent inclutvhosts: la fusion des derniers vhosts publiés (lastVhosts) et des décisions de pause enregistrées, avec ou sans vhost actuellement publié correspondantPOST /api/agents/<id>/approve|/reject|/revoke|/regenerate-token(permissionDEPLOY)POST /api/agents/<id>/vhosts/pause|/resume—{ "serverNames": ["..."] }(permissionDEPLOY)DELETE /api/agents/<id>— uniquementrejected/revoked(permissionDEPLOY)POST /api/agent/manifest— Bearer token de l'agent, réservé à cette seule routeGET /api/agent/tunnel— upgrade WebSocket (mode 3), même jeton Bearer, réservé à l'agent lui-même
Agent de référence (nginx-agent/)
Un vrai binaire à déployer sur chaque hôte distant, en Go, zéro dépendance
tierce — lit les labels nginx-control.* sur son propre Docker local
(même préfixe et esprit que la Partie 1, avec la différence assumée qu'une
cible de location est une adresse directement joignable, jamais un nom de
conteneur), s'enrôle, et pousse son manifeste (vhosts + métriques) sur un
intervalle configurable ; porte aussi le client du tunnel mode 3 et les
listeners du mode relay. Se déploie soit comme binaire natif, soit en
conteneur (nginx-agent/Dockerfile + compose.yml/sample.env fournis
dans ce même dossier — image scratch, toutes les options configurables
par variable d'environnement). Voir nginx-agent/README.md pour la
compilation, le schéma de labels complet, le déploiement Docker et la
marche à suivre (enrôlement → approbation → jeton → premier push).
GoAccess
Le dashboard découvre automatiquement les .log dans ./logs/ et démarre un conteneur GoAccess dédié par vhost via l'API Docker socket.
Variables obligatoires :
GOACCESS_IMAGE=allinurl/goaccess:latest
NGINX_NETWORK=nginx-net # docker network ls
HOST_LOGS=/containers/nginx-rproxy/logs # chemin absolu hôte
HOST_GOACCESS=/containers/nginx-rproxy/goaccess
Format de log dans nginx :
# Log dédié par vhost (format combined — détecté auto)
access_log /var/log/nginx/example.com.access.log;
# Log global tous vhosts (format combined_vhost — détecté auto)
access_log /var/log/nginx/vhosts_access.log combined_vhost;
GoAccess détecte automatiquement le format en lisant la première ligne du fichier — aucune configuration manuelle.
Reset d'un conteneur GoAccess :
docker stop ngx-goaccess-example-com
docker rm ngx-goaccess-example-com
# Puis Démarrer depuis le dashboard
Rapport temps réel : GoAccess pousse les mises à jour via WebSocket. Le rapport est accessible directement dans le dashboard sans port exposé — le WebSocket est proxifié via /api/goaccess/ws/{sourceId}.
API REST
Auth : cookie de session (UI) ou Authorization: Bearer <API_TOKEN>.
| Méthode | Route | Description |
|---|---|---|
GET |
/api/status |
Résumé global nginx |
GET |
/api/metrics |
VTS + historique |
GET |
/api/zones |
Stats par virtual host |
GET |
/api/upstreams |
Backends upstream |
GET |
/api/configs |
Fichiers de configuration |
GET |
/api/ssl |
Certificats parsés |
GET |
/api/nginx-logs |
Fichiers de log |
GET |
/api/nginx-logs/stream |
Stream SSE live |
POST |
/api/nginx/test |
Test config (conteneur éphémère) |
POST |
/api/nginx/reload |
Reload nginx |
GET |
/api/snippets/meta |
Snippets avec métadonnées parsées |
GET |
/api/snippets/ssl |
Snippets SSL uniquement |
GET |
/api/vhost/ssl-sources |
Sources SSL (snippets + certs + LE) |
POST |
/api/vhost/generate |
Générer une config vhost |
GET |
/api/git/status ⁽¹⁾ |
Statut dépôt + diff |
POST |
/api/git/pull ⁽¹⁾ |
Pull |
POST |
/api/git/test ⁽¹⁾ |
Test config depuis Git |
POST |
/api/git/deploy ⁽¹⁾ |
Pipeline complet |
POST |
/api/git/init-backup-branch |
Créer branche backup |
GET |
/api/git/test-connection |
Tester connexion Git |
GET |
/api/backups |
Liste des ZIP |
POST |
/api/backups ⁽¹⁾ |
Créer une sauvegarde |
POST |
/api/backups/restore |
Restaurer un ZIP |
GET |
/api/backups/download?name=… |
Télécharger un ZIP |
DELETE |
/api/backups/:name |
Supprimer |
GET |
/api/crowdsec/status |
Métriques CrowdSec (Prometheus ou LAPI) |
GET |
/api/crowdsec/decisions |
Décisions actives |
GET |
/api/crowdsec/alerts |
Alertes récentes |
GET |
/api/goaccess/sources |
Sources logs + état conteneurs |
POST |
/api/goaccess/start |
Démarrer GoAccess |
POST |
/api/goaccess/stop |
Arrêter GoAccess |
GET |
/api/goaccess/report?sourceId=… |
Rapport HTML proxy |
GET |
/api/goaccess/logs?sourceId=… |
Logs du conteneur GoAccess |
GET |
/api/auth/me |
Session courante |
GET |
/api/auth/users |
Utilisateurs (admin) |
POST |
/api/auth/hash |
Générer un hash |
GET/POST |
/api/webhooks |
Lister / créer |
DELETE |
/api/webhooks/:id |
Supprimer |
POST |
/api/webhooks/:id/test |
Tester |
GET |
/api/backends |
Vhosts avec leurs cibles backend résolues |
POST |
/api/backends/check |
Test à la demande d'une cible (référence, jamais un host/port) |
GET |
/api/audit |
Audit HTTP/HTTPS statique (vhost + backend) |
GET |
/api/monitor |
Cibles sous monitoring continu + état courant |
GET |
/api/monitor/history?key=… |
Historique des checks + pannes d'une cible |
POST |
/api/monitor/check-now |
Forcer une sonde immédiate de toutes les cibles surveillées |
GET |
/api/notifications |
Liste des notifications (centre de notification, cloche d'en-tête) |
GET |
/api/notifications/unread-count |
Nombre de notifications non lues |
POST |
/api/notifications/:id/read |
Marquer une notification comme lue |
POST |
/api/notifications/read-all |
Tout marquer lu |
DELETE |
/api/notifications/:id |
Supprimer une notification |
POST |
/api/notifications/clear-read |
Supprimer les notifications lues |
POST |
/api/notifications/clear |
Vider tout le centre de notification |
⁽¹⁾ Ces cinq routes (/api/git/status, /api/git/pull, /api/git/test,
/api/git/deploy, POST /api/backups) acceptent aussi, en plus de la
session/du jeton admin, un jeton de déploiement scoped
(config/deploy-tokens.yml) — voir « Déclencher depuis un pipeline CI/CD »
dans la section Déploiement Git ci-dessus.
Documentation interactive (page « API REST », v12.41.0)
La page API REST du dashboard (Administration) offre une vue plus
complète que le tableau ci-dessus : catalogue d'environ 130 endpoints
organisés par catégorie, recherche texte libre, filtre par catégorie, un
bouton Essayer par endpoint (demande la valeur de chaque :paramètre
avant l'appel) et, pour quelques endpoints clés — dont le pipeline Git
(/api/git/deploy) — un bouton Exemple ouvrant une fenêtre modale avec
une requête/réponse simple à titre d'illustration. Chaque catégorie et
chaque description d'endpoint est traduite EN/FR et suit un changement de
langue en cours de session, sans recharger la page.
Cette page est entièrement dans son propre fichier,
public/assets/js/api-doc.js (extrait d'index.html, v12.42.0) — même
démarche que les autres pages déjà extraites (CrowdSec, Générateur de
VHost, GoDNS, ...).
Bug corrigé (v12.42.0) : le bouton Essayer ne réagissait qu'au
premier endpoint de la liste, quel que soit celui sur lequel on cliquait —
un identifiant DOM dupliqué sur toute la page (ENDPOINTS.indexOf(e)
cherchait un objet par égalité de référence, ce qui échouait toujours et
renvoyait -1 pour chaque endpoint). Chaque ligne porte désormais un
identifiant stable et unique.
v12.43.0 (retour utilisateur) : deux améliorations supplémentaires.
- Le corps de requête suggéré (textarea sous chaque endpoint POST/PUT/PATCH)
n'affichait qu'un
{"key":"value"}générique et identique partout, sans rapport avec les champs réellement attendus. Il reflète désormais les champs documentés dans la description de l'endpoint (ex.{"ip":"<ip>","duration":"<duration>","reason":"<reason>"}pour/crowdsec/ban), ou un message explicite quand l'endpoint n'attend réellement aucun corps. - Le champ de recherche (
#api-search) était réduit à une largeur d'à peine un caractère à cause d'une course de mise en page avec le<select>de catégorie voisin (tous deux hérités de la même règle CSSwidth:100%, dans un conteneur flex) — un bug purement visuel, la logique de filtrage n'a jamais eu de limite de longueur. Les deux éléments ont désormais des largeurs explicites.
Menu — API REST et Webhooks sous Administration (v12.41.0)
Les pages API REST et Webhooks sont désormais regroupées sous la
section Administration du menu (elles nécessitent déjà les permissions
view_api_doc/manage_webhooks, réservées au rôle admin). Elles se
masquent en plus automatiquement du menu si aucun jeton/secret n'est
configuré (API_TOKEN pour l'un, WEBHOOK_SECRET pour l'autre) — inutile
d'afficher une page qui ne peut de toute façon rien faire sans eux. Ce
comportement suit la même convention ENV > YAML > défaut que le reste du
projet ; voir MENU_API / MENU_WEBHOOKS dans la section Variables
d'environnement.
Variables d'environnement
Nginx
| Variable | Défaut |
|---|---|
NGINX_TAG |
1.30.1 |
NGINX_WORKER_PROCESSES |
auto |
NGINX_WORKER_CONNECTIONS |
768 |
Dashboard
| Variable | Description | Défaut |
|---|---|---|
PORT |
Port d'écoute | 3000 |
NGINX_VTS_URL |
URL endpoint VTS | http://nginx:8080/status.json |
NGINX_CONTAINER |
Nom conteneur nginx | nginx |
NGINX_IMAGE |
Image tests éphémères | (auto-détecté) |
API_TOKEN |
Token Bearer API (optionnel, désactivé tant qu'il n'est pas défini explicitement — ou générable depuis la page Système, v12.33.0) | changeme (désactivé) |
WEBHOOK_SECRET |
Secret envoyé avec chaque webhook sortant (optionnel — ou générable depuis la page Système, v12.33.0) | changeme (désactivé) |
SESSION_SECRET |
Secret sessions | (auto-généré et persisté si absent, v12.32.0) |
SESSION_TTL_HOURS |
Durée sessions | 8 |
BACKUP_KEEP |
ZIP à conserver | 20 |
GIT_REPO_URL |
URL dépôt | (vide) |
GIT_BRANCH |
Branche source | main |
GIT_TOKEN |
Token HTTPS | (vide) |
HOST_LOGS |
Chemin hôte absolu des logs | (vide) |
HOST_GOACCESS |
Chemin hôte absolu GoAccess | (vide) |
NGINX_NETWORK |
Réseau Docker partagé | nginx-net |
GOACCESS_IMAGE |
Image GoAccess | allinurl/goaccess:latest |
CROWDSEC_PROMETHEUS_URL |
URL métriques Prometheus | (vide) |
CROWDSEC_LOCAL_ONLY |
Masquer décisions CAPI | false |
CROWDSEC_URL |
URL LAPI | (vide) |
CROWDSEC_API_KEY |
Clé API bouncer | (vide) |
ANALYZER_DEFAULT_IMAGE |
Image par défaut de l'analyzer quand container_image n'est pas défini dans analyzer.yml — surchargeable au build (--build-arg, voir Dockerfile) ou à l'exécution |
forge.rdr-it.com/dockerfiles/nginx-analyzer:latest |
AGENT_DEFAULT_IMAGE |
Image de l'agent Go distant (nginx-agent) proposée dans le kit de déploiement des agents (.env/compose.yml) — ARG/ENV du Dockerfile du dashboard, surchargeable à l'exécution ; nom sans tag, défaut forge.rdr-it.com/dockerfiles/nginx-control-agent. |
|
MONITOR_RETENTION_DAYS |
Rétention de l'historique du monitoring continu (checks + incidents clos) | 30 |
NOTIF_CENTER_RETENTION_DAYS |
Rétention des notifications du centre de notification (cloche d'en-tête), lues ou non | 90 |
APP_VERSION |
Numéro de version affiché par le dashboard (badge d'en-tête, GET /api/status) — injecté au build via ARG APP_VERSION (voir Dockerfile et docker-build.yml, qui passe --build-arg APP_VERSION=${TAG_NAME} sur chaque tag Git) |
dev |
TZ |
Fuseau horaire (nom IANA, ex. Europe/Paris) — voir l'encadré ci-dessous |
(vide, équivaut à UTC) |
MENU_WAF |
Visibilité du menu WAF (v12.39.0) — auto/show/hide, priorité sur config/menu.yml |
auto |
MENU_GODNS |
Visibilité du menu GoDNS (v12.39.0) — show/hide, priorité sur config/menu.yml |
show |
GODNS_IP_CHECK |
Vérification multi-source de l'IP publique rapportée par GoDNS (v12.39.0) — voir « GoDNS — vérification multi-source de l'IP publique » | true |
GODNS_IP_CHECK_URLS |
Services « what is my IP » interrogés pour GODNS_IP_CHECK, séparés par des virgules |
https://api.ipify.org,https://icanhazip.com,https://ifconfig.me/ip |
CHANGELOG_URL |
URL du fichier CHANGELOG.md affiché par le bouton Changelog (page Système, v12.41.0) — voir « Changelog intégré » |
(vide, bouton masqué) |
MENU_API |
Visibilité du menu API REST (v12.41.0) — auto/show/hide, priorité sur config/menu.yml |
auto |
MENU_WEBHOOKS |
Visibilité du menu Webhooks (v12.41.0) — auto/show/hide, priorité sur config/menu.yml |
auto |
MENU_DOCKER_AUTOCONFIG |
Visibilité du menu Auto-config Docker (v12.41.0) — auto/show/hide, priorité sur config/menu.yml |
auto |
MENU_AGENTS |
Visibilité du menu Hôtes distants (v12.41.0) — auto/show/hide, priorité sur config/menu.yml |
auto |
MENU_SYNC / MENU_VHOSTGEN / MENU_GRAPH / MENU_DEPLOY / MENU_BLOCKLISTS / MENU_WAFEMU / MENU_GEOMAP / MENU_DIGEST |
Visibilité des menus correspondants (v12.72.0) — show/hide, priorité sur config/menu.yml |
show |
MENU_GOACCESS |
Visibilité du menu GoAccess (v12.72.0) — auto/hide |
auto |
ALERTING_URL |
URL du fichier Markdown d'alertes distantes (v12.45.0) — voir « Message important (alertes distantes) » | (vide, bouton masqué) |
ALERTING_ENABLE |
Active/désactive la fonctionnalité d'alerting (v12.45.0) — priorité sur alerting_enable dans config/features.yml |
false |
ALERTING_POLL_INTERVAL_MIN |
Intervalle (minutes) entre deux vérifications de ALERTING_URL par le planificateur serveur (v12.45.0), minimum 5 |
60 |
Fuseau horaire (
TZ) — le dashboard et l'analyzer (Dockerfiledes deux images installetzdata) tournent en UTC par défaut siTZn'est pas défini, ce qui était jusqu'ici le seul comportement possible. Deux endroits lisent l'heure locale du conteneur plutôt que UTC : l'horodatage ModSecurity par défaut, qui ne porte aucun fuseau et est donc interprété dans le fuseau du conteneur (nginx-analyzer/lib/parse-waf.js), et la référence hebdomadaire de trafic (baseline), qui regroupe volontairement par heure LOCALE de la semaine plutôt que par heure UTC pour suivre le rythme humain (nuit/jour, semaine/week-end) même après un changement d'heure (nginx-analyzer/lib/baseline.js). DéfinirTZsur le fuseau réel du serveur nginx (environment: TZ=Europe/Parisdansdocker-compose.yml, pour le servicenginx-dashboardet/ounginx-analyzer) aligne les deux sur l'heure locale réelle de l'exploitation, sans rebuild d'image.
Ces variables Git/CrowdSec/GoAccess ci-dessus (ainsi que GIT_BACKUP_BRANCH,
CROWDSEC_MACHINE_ID, CROWDSEC_MACHINE_PASSWORD, GOACCESS_LOG_FORMAT,
GOACCESS_REFRESH) restent la valeur par défaut : dès que le champ
correspondant est renseigné dans git.yml, crowdsec.yml ou goaccess.yml,
il prend le dessus, sans avoir à toucher au conteneur — voir « Fichiers de
configuration » plus bas.
URLs directes par page (deep-linking)
Depuis la v12.31.0, chaque page du menu a sa propre URL (/map, /live-logs,
/waf, /blocklists, ...) — un F5, un lien direct ou un favori sur l'une
de ces URLs affiche la bonne page directement, et les boutons
précédent/suivant du navigateur naviguent entre les pages déjà visitées.
Avant cette version, seules / et /dashboard étaient reconnues : un F5 ou
un lien direct sur une autre URL ramenait toujours à Vue d'ensemble (ou
répondait 404 pour une URL de page).
La table complète des correspondances slug ↔ page vit dans
lib/page-routes.js (seul le nom d'URL diffère du nom interne pour deux
pages : geomap → /map, logs → /live-logs ; toutes les autres gardent
leur nom interne tel quel en URL). Après une connexion, on atterrit toujours
sur Vue d'ensemble, quelle que soit l'URL qui a déclenché la redirection
vers la page de connexion — choix délibéré pour rester simple plutôt que de
faire persister la page demandée à travers une expiration de session.
Sécurité
- Sessions HMAC-SHA256, HttpOnly + SameSite=Strict
- Mots de passe SHA-256 + salt, comparaison timing-safe
API_TOKEN/WEBHOOK_SECRETgénérables depuis la page Système plutôt que seulement via.env(v12.33.0), affichés une seule fois à la génération :API_TOKENn'est jamais relu par le dashboard (seulement vérifié face à ce qu'un appelant présente) — seule son empreinte (scrypt, même format que les mots de passe) est conservée dansconfig/.generated-secrets.json, jamais la valeur en clair.WEBHOOK_SECRET, à l'inverse, est envoyé par le dashboard à chaque webhook sortant : il doit rester lisible, donc persisté en clair dans le même fichier (permissions0600, écriture atomique) — jamais réaffiché une fois la fenêtre de génération refermée.- Volumes : seuls certains sont montés en lecture seule (
:ro) —certs/(Let's Encrypt, géré par certbot),logs/,geoip_data/. Les répertoires de configuration nginx (sites/,conf/,snippets/,streams/,ssl/) sont montés en lecture-écriture, à dessein : le déploiement Git et l'édition de fichiers en ligne (ALLOW_EDIT) en ont besoin pour écrire. La mention précédente de « volumes configs en lecture seule » ici était inexacte pour ces répertoires-là. - Socket Docker monté avec le flag
:ro— utile pour qu'undocker inspectdu conteneur affiche clairement l'intention, mais ce flag ne restreint en rien ce que l'API Docker elle-même accepte une fois la connexion établie (créer, démarrer, arrêter ou exécuter dans n'importe quel conteneur du démon reste possible) ::roprotège l'inode du socket sur le montage, pas les commandes envoyées à travers. Exposer ce socket au conteneur du dashboard équivaut, en pratique, à lui donner un accès root au moteur Docker de l'hôte — c'est un compromis assumé (nécessaire pournginx -t/nginx -s reloadet l'auto-config), pas une sandbox. - GoAccess — aucun port exposé, WebSocket proxifié
- VTS — port 8080 restreint aux IPs privées
- Jetons de déploiement CI/CD scopés (
config/deploy-tokens.yml) — jamais le jeton admin global dans un secret de pipeline, containment structurel à 5 routes (POST /api/git/pull,/test,/deploy,GET /api/git/status,POST /api/backups) + action par jeton, voir « Déploiement Git » - Auto-config Docker par labels : approbation manuelle active par défaut
(
config/docker-autoconfig.yml,require_approval: true), validation ancrée de chaque valeur de label, conflit deserver_namejamais résolu en silence — voir « Auto-config Docker » - Générer des secrets :
openssl rand -hex 32 - Les images
nginx-dashboard,nginx-analyzeretnginx-agenttournent toutes enrootà l'intérieur de leur conteneur (aucunUSERnon-root dans leursDockerfile) — non corrigé dans cette livraison : le dashboard a de toute façon besoin d'un accès complet au socket Docker (voir ci-dessus, qui équivaut déjà à root sur l'hôte via le démon Docker), ce qui rend un simpleUSER nobodylargement cosmétique tant que ce socket reste monté ; passer réellement en non-root demanderait de faire correspondre l'UID/GID du conteneur à celui du groupedockerde chaque hôte, ce qui n'est pas portable en l'état. À isoler correctement, cela demanderait un composant intermédiaire dédié à l'accès Docker plutôt qu'un changement deUSERseul. APP_VERSION=devpar défaut sur une installation viadocker-compose.ymlsi celui-ci n'a pas été modifié pour transmettre--build-arg APP_VERSION=...(voirDockerfile) : le compose ne le fait pas lui-même, il faut le lier à un tag Git ou une variable d'environnement de build côté CI/CD si le numéro de version affiché doit refléter la version déployée.- Aucune dépendance à un CDN tiers pour s'afficher (fix v12.30.0) :
le dashboard sert lui-même ses polices (
public/assets/fonts/, extraites des paquets npm@fontsource/jetbrains-monoet@fontsource/syne) et Chart.js (public/assets/vendor/chart.umd.min.js), plutôt que de les charger depuis fonts.googleapis.com/fonts.gstatic.com/cdn.jsdelivr.net. Avant cette version, Chart.js était chargé depuis un CDN alors que la CSP (Content-Security-Policy,script-src 'self' 'unsafe-inline') ne l'autorisait déjà pas — le graphique du tableau de bord ne s'affichait donc jamais chez un opérateur dont le navigateur applique réellement cette policy. Bénéfice secondaire : le dashboard fonctionne à l'identique sur un réseau sans accès sortant vers l'internet public.
Snippets de filtrage
# Bots malveillants et IA
if ($bad_bot) { return 444; }
if ($ai_bot) { return 444; }
# Filtrage géographique
if ($allow_visit_001 = 0) { return 403; } # France + Good Bots
if ($allow_visit_002 = 0) { return 403; } # France + IE + GB + Good Bots
Sync fichiers de référence
Le dashboard peut synchroniser automatiquement les fichiers conf/sites/snippets/streams depuis un dépôt Forgejo/Gitea de référence.
Configuration
SYNC_REF_URL=https://forge.rdr-it.com/romain/nginx-rproxy-reference
Structure attendue dans le dépôt — un dépôt dédié entièrement à ces quatre dossiers, à sa racine :
repo/
├── conf/
├── sites/
├── snippets/
└── streams/
Si votre dépôt de référence imbrique plutôt ces dossiers sous un sous-dossier
(par exemple un monorepo partagé avec d'autres projets), précisez ce
sous-dossier avec SYNC_REF_PATH_PREFIX :
SYNC_REF_PATH_PREFIX=Nginx-RProxy
SYNC_REF_URL peut aussi être défini au build de l'image (--build-arg SYNC_REF_URL=..., voir le Dockerfile) pour livrer une image avec un dépôt
de référence déjà configuré par défaut — la valeur de .env, si présente,
prime toujours au démarrage du conteneur.
Versioning des fichiers
Ajouter dans l'en-tête de chaque fichier de référence :
# name: proxy-common
# description: Headers proxy standard
# emplacement: server,location
# version: 1.0.0
Fichiers sans # version = version 0.0.0 (toujours proposés à la mise à jour).
Comportement lors de la sync
| Situation | Résultat |
|---|---|
| Fichier absent localement | Créé en .conf.DISABLE |
Fichier actif (.conf) |
Contenu mis à jour, reste actif |
Fichier désactivé (.conf.DISABLE) |
Contenu mis à jour, reste désactivé |
| Nouveau fichier de référence | Toujours créé en .conf.DISABLE |
Le contenu est toujours mis à jour. L'état DISABLE/ENABLE est toujours préservé.
Pipeline
La sync n'écrit jamais directement en prod. Elle passe par :
Check versions → Preview diff → Écriture git-work → Test éphémère → Backup → Deploy → Reload
Édition et création des fichiers nginx (sites / conf.d / snippets / streams)
La page Configuration nginx (/api/configs/*, features/configs.js) est
en lecture seule par défaut : elle liste et affiche le contenu des quatre
répertoires gérés (sites, conf.d, snippets, streams). Deux actions
d'écriture, désactivées par défaut, s'activent indépendamment l'une de
l'autre par variable d'environnement — toutes deux passent par le même
pipeline de sécurité avant de toucher un fichier réellement utilisé par
nginx : écriture dans un fichier temporaire → test dans un conteneur nginx
éphémère avec le changement substitué → sauvegarde ZIP → application →
nginx -s reload. Un fichier qui ne passe pas le test n'atteint jamais la
production.
ALLOW_EDIT=true— active l'édition d'un fichier existant depuis la page (bouton « Éditer » dans la visionneuse). Un fichier qui provient du dépôt de référence (Sync ref,SYNC_REF_URL) ne peut pas être édité ici — le message renvoyé renvoie vers Sync ref, seule source valable pour ces fichiers-là.ALLOW_CREATE=true— active la création d'un nouveau fichier (bouton « + Nouveau fichier »), uniquement quand Git n'est pas configuré (git.yml/GIT_REPO_URLtous deux vides). Sans dépôt Git, les répertoires gérés par le dashboard sont la seule source de vérité, donc créer un fichier directement dedans est sans risque ; dès qu'un dépôt Git est configuré, un nouveau fichier est censé provenir du dépôt de référence (Sync ref) ou d'un déploiement Git, pour que les répertoires restent reproductibles à partir de ce dépôt — créer un fichier localement dans ce cas romprait cette garantie silencieusement.GET /api/configs/create-statusexpose l'état courant (allowCreate,gitConfigured,enabled) pour que l'interface masque le bouton quand l'une des deux conditions manque. Seules les quatre sectionssites/conf/snippets/streamssont concernées (passsl/, un point de montage à part) ; le nom de fichier est strictement un nom de base (aucun/ni..) avec une extension parmi celles déjà acceptées en lecture (.conf,.DISABLE,.sample,.inc,.nginx,.txt), et un nom déjà existant est refusé (409) — la création ne remplace jamais un fichier, c'est le rôle de l'édition.
| Variable | Description | Défaut |
|---|---|---|
ALLOW_EDIT |
Édition en ligne d'un fichier nginx existant | false |
ALLOW_CREATE |
Création d'un nouveau fichier nginx, si Git n'est pas configuré | false |
Tâches planifiées (page Scheduler, v12.56.0)
Les tâches sont stockées en base SQLite (plus de YAML) et se gèrent graphiquement :
- Nouvelle tâche : action, planification visuelle (toutes les X min/h, tous les jours, jours de la
semaine, jour du mois) ou mode avancé cron, aperçu en direct des 5 prochaines exécutions
(fuseau horaire du serveur, variable
TZ), options propres à l'action, activation, e-mail de fin. - Actions : reload nginx, restart nginx (test de config préalable), redémarrage de l'analyzer, résumé périodique, sauvegarde (local / git / les deux), redémarrage de GoAccess.
- Liste : prochaine exécution, dernier résultat, activer/désactiver, Exécuter maintenant, historique (50 dernières exécutions par tâche), modification, suppression.
- Fichier tâche : bouton Exporter (
scheduled-tasks.json) / Importer (fusion ou remplacement). - Migration : au premier démarrage en v12.56.0,
scheduler.ymlest importé une fois ; il est ensuite ignoré. - Droits : lecture
view_configs; création, modification, exécution, importnginx_control. - Ajouter une action : une entrée dans
lib/scheduler-tasks.js(libellés fr/en, schéma des paramètres, fonctionrun) — le formulaire est généré depuis ce schéma, aucune modification de l'interface. - API :
GET /api/scheduler/{tasks,types,runs,export},POST /api/scheduler/{preview,import},POST /api/scheduler/tasks/{save,toggle,delete,run}.
Notifications (page Notifications, v12.57.0)
SMTP et alertes e-mail se règlent par formulaires (plus de YAML à écrire) :
- SMTP : préréglages de fournisseurs, serveur/port/sécurité, expéditeur, identifiants. Le mot de passe n'est jamais renvoyé au navigateur (champ vide = conservé). Bouton Envoyer un e-mail de test : utilise les valeurs saisies sans les enregistrer.
- Alertes : une carte par événement (certificats, nginx -t / reload / restart, analyzer, sauvegarde, listes de blocage, autoconfig Docker, manifeste agents) avec activation et destinataires.
- Avancé (YAML) : éditeurs bruts de
smtp.ymletnotifications.yml(les formulaires régénèrent ces fichiers ; les commentaires manuels ne sont pas conservés). - Droits : permission
deploy. API :GET /api/notify/form,POST /api/notify/{smtp-form,rules-form,test}. - Ajouter une alerte : une entrée dans
RULES(lib/notify-form.js) et l'appelsendNotification('<id>', sujet, corps)là où l'événement se produit.
Créer un agent depuis le dashboard (v12.58.0)
Page Hôtes Docker distants → Nouvel agent : nom de l'hôte, URL du dashboard, options (relais, mode du
jeton…). Le dashboard crée l'agent déjà approuvé, génère le jeton et fournit .env, compose.yml et les
commandes à lancer sur l'hôte distant — aucune étape d'enrôlement/approbation. Le jeton n'est affiché
qu'une fois (kit aussi proposé à l'approbation d'un agent auto-enrôlé et à la régénération du jeton).
POST /api/agents/create (permission deploy, nécessite enable: true dans agents.yml).
Synchronisation de certificats (page SSL, v12.59.0)
Carte Synchronisation de certificats (administrateurs). Chaque instance peut être source et/ou destination.
Cas 1 — DMZ → LAN (pull) : sur la DMZ, créer un jeton pull limité aux certificats voulus (affiché une fois).
Sur le LAN, ajouter un lien Récupérer : URL https://dmz:PORT, jeton, mode TLS (empreinte pour un certificat
auto-signé), correspondances nom distant → nom local. Planifier la tâche Scheduler certsync (quotidienne).
Cas 2 — maître → autres VPS (push) : sur chaque VPS destinataire, créer un jeton push. Sur le maître,
ajouter un lien Envoyer par destinataire. Un seul certbot (maître) obtient *.domain.tld.
Utilisation dans nginx :
ssl_certificate /etc/letsencrypt/synced/<nom>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/synced/<nom>/privkey.pem;
Docker : ./certs est monté en lecture seule dans le dashboard ; ajouter un montage lecture-écriture
dédié (le dossier hôte ./certs/synced doit exister, nginx le voit déjà via ./certs:/etc/letsencrypt) :
- ./certs:/nginx/certs:ro
- ./certs/synced:/nginx/certs/synced # synchro de certificats (rw)
(mkdir -p certs/synced puis recréer le conteneur). Sans cela : erreur « non inscriptible ».
Le dashboard teste (nginx -t) et recharge après chaque mise à jour ; échec = retour à l'état précédent.
API (jeton) : GET /api/certsync/list, GET /api/certsync/pull?name=, POST /api/certsync/push.
Fichiers de configuration
Tous les fichiers de configuration sont dans ./nginx-dashboard/config/ (monté en /config/ dans le conteneur).
Douze d'entre eux (marqués éditable ci-dessous) se modifient directement
depuis le dashboard, page Configuration (section admin) — plus besoin de
se connecter à la machine pour les changer. Les trois autres déjà éditables
(smtp.yml, notifications.yml, scheduler.yml) ont leur propre éditeur
sur la page Notifications, et godns.config.yaml/.json sur la page
GoDNS — voir « Éditeur de configuration » plus bas pour le détail des
deux mécanismes.
| Fichier | Description |
|---|---|
users.yml |
Utilisateurs et rôles (admin/operator/viewer) |
smtp.yml |
Configuration SMTP pour les notifications |
notifications.yml |
Règles d'alerte (destinataires, seuils) |
scheduler.yml |
Ancien format des tâches planifiées — importé une seule fois en base au démarrage (v12.56.0), ensuite ignoré ; les tâches se gèrent dans la page Scheduler |
certbot.yml |
Certbot défi HTTP-01 (éditable) |
certbot-dns.yml |
Certbot défi DNS-01 — multi-fournisseurs, wildcards (page Certbot) (éditable) |
godns.yml |
GoDNS Dynamic DNS (éditable) |
godns.config.yaml |
Configuration GoDNS (YAML) |
godns.config.json |
Configuration GoDNS (JSON, alternatif) |
geoipupdate.yml |
Mise à jour des bases GeoIP MaxMind (page GeoIP) (éditable) |
error-pages.yml |
Pages d'erreur personnalisées (page Pages d'erreur) (éditable) |
analyzer.yml |
Agent d'analyse des journaux (page Analyse) (éditable) |
git.yml |
Dépôt Git (déploiement + sauvegardes + sync fichiers ref) (éditable) |
crowdsec.yml |
Intégration CrowdSec — LAPI et/ou Prometheus (page CrowdSec) (éditable) |
goaccess.yml |
Image, format de log et rafraîchissement GoAccess (page GoAccess) (éditable) |
blocklists.yml |
Sources de blocklists IP externes, fréquence, action de blocage (page Blocklists IP) (éditable) |
deploy-tokens.yml |
Jetons scopés pour déclencher un déploiement Git depuis un pipeline CI/CD (éditable) |
docker-autoconfig.yml |
Auto-config Docker par labels — approbation, motifs autorisés, intervalle de sondage (page Auto-config Docker) (éditable) |
menu.yml |
Visibilité des éléments de menu optionnels — WAF, GoDNS (v12.39.0) — voir « Interface » plus haut |
git.yml, crowdsec.yml et goaccess.yml uniformisent en fichier de
configuration des réglages jusqu'ici uniquement disponibles en variable
Docker (GIT_REPO_URL, CROWDSEC_URL, GOACCESS_IMAGE, etc.). La règle de
priorité est la même partout dans le dashboard (déjà utilisée par
ANALYZER_DEFAULT_IMAGE) : un champ renseigné dans le YAML l'emporte, un
champ absent ou vide retombe sur la variable Docker correspondante. Les
trois fichiers sont livrés avec tous les champs vides, donc un déploiement
existant configuré en .env n'est affecté par rien tant qu'aucun champ
n'est explicitement rempli depuis la page Configuration.
Restent volontairement en variable Docker uniquement, hors de ces fichiers
YAML : GIT_SSH_KEY (chemin vers une clé privée montée dans le conteneur),
DIR_GIT_WORK et DIR_GOACCESS (chemins de montage internes fixés par
docker-compose.yml, même catégorie que DIR_SITES/DIR_CONF) — ce sont
des réglages d'infrastructure du conteneur, pas des comportements applicatifs
qu'un opérateur voudrait changer à chaud.
Éditeur de configuration
Pour éviter d'avoir à se connecter à la machine pour éditer un fichier YAML à la main, le dashboard propose deux mécanismes d'édition en ligne, tous deux réservés aux administrateurs et tous deux basés sur le même principe de masquage des secrets :
Page Configuration (/api/config-editor/*) — un éditeur générique
pour les fichiers de fonctionnalités qui n'avaient jusqu'ici aucun
éditeur dédié : certbot.yml, certbot-dns.yml, geoipupdate.yml,
error-pages.yml, analyzer.yml, godns.yml, git.yml, crowdsec.yml,
goaccess.yml, blocklists.yml, deploy-tokens.yml, docker-autoconfig.yml.
La liste des fichiers
éditables est une liste fixe côté serveur (FILES dans
features/config-editor.js) : la clé envoyée par le client sélectionne un
fichier dans cette liste, elle ne construit jamais un chemin — une clé
inconnue ou une tentative de traversée de répertoire (../../etc/passwd)
est refusée avant toute lecture ou écriture disque. Volontairement absents
de cette liste : users.yml (a sa propre gestion via la page
Utilisateurs) et tout fichier contenant des identifiants bruts
(credentials.ini du défi DNS, par exemple) — l'éditeur ne lit et n'écrit
que des fichiers de configuration de fonctionnalité, jamais un secret
autonome, conformément à la règle du projet.
Éditeurs existants, inchangés — smtp.yml, notifications.yml et
scheduler.yml s'éditent depuis la page Notifications
(/api/notify/config-files), et godns.config.yaml/.json depuis la
page GoDNS (/api/godns/config-file). Ces deux routes existaient déjà
avant l'éditeur générique et utilisent le même mécanisme de masquage —
l'éditeur générique n'a fait qu'étendre un principe déjà éprouvé aux douze
fichiers qui n'en profitaient pas encore.
Masquage des secrets (les deux mécanismes) — à la lecture, toute
valeur d'un champ reconnu comme sensible (password, secret, token,
api_key, license_key, private_key, credentials, machine_password,
etc.) est
remplacée par ******** avant d'être envoyée au navigateur ; un champ qui
n'est pas un secret (comme credentials_host_path, qui est un chemin, pas
une valeur secrète) reste visible normalement. À la sauvegarde, si un champ
masqué est resté tel quel (********), la vraie valeur déjà en place sur
le disque est réinjectée automatiquement — le texte affiché à l'écran ne
peut donc jamais écraser un secret par erreur. Si le champ a été modifié
explicitement (une autre valeur a remplacé ********), c'est cette
nouvelle valeur qui est prise en compte.
Permissions — lire ou écrire un de ces fichiers exige la permission
DEPLOY (comme toute action qui touche à la configuration d'une
fonctionnalité) ; afficher un secret en clair (bouton « révéler ») exige en
plus la permission MANAGE_USERS, réservée au rôle admin. Un rôle
viewer ou operator sans DEPLOY n'a donc accès à aucune des deux pages
d'édition.
GoDNS — panneau web : accès via un vhost dédié (v12.40.0, remplacé en v12.43.0)
Le conteneur GoDNS embarque son propre panneau web (web_panel.enabled: true
dans godns.config.yaml). En v12.40.0, le bouton Web Panel (onglet
Status de la page GoDNS) l'ouvrait dans une iframe embarquée, relayée sur le
réseau Docker interne par le dashboard (GET /api/godns/panel) — même
principe que le rapport GoAccess, pour éviter d'avoir à publier le port du
panneau sur l'hôte.
Retour utilisateur (v12.43.0) : ce relais ne fonctionne pas avec le
panneau GoDNS, une application Next.js dont les requêtes d'assets relatives
(_next/static/chunks/*.js/.css) ne survivent pas à un relai HTTP
générique — elles reviennent en 404 ou avec un Content-Type application/json, que le navigateur refuse d'exécuter/appliquer. Écrire un
vrai proxy conscient du routing Next.js aurait été disproportionné pour un
panneau qui s'ouvre nativement, sans aucun de ces problèmes, derrière un
vhost nginx classique.
Le bouton Web Panel ouvre désormais une modale d'instructions
statique : elle indique le conteneur et le port GoDNS à utiliser comme
cible (proxy_pass http://<container_name>:<port>;), avec un raccourci
direct vers le Générateur de VHost. Il suffit d'y créer un vhost avec
ce proxy_pass et le server_name de son choix, de recharger nginx, puis
d'accéder au panneau via ce nom de domaine — le panneau complet
fonctionne alors directement, sans relais ni limitation de compatibilité.
Le relais interne et sa route (GET /api/godns/panel) ont été retirés du
code.
GoDNS — vérification multi-source de l'IP publique (v12.39.0)
Retour utilisateur : GoDNS a mis à jour tous ses enregistrements DNS
vers une IP appartenant à la plage Cloudflare, alors que l'IP réelle du
serveur était différente. Cause typique : un des ip_urls de
godns.config.yaml pointe vers un service « what is my IP » qui est
lui-même derrière un CDN en mode proxy (nuage orange) — le service répond
alors avec l'IP du CDN (celle qui s'est connectée à lui), pas l'IP réelle
du visiteur. GoDNS fait confiance à la première réponse de sa liste
ip_urls et ne peut pas détecter ce genre d'anomalie lui-même.
Le dashboard ajoute une vérification indépendante, best-effort :
- À chaque rafraîchissement de la page GoDNS (
GET /api/godns/info), il interroge lui-même plusieurs services publics distincts (GODNS_IP_CHECK_URLS, par défautapi.ipify.org,icanhazip.com,ifconfig.me) et calcule l'IP qui fait consensus (majorité absolue) — un seul service en désaccord ou injoignable ne suffit pas à fausser le résultat. - Si l'IP rapportée par GoDNS diffère du consensus, un bandeau d'avertissement s'affiche en haut de l'onglet Status, avec le détail par service interrogé.
- Aucun blocage : c'est une alerte, pas un contrôle a priori — le dashboard
n'a pas de rôle de policier sur
godns.config.yaml, qui reste éditable librement depuis la page GoDNS (permissionDEPLOY).
Réglages (GET /api/system-info, catégorie « GoDNS ») :
| Variable | Description | Défaut |
|---|---|---|
GODNS_IP_CHECK |
Active/désactive la vérification (aucun appel externe supplémentaire si false) |
true |
GODNS_IP_CHECK_URLS |
Services interrogés, séparés par des virgules — choisir des services qui ne sont pas eux-mêmes derrière un CDN/reverse-proxy | https://api.ipify.org,https://icanhazip.com,https://ifconfig.me/ip |
Si le bandeau apparaît : vérifier les ip_urls de
godns.config.yaml/.json (page GoDNS, éditeur de fichier) — remplacer
tout service maison ou proxifié par des services indépendants et directs
comme ceux listés ci-dessus, puis relancer le conteneur GoDNS.
GoDNS — bouton Web Panel absent malgré web_panel.enabled: true (v12.41.0)
Retour utilisateur : le bouton Web Panel (onglet Status de la page
GoDNS) restait invisible même avec web_panel.enabled: true dans
godns.config.yaml. Cause : le dashboard vérifiait le mauvais fichier —
config/godns.yml (propriétaire du dashboard, ne contrôle que le cycle de
vie du conteneur et la publication éventuelle de port) au lieu de
godns.config.yaml/.json (propriétaire de GoDNS, monté en lecture seule,
seul fichier que le binaire GoDNS lit réellement pour activer son panneau).
Ces deux fichiers sont distincts et peuvent parfaitement diverger. Le
dashboard lit désormais directement le vrai fichier de configuration GoDNS
(YAML ou JSON) pour décider d'afficher le bouton.
Boutons « Copier » à côté d'un jeton/secret (v12.44.0)
Retour utilisateur : le bouton Copier affiché à côté d'un jeton ou d'un
secret (API_TOKEN/WEBHOOK_SECRET généré, jeton d'agent distant, sortie du
Générateur de VHost, contenu de fichier de config, snippet CrowdSec,
info-bulle user-agent) ne faisait strictement rien sur un dashboard servi en
HTTP simple — un cas courant pour un panneau d'administration réservé au LAN
interne. Cause : l'API navigator.clipboard n'existe que dans un « contexte
sécurisé » (HTTPS ou localhost) et vaut undefined en HTTP simple ; deux
implémentations différentes du bouton Copier échouaient silencieusement ou
levaient une erreur jamais interceptée dans ce cas.
Une fonction commune copyToClipboard() gère désormais les deux cas : elle
utilise l'API moderne quand elle est disponible, et se rabat sinon sur l'API
historique document.execCommand('copy') (qui, elle, fonctionne en HTTP
simple) via un <textarea> temporaire hors-écran. Chaque bouton Copier du
dashboard donne un retour visuel bref (« Copié ! » ou « Échec de la copie »)
plutôt que de rester muet.
Changelog intégré (page Système, v12.41.0)
Un bouton Changelog (page Système, à côté de Rafraîchir) ouvre une
fenêtre modale qui affiche le fichier CHANGELOG.md du projet, rendu en
Markdown simple (titres, listes, code, liens — aucune dépendance npm
ajoutée). Le fichier n'est pas embarqué dans l'image : il est récupéré
depuis une URL distante configurable :
CHANGELOG_URL=https://forge.example.com/romain/nginx-rproxy-suite/raw/branch/main/CHANGELOG.md
CHANGELOG_URL peut aussi être définie au build de l'image (ARG CHANGELOG_URL, voir le Dockerfile), pour livrer une image dont le bouton
Changelog pointe déjà vers le bon dépôt sans configuration supplémentaire à
l'exécution. Absente : le bouton reste masqué. Le contenu récupéré est mis
en cache 5 minutes côté serveur, avec les mêmes garde-fous que la
vérification de version (profondeur de redirection limitée, timeout,
taille maximale) — pas d'appel réseau à chaque ouverture de la modale.
Message important (alertes distantes, v12.45.0)
Un mécanisme d'alerte, sur le même principe que le Changelog intégré
ci-dessus : un fichier Markdown hébergé hors de l'image, relu
périodiquement, dont chaque alerte est un bloc ## titre identifié par une
ligne ID: stable (n'importe quel identifiant qui ne change jamais pour
cette alerte convient — un timestamp Unix au moment de la rédaction en est
un exemple pratique) :
## Maintenance planifiée le 12/10
ID: 1728691200
LEVEL: warning
Une coupure de 10 minutes est prévue sur le reverse-proxy le 12/10 à 2h du
matin (mise à jour de sécurité nginx).
LEVEL est optionnel (info par défaut, ou warning/error — les mêmes
niveaux que le centre de notification). Une alerte sans ID: est ignorée
(avec un avertissement en log) : sans identifiant stable, impossible de
savoir plus tard si elle a déjà été vue. Le fichier distant « vit » : une
alerte qui en disparaît n'est pas effacée de l'historique local, simplement
plus jamais reproposée.
Configuration (Dockerfile ou .env) :
ALERTING_URL=https://forge.example.com/romain/nginx-rproxy-suite/raw/branch/main/ALERTS.md
ALERTING_ENABLE=true
ALERTING_POLL_INTERVAL_MIN=60
ALERTING_URL seule ne suffit pas à activer la fonctionnalité : elle reste
désactivée par défaut (opt-in), même URL configurée — pratique pour une
image de base partagée par toute une flotte, où seules certaines instances
doivent réellement interroger le fichier. Deux façons de l'activer,
combinables (priorité variable d'environnement > fichier) :
ALERTING_ENABLE=true(variable d'environnement Docker) ;- ou
alerting_enable: truedansconfig/features.yml(relu à chaque vérification, sans redémarrage). Ce fichier est volontairement transversal : de futurs interrupteurs marche/arrêt simples pourront le rejoindre sans qu'un nouveau fichier soit créé à chaque fois. Il n'a aucun rapport avec la fonctionnalité « Nginx control » (reload/statut nginx) malgré la proximité de vocabulaire.
Une alerte non lue jamais vue dans la session en cours du navigateur
déclenche une modale forcée (rendue en Markdown, comme le Changelog) et
une notification navigateur (permission demandée seulement à ce
moment-là, jamais au chargement de la page, jamais redemandée après un
refus). Un bouton Lu par alerte la marque lue sans la reproposer. Sur la
page Système, un bouton Alertes (visible seulement si ALERTING_URL est
configurée) ouvre l'historique complet — lues et non lues — avec un
rafraîchissement manuel immédiat, utile juste après l'activation sans
attendre le prochain cycle automatique (ALERTING_POLL_INTERVAL_MIN, une
heure par défaut, minimum 5 minutes).
Côté stockage : aucune nouvelle base. Chaque alerte devient une notification ordinaire du centre de notification existant (voir « Centre de notification » plus haut), ce qui réutilise directement sa persistance, sa politique de rétention et sa route « marquer comme lu » — rien de nouveau à maintenir en parallèle. Un fichier distant inaccessible (pas de réseau, 404, DNS mort, timeout, taille excessive) ne génère jamais d'erreur visible : le cycle suivant réessaiera simplement.
Git configuré = source de vérité (v12.46.0, retours utilisateur)
Trois correctifs réunis sous le même principe : une fois Git configuré
(GIT_REPO_URL/git.yml), le dépôt est la source de vérité, et plus rien
d'autre ne doit pouvoir la contredire en silence.
Premier clone Git en échec après quelques heures d'usage (auparavant
« résolu » avec docker compose down -v && docker compose up -d, ce qui
marchait mais cachait la vraie cause). DIR_GIT_WORK (le volume
git-work) sert aussi d'espace de travail éphémère aux tests de
configuration (ALLOW_EDIT/ALLOW_CREATE), que Git soit configuré ou non.
Un test interrompu pouvait y laisser un répertoire temporaire
(.sandbox, .test-merge*), et git clone refuse un répertoire cible non
vide — bloquant le tout premier clone avec une erreur peu explicite. Le
dashboard nettoie désormais ces répertoires connus avant de cloner, et
refuse clairement (en nommant ce qui reste) si autre chose d'inattendu s'y
trouve, plutôt que de le supprimer à l'aveugle.
L'édition inline (ALLOW_EDIT) restait active malgré Git configuré.
Modifier un fichier en place pendant que Git est configuré crée un
changement que le dépôt ignore, écrasé silencieusement au prochain
déploiement. Même règle désormais que pour la création de fichiers
(ALLOW_CREATE, voir plus bas) : l'édition inline est désactivée dès que
Git est configuré, quelle que soit la valeur de ALLOW_EDIT.
La restauration d'une sauvegarde locale restait possible malgré Git
configuré, désynchronisant les deux sans que rien ne le signale ensuite.
La restauration est désormais refusée tant que Git est configuré ; le
bouton correspondant disparaît de la page Sauvegardes, avec une note
expliquant de passer par un git revert + redéploiement à la place.
Page Système (registre des paramètres du dashboard)
Une page qui répond à une question récurrente : « quelles variables
affectent le dashboard, et lesquelles ai-je réellement changées par rapport
au défaut ? ». Réservée aux admins (GET /api/system-info), elle liste tout
ce que lib/config.js résout depuis l'environnement — regroupé par
catégorie (Général, Comportement de déploiement, Sécurité & sessions,
Déploiement Git, Docker, Répertoires nginx, GeoIP, CrowdSec, GoAccess, Sync
& mises à jour, Rétention, Branding, Menu, GoDNS) — avec pour chaque paramètre :
- sa valeur actuelle ;
- sa valeur par défaut, avec une pastille visuelle quand la valeur actuelle en diffère ;
- comment le surcharger : variable d'environnement (le cas de loin le
plus fréquent), ou « fixe » pour ce qui ne se configure pas (comme
VERSION) ; - une courte explication.
NGINX_IMAGE a un traitement particulier, conformément à la façon dont le
dashboard l'utilise réellement (voir plus haut, section conteneur de test
éphémère) : laissée vide, elle est détectée automatiquement depuis l'image
du conteneur nginx en cours d'exécution, et la page l'indique explicitement
(« auto-détectée ») plutôt que d'afficher un champ vide — ou l'erreur de
détection le cas échéant (conteneur nginx introuvable, par exemple).
Secrets — jamais de valeur brute, même ici. SESSION_SECRET,
API_TOKEN, WEBHOOK_SECRET, GIT_TOKEN, GIT_SSH_KEY,
CROWDSEC_API_KEY et CROWDSEC_MACHINE_PASSWORD n'affichent jamais leur
contenu — uniquement un badge « Configuré » ou « Non configuré », calculé
côté serveur (lib/system-info.js) avant même que la réponse HTTP ne soit
construite. Une page réservée aux admins n'est pas une raison pour exposer
un secret au navigateur : le principe est le même que pour l'éditeur de
configuration ci-dessus, appliqué ici à des booléens plutôt qu'à un
masquage ******** réversible, puisque rien ici n'a besoin d'être
ré-affiché en clair.
Un second tableau, sous les catégories ci-dessus, liste les réglages qui ne
passent pas par une variable d'environnement mais par un fichier YAML dans
le répertoire de configuration (users.yml, smtp.yml, certbot.yml,
etc.) — chacun a déjà sa propre page dédiée dans le dashboard, listés ici
uniquement pour que l'inventaire reste complet.
Anti-dérive. Le registre (lib/system-info.js) est un tableau de
données à part, volontairement séparé de lib/config.js : une variable
d'environnement ajoutée là-bas sans être répercutée ici ferait dériver
silencieusement la documentation. test/system-info.test.js empêche cette
dérive : chaque clé exportée par lib/config.js doit apparaître soit comme
une entrée du registre, soit dans une liste explicite d'exclusions
volontaires (alias, valeurs dérivées comme les millisecondes converties en
heures, chemins internes) — jamais aucune des deux, jamais les deux.
Traductions (EN/FR)
Les dictionnaires EN/FR vivent dans public/assets/lang/en.json et
public/assets/lang/fr.json (extraits de index.html, même démarche que le
CSS — voir plus haut) et sont chargés au démarrage par loadTranslations()
avant le premier rendu. Un data-i18n="ma.cle" sur un élément, une clé
ma.cle présente dans les deux fichiers avec la même structure : c'est tout
le mécanisme.
Couverture actuelle, honnêtement incomplète. Les pages présentes dès la
V1 (Overview, Zones, Upstreams, Configs, SSL, Deploy, Control, Admin,
Webhooks, la carte en direct...) sont correctement internationalisées.
Configuration, Notifications, Scheduler, Système et Analyse le sont
désormais aussi, complétées en même temps que leur JS était extrait dans
leur(s) fichier(s) dédié(s) (voir ci-dessous) — les deux se font page par
page, dans la même passe. Notifications et Scheduler partagent le même
fichier JS et les mêmes clés de traduction (common.save,
common.saveSuccess, notify.rulesTitle, ...) : les deux pages éditent en
réalité le même notifications.yml par deux chemins différents, donc les
mêmes libellés y ont vraiment leur place. La page Analyse a fait le même
constat pour ses fenêtres temporelles (1h/24h/7j, common.window.*) :
partagées avec la page WAF pas encore traitée, elles sont dès maintenant
génériques plutôt que préfixées analyzer.*, pour ne pas devoir les
dupliquer quand WAF sera traitée à son tour. La carte en direct et la page
Analyse partagent aussi désormais les mêmes libellés de catégorie de robot
(geomap.legend.*, via gmCategoryLabel()) — Analyse lisait jusque-là
GM_CATEGORY_LABEL brut (toujours en français), ce que sa propre passe de
traduction a corrigé au passage.
Page Système : le contenu backend est traduit aussi, pas seulement le
chrome. Une première passe avait laissé de côté le nom des catégories et
la description de chaque paramètre, en français dans les deux langues, avec
la description en README que c'était un chantier séparé — trop vite : rien
n'empêchait de le faire dans la foulée, seulement plus de contenu à
traduire. C'est fait : public/assets/js/system-info.js fait passer
cat.name et entry.description par des tables de correspondance
(SYSINFO_CATEGORY_SLUG, SYSINFO_YAML_PURPOSE_SLUG) vers des clés
sysinfo.category.* / sysinfo.entryDesc.<KEY> / sysinfo.yamlPurpose.*,
avec repli gracieux sur le texte français brut du backend si une clé
venait à manquer (utile le jour où une entrée est ajoutée à
lib/system-info.js sans sa traduction : mieux vaut un intitulé en
français que sysinfo.entryDesc.XYZ affiché tel quel). Le message
d'erreur fixe de l'auto-détection de NGINX_IMAGE ("Conteneur nginx
introuvable") est traduit de la même façon ; les autres erreurs Docker,
elles, viennent du démon Docker lui-même et ne peuvent pas être traduites.
test/system-info.test.js vérifie qu'aucune catégorie, description ou
réglage YAML du registre backend n'échappe à cette table de
correspondance.
Limitation assumée pour la page Analyse. Le bloc affiché quand
l'analyseur n'est pas configuré (#an-disabled) garde son texte
d'instructions (chemins, exemple YAML) en français : c'est un mélange dense
de prose et de syntaxe de configuration littérale (noms de clés YAML), et le
découper proprement en plusieurs clés data-i18n aurait un coût largement
supérieur à sa valeur — ce bloc n'est visible que le temps de la première
configuration. Le reste de la page (titre, état de l'agent, alertes,
exceptions, tableau de trafic, tous les messages générés en JS) est
traduit.
Bug corrigé : le menu latéral restait en français pour les pages déjà
traduites. Traduire une page ne traduit pas son entrée de menu — ce sont
deux éléments HTML distincts. Configuration, Système et Analyse
avaient chacune leur page correctement traduite mais leur lien de menu
(nav-config-editor, nav-system-info, nav-analyzer) était resté un
texte brut sans data-i18n, jamais mis à jour par applyTranslations().
Corrigé pour les trois — WAF (voir plus bas) n'a pas eu besoin de ce
correctif, son entrée de menu étant le sigle "WAF", identique dans les deux
langues. nav-digest (page Résumé, voir plus bas) a reçu le même
traitement dès son passage. Les pages pas encore traduites (GoDNS, GeoIP,
Pages d'erreur) gardent volontairement leur entrée de menu en français
pour l'instant — cohérent avec leur page, qui l'est aussi encore.
Page WAF : traduite dans la foulée d'Analyse, dont elle dépend. Même
passe combinée (découpage JS + traduction) que les pages précédentes.
Réutilise largement les clés analyzer.* et common.window.* déjà en
place (agent injoignable, fin de liste, précédentes/suivantes, pagination,
fenêtres temporelles) plutôt que d'en dupliquer des équivalentes — c'est
tout le sens de les avoir rendues génériques lors du passage d'Analyse.
Comme pour Analyse, le bloc affiché quand la page n'est pas disponible
(#waf-disabled) est traduit malgré ses deux extraits de code
(<vhost>.waf.log, SecAuditLogFormat JSON) via un découpage en
plusieurs data-i18n autour de chaque <code> — plus simple ici qu'avec
Analyse, où le bloc équivalent était resté volontairement en français vu
sa densité. Le <pre> d'exemple de configuration ModSecurity, lui, reste
en l'état : ce sont des directives littérales, pas de la prose.
Page Résumé (digest) : traduite, en réutilisant largement les clés
analyzer.col.* déjà en place. Les en-têtes de statistiques (Requêtes,
Erreurs, Trafic, Robots) et le titre de section « Par vhost » sont
strictement identiques à ceux de la page Analyse — aucune nouvelle clé
n'a été créée pour eux, analyzer.col.requests/errors/traffic/bots et
analyzer.tabVhosts sont réutilisées telles quelles. Le bandeau « aucun
résumé généré » (qui référence digest et scheduler.yml) suit le même
découpage en plusieurs data-i18n autour des deux <code> que WAF et
Analyse. Toutes les phrases générées en JS (date/période de génération,
décisions CrowdSec actives, requêtes bloquées par le WAF, jours avant
expiration d'un certificat, sections indisponibles) passent par t()
avec variables ({n}, {date}, {period}, {list}). L'entrée de menu
nav-digest reçoit sa propre clé nav.digest (le libellé de menu
« Résumé » diffère du titre de page « Résumé périodique », donc pas de
réutilisation possible ici). Au passage, DIG_CATEGORY_LABEL, une table
de libellés jamais utilisée nulle part dans le code, a été supprimée
plutôt que traduite.
Page GoDNS : traduite, avec au passage deux bugs corrigés.
- Anglicismes incohérents avec le reste du dashboard : les boutons de
contrôle du conteneur affichaient "Start"/"Stop"/"Restart"/"Update" en
toutes lettres, alors que toutes les pages sœurs (Certbot, GeoIP, Pages
d'erreur, Analyse) utilisent "Démarrer"/"Arrêter"/"Mettre à jour
l'image". Corrigé en réutilisant telles quelles
analyzer.start,analyzer.stopetanalyzer.updateImage(zéro nouvelle clé), plus une nouvelle clégodns.restartpour "Redémarrer" (les autres pages n'ont pas de bouton "Restart" séparé). Même chose pour l'état du conteneur ("Running"/"Stopped"/"Absent" →godns.running/godns.stopped/godns.containerAbsent, alignés sur le phrasé des pages sœurs) et pour le message d'action en cours, qui affichait le mot-clé technique brut (start...) au lieu d'un libellé lisible. - Clés
certbot.title/certbot.sub/certbot.disabled/certbot.no.emailinversées entreen.jsonetfr.json— trouvé en cherchant des clés réutilisables pour GoDNS. Un utilisateur en français voyait ces quatre textes en anglais et inversement. Corrigé par un simple échange des valeurs ; aucune autre clé du dictionnaire n'a le même symptôme (vérifié par une recherche des caractères accentués français dansen.json). - Reste de la page : titre, sous-titre, bandeau "non configuré" (avec le
même découpage
data-i18nautour de<code>que WAF/Résumé), infos temps réel, tableau des domaines, éditeur de configuration (statuts de chargement/sauvegarde, confirmation de remplacement par le template, validation JSON), logs — tous traduits. 32 nouvelles clésgodns.*.
Page GeoIP (geoipupdate) : traduite, en réutilisant largement les clés
déjà posées pour Analyse et GoDNS. analyzer.start/analyzer.stop/
analyzer.updateImage couvrent les trois boutons d'action ; la deuxième
phrase du texte d'aide ("« Mettre à jour l'image » va chercher…") est
mot pour mot celle déjà traduite pour Analyse
(analyzer.imageUpdateHintPre/Post) — zéro nouvelle clé pour ces
concepts. godns.containerAbsent/godns.running/godns.stopped
réutilisées pour le statut du conteneur, avec une clé complémentaire
(geoip.startedAt) pour la date de démarrage que GoDNS n'affiche pas.
Le bandeau "license_key manquant" (avec son lien vers maxmind.com) et le
bandeau "non activé" suivent le même découpage en plusieurs data-i18n
que les pages précédentes. 26 nouvelles clés geoip.*.
Page Pages d'erreur (error-pages) : traduite — dernière page du
chantier. Réutilisation quasi totale des clés déjà posées pour GeoIP :
analyzer.start/analyzer.stop/analyzer.updateImage,
analyzer.imageUpdateHintPre/Post (phrase identique), et surtout
godns.containerAbsent/godns.running/godns.stopped/geoip.startedAt/
geoip.starting/geoip.checkingUpdate/geoip.updateImpossible/
geoip.newImageDownloaded/geoip.containerRecreated/
geoip.imageAlreadyUpToDate/geoip.configurePre — cette page suit
exactement le même schéma de statut de conteneur et de mise à jour
d'image que GeoIP, donc pratiquement aucune clé neuve pour cette partie.
Seulement 13 nouvelles clés errorPages.* : titre (page + menu, qui
diffèrent l'un de l'autre nulle part ici — même texte, mais l'entrée de
menu n'avait pas de data-i18n du tout), bandeau "non activé", rappel du
câblage nginx (global-error.conf), thème actif/configuré, lien vers la
liste des thèmes, avertissement de désynchronisation de thème,
confirmation d'arrêt.
Chantier i18n + découpage JS terminé. Les 12 pages du dashboard sont
maintenant toutes traduites EN/FR et leur JavaScript spécifique vit dans
public/assets/js/*.js plutôt qu'inline dans index.html : Configuration,
Système, Analyse, WAF, Résumé, GoDNS, GeoIP, Pages d'erreur (plus les
pages traitées avant ce chantier). Au passage, deux bugs de traduction
préexistants ont été trouvés et corrigés (menu latéral pas traduit pour
plusieurs pages, clés certbot.* inversées entre en.json et
fr.json) et deux incohérences de phrasé (anglicismes sur GoDNS, libellé
de catégorie bot dupliqué entre la carte et Analyse). Voir CHANGELOG.md
pour le détail page par page.
Garde-fou anti-régression. test/frontend.test.js vérifie désormais,
pour l'ensemble du fichier (pas seulement une page ciblée), que toute clé
utilisée via t('ma.cle') ou data-i18n="ma.cle" existe bien dans les deux
dictionnaires — une clé oubliée dans l'un des deux casse le test au lieu de
n'afficher qu'une clé brute à l'écran, découverte plus tard par un
utilisateur.
Découpage JS par page. Le JS spécifique à une page migre dans
public/assets/js/<page>.js, un fichier par page traitée, chargé via
<script src="/assets/js/<page>.js"> juste après le script principal —
un script classique (pas un module ES), qui partage donc la même portée
globale et peut appeler api(), t() et les autres helpers déjà définis,
exactement comme le code inline qu'il remplace.
Conteneurs gérés par le dashboard et redémarrage de l'hôte
Certbot (HTTP et DNS), geoipupdate, error-pages et l'analyzer sont chacun
créés avec RestartPolicy: unless-stopped : Docker les redémarre lui-même
après un redémarrage du serveur — mais seulement s'ils existent déjà.
Ça ne couvre pas deux cas : la toute première fois qu'on passe
enable: true (le conteneur n'a jamais été créé) et après avoir cliqué
« Arrêter » dans le dashboard (qui supprime le conteneur, pas seulement
docker stop).
Dans ces deux cas, seul le dashboard lui-même peut s'en apercevoir — Docker
n'a pas d'avis sur un conteneur qu'on ne lui a jamais demandé de créer. À
son propre démarrage, le dashboard vérifie donc chacun d'eux : si
enable: true dans son fichier de configuration mais que le conteneur
n'existe pas, il le recrée automatiquement (silencieusement en cas de
succès ; un avertissement dans les logs du dashboard sinon, jamais une
erreur bloquante). Un conteneur volontairement arrêté depuis la page
correspondante n'est jamais relancé de force par ce mécanisme — seul
« absent alors qu'activé » déclenche une recréation. Pour l'analyzer,
host_data_path (l'historique et la baseline y vivent) doit en plus être
renseigné, sans quoi la reconstitution automatique est refusée plutôt que
de recréer un conteneur qui perdrait son état au premier redémarrage. Pour
Certbot DNS, c'est certs_host_path qui joue ce rôle.
Mise à jour des images des conteneurs gérés
Certbot (HTTP et DNS), geoipupdate, error-pages et l'analyzer utilisent chacun une image publique pré-construite. Chaque page correspondante (Certbot, GeoIP, Pages d'erreur, Analyse) propose un bouton « Mettre à jour l'image », à côté de Démarrer/Arrêter, qui affiche l'image concernée et effectue les étapes suivantes :
- Télécharge la dernière version de l'image configurée (
docker pulléquivalent, via l'API Docker). - Ne recrée le conteneur en cours d'exécution que si l'image a réellement changé — un pull seul n'a aucun effet sur un conteneur déjà démarré, tant qu'il n'est pas explicitement recréé.
- Si le conteneur n'existait pas ou était arrêté, seule l'image est téléchargée ; rien n'est démarré à sa place (cliquer Démarrer ensuite si besoin).
Un échec de téléchargement (tag inexistant, dépôt indisponible) est signalé clairement plutôt que silencieusement ignoré — l'API Docker répond toujours HTTP 200 sur cet appel, y compris en cas d'échec ; l'erreur réelle est extraite du flux de progression retourné par Docker.
Certbot — défi DNS (Cloudflare et autres fournisseurs)
Complémentaire au défi HTTP-01 (section Certbot plus haut), pas un
remplacement : le défi DNS-01 est le seul moyen d'obtenir un certificat
wildcard (*.exemple.com), et fonctionne même quand le port 80 n'est
pas exposé publiquement. Géré depuis la même page Certbot, dans la
section « Défi DNS » — démarrage, arrêt, mise à jour de l'image,
recréation automatique après redémarrage du serveur, sur le même modèle
que les autres conteneurs gérés.
Fournisseurs. Cloudflare, OVH, DigitalOcean et Google Cloud DNS ont des
réglages par défaut déjà corrects (image, nom du plugin, format des
identifiants). Un autre fournisseur certbot-dns-* fonctionne aussi : mettre
provider: custom dans certbot-dns.yml et renseigner container_image,
plugin_flag, credentials_flag et propagation_flag à la main (voir la
documentation du plugin concerné). AWS Route 53 n'est volontairement pas
dans la liste par défaut : son plugin s'authentifie par variables
d'environnement plutôt que par fichier d'identifiants, un mécanisme
différent de celui couvert ici — il reste accessible via provider: custom
mais demande un réglage plus manuel.
- Créer le fichier d'identifiants du fournisseur sur l'hôte (jamais lu ni
écrit par le dashboard — même règle que
license_keypour GeoIP), par exemple pour Cloudflare :dns_cloudflare_api_token = VOTRE_TOKEN_API - Renseigner
nginx-dashboard/config/certbot-dns.yml:enable: true provider: cloudflare email: admin@example.com credentials_host_path: /chemin/absolu/hote/vers/credentials.ini certs_host_path: /chemin/absolu/hote/vers/certscerts_host_pathdoit pointer vers le même dossier hôte que celui monté en./certsdansdocker-compose.yml(le même quecertbot.ymly utilise) — voir « risque de conflit » plus bas. - Depuis la page Certbot → section Défi DNS : Démarrer, puis
dans le champ Émettre un ou plusieurs certificats, une ligne par
certificat, domaines séparés par une virgule :
Chaque ligne devient sa propre commandedomain-a.tld, *.domain-a.tld domain-b.tld, *.domain-b.tldcertbot certonly, exécutée l'une après l'autre — reproduit exactement l'exemple à plusieurs certificats dudocker-compose.ymlracine, en un clic plutôt qu'un script figé.
Risque de conflit entre le défi DNS et le défi HTTP. Aucun risque côté
stockage : les deux méthodes écrivent dans le même /etc/letsencrypt, et
c'est le fonctionnement normal de certbot — chaque certificat garde, dans
son propre fichier de renouvellement, le nom de l'authentificateur qui l'a
émis, et certbot renew l'utilise automatiquement, quel que soit le
conteneur qui lance la commande. Le seul vrai risque : redemander le même
domaine avec les deux méthodes. La seconde émission écrase simplement
l'authentificateur enregistré pour ce domaine — ce n'est pas une fusion
propre, c'est le second qui gagne. Le dashboard applique la même
vérification de conflit qu'à l'émission HTTP (domaine déjà couvert, exact
ou wildcard) avant de lancer une émission DNS, donc ce cas précis est
bloqué avant d'aboutir plutôt que découvert après coup.
Analyseur de journaux (analyzer)
Le dashboard gère entièrement le cycle de vie du conteneur d'analyse des
journaux depuis sa page Analyse — démarrage, arrêt, mise à jour de
l'image, recréation automatique après un redémarrage du serveur — sur le
même modèle que certbot/geoipupdate/error-pages. Contrairement aux trois
autres, l'analyzer utilisait auparavant une image construite localement
via le service nginx-analyzer du docker-compose.yml racine
(build: context: ./nginx-analyzer) ; le faire gérer par le dashboard
suppose de publier l'image sur un dépôt (par exemple
forge.rdr-it.com/dockerfiles/nginx-analyzer) plutôt que de la
reconstruire à chaque docker compose up.
- Renseigner
nginx-dashboard/config/analyzer.yml:
Comme pourenable: true container_image: forge.rdr-it.com/dockerfiles/nginx-analyzer:1.0.0 host_logs_path: /chemin/absolu/hote/vers/logs host_data_path: /chemin/absolu/hote/vers/analyzer host_geoip_path: /chemin/absolu/hote/vers/geoip_datawebroot_host_path/certs_host_path(Certbot) etgeoip_host_path(GeoIP), ces trois chemins doivent être les chemins hôte absolus des dossiers montés dansdocker-compose.yml— le dashboard ne peut pas déduire ces chemins depuis l'intérieur de son propre conteneur.host_data_pathest le plus important des trois : c'est là que vit la base SQLite (historique, baseline, alertes) — sans lui, la reconstitution automatique après redémarrage est refusée plutôt que de risquer de perdre cet état. - Depuis la page Analyse : Démarrer.
⚠️ Si vous préférez continuer à construire l'image localement via le
service nginx-analyzer du docker-compose.yml racine (désormais
commenté par défaut), décommentez-le et laissez enable: false dans
analyzer.yml — les deux méthodes se disputent le même nom de conteneur
(ngx-analyzer).
GeoIP (geoipupdate)
Le dashboard peut gérer lui-même le cycle de vie d'un conteneur
maxmindinc/geoipupdate — démarrage, arrêt, mise à jour immédiate — depuis
sa page GeoIP, sans passer par le profil compose geo/all (les deux
méthodes restent utilisables séparément, voir le commentaire du service
geoipupdate dans docker-compose.yml).
- Créer un compte MaxMind gratuit : https://www.maxmind.com/en/geolite2/signup
- Renseigner
nginx-dashboard/config/geoipupdate.yml:enable: true account_id: VOTRE_ID license_key: VOTRE_CLE edition_ids: GeoLite2-City GeoLite2-Country GeoLite2-ASN frequency_hours: 168 geoip_host_path: /chemin/absolu/hote/vers/geoip_datageoip_host_pathdoit être le chemin hôte absolu du dossier monté en./geoip_datadansdocker-compose.yml— le dashboard ne peut pas déduire ce chemin depuis l'intérieur de son propre conteneur (même limitation quewebroot_host_path/certs_host_pathpour Certbot). - Depuis la page GeoIP : Démarrer, puis Mettre à jour maintenant
pour une première synchronisation sans attendre
frequency_hours.
L'image officielle boucle en interne (télécharge, attend frequency_hours,
recommence) : le dashboard ne fait qu'en gérer le cycle de vie, jamais
l'ordonnancement lui-même. license_key n'est jamais renvoyée au
navigateur — seul un indicateur « configurée ou non » l'est.
Mettre à jour l'image (bouton dédié, voir plus haut) tire une nouvelle
version de maxmindinc/geoipupdate et recrée le conteneur seulement si
l'image a changé — distinct de « Mettre à jour maintenant », qui force une
synchronisation immédiate des bases avec l'image déjà en place.
Pages d'erreur (error-pages)
Le dashboard peut gérer le cycle de vie d'un conteneur
tarampampam/error-pages depuis sa page Pages d'erreur — démarrage,
arrêt, changement de thème. C'est le plus simple des trois conteneurs
gérés : ni secret, ni volume, aucun chemin hôte à renseigner.
- Renseigner
nginx-dashboard/config/error-pages.yml:
Liste des thèmes disponibles : https://github.com/tarampampam/error-pages#-templatesenable: true container_image: tarampampam/error-pages:latest template_name: connection - Le câblage nginx est déjà fourni tel quel dans
global-error.conf(fichier d'exemple du projet) : rien à modifier côté nginx si vous l'utilisez tel quel — il attend un conteneur joignable sous le nom Docker exacterror-pagessur le port 8080, c'est pourquoi ce conteneur n'est pas préfixénginx-dashboard-…comme les deux autres. - Depuis la page Pages d'erreur : Démarrer.
Le thème est fixé au démarrage du conteneur (variable d'environnement,
jamais relue en cours de route) — changer template_name dans le fichier
puis cliquer de nouveau sur Démarrer recrée le conteneur avec la
nouvelle valeur ; un simple redémarrage ne suffirait pas.
⚠️ Si vous utilisez aussi le service error-pages du docker-compose.yml
racine (actif par défaut, sans profil), choisissez l'une des deux méthodes :
les deux ensemble se disputent le même nom de conteneur.
Mettre à jour l'image (voir plus haut) tire une nouvelle version de
tarampampam/error-pages et recrée le conteneur seulement si l'image a
changé, en conservant le thème actuellement configuré.
Commandes utiles
# Test et reload nginx
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
# Logs en temps réel
docker compose logs -f nginx
docker compose logs -f nginx-dashboard
# Rebuild dashboard
docker compose build nginx-dashboard
docker compose up -d --force-recreate nginx-dashboard
# Conteneurs GoAccess actifs
docker ps --filter "name=ngx-goaccess-"
# Vérifier le réseau Docker
docker network ls | grep nginx
# Générer une clé CrowdSec
docker exec crowdsec cscli bouncers add nginx-dashboard