No description
  • JavaScript 72.9%
  • Go 20.1%
  • HTML 5.1%
  • CSS 1.2%
  • Dockerfile 0.4%
  • Other 0.3%
Find a file
2026-10-06 15:20:53 +02:00
.dev Update alert-message 2026-09-29 08:52:05 +02:00
.forgejo/workflows update workflows 2026-10-02 00:03:23 +02:00
Docs 12.20.0 2026-10-03 18:19:23 +02:00
nginx-agent 12.20.9 2026-10-06 11:26:41 +02:00
nginx-analyzer 12.20.13 2026-10-06 15:04:35 +02:00
nginx-analyzer-go 12.20.13 2026-10-06 15:04:35 +02:00
nginx-challenge 12.20.11 2026-10-06 12:33:42 +02:00
nginx-dashboard 12.20.14 2026-10-06 15:20:53 +02:00
CHANGELOG.md 12.20.14 2026-10-06 15:20:53 +02:00
docker-compose.yml 12.20.7 2026-10-05 15:34:53 +02:00
ngx-dash-current-version update info version 2026-09-20 20:26:28 +02:00
README.md 12.20.12 2026-10-06 14:44:39 +02:00
sample.env 12.20.7 2026-10-05 15:34:53 +02:00

Nginx Reverse Proxy Stack

Nginx Dashboard Node

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.conf via 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) → format combined
  • Premier token = hostname (example.com) → format combined_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, voir sample.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 -waf ou -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.yml Défaut Valeurs
    MENU_WAF waf_menu auto auto (détecte le suffixe -waf/-coraza de l'image nginx en cours), show, hide
    MENU_GODNS godns_menu show show, hide (pas de mode auto : rien dans une image de conteneur n'indique un usage de DNS dynamique)
    MENU_API api_menu auto auto (masqué si API_TOKEN absent), show, hide
    MENU_WEBHOOKS webhooks_menu auto auto (masqué si WEBHOOK_SECRET absent), show, hide
    MENU_DOCKER_AUTOCONFIG docker_autoconfig_menu auto auto (masqué si enable: false dans docker-autoconfig.yml), show, hide
    MENU_AGENTS agents_menu auto auto (masqué si enable: false dans agents.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és
    

    L'é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.yml en conservant ses commentaires ; l'édition manuelle du fichier reste valable. Entrées supplémentaires masquables (défaut show) : sync_menu, vhostgen_menu, graph_menu, deploy_menu, blocklists_menu, wafemu_menu, geomap_menu, digest_menu, cache_menu, notifications_menu (variables MENU_CACHE, MENU_NOTIFICATIONS, MENU_SYNC, MENU_VHOSTGEN, MENU_GRAPH, MENU_DEPLOY, MENU_BLOCKLISTS, MENU_WAFEMU, MENU_GEOMAP, MENU_DIGEST) et goaccess_menu (auto = affiché si des sources sont configurées, ou hide ; MENU_GOACCESS). Une variable MENU_* 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.yml d'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-*.conf du dossier snippets/ — génère include snippets/ssl-xxx.conf
  • Certificats manuels du dossier ssl/ — pré-remplit ssl_certificate et ssl_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 directive return 301 https://... ni rewrite ... 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, pas WARNING : 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://...) avec proxy_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 ; si proxy_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ÉSOLU dans 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 bloc server{} du fichier — exclut le fichier entier : aucune card Diagnostic pour lui.
  • # nginx-control-diagnostic-vhost: off, à l'intérieur d'un bloc server{} — 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 (ou off, ou absent — équivalent à off) active ou non le monitoring pour ce bloc ; chaque bloc server{} d'un fichier a son propre flag, indépendamment des autres.
  • # nginx-control-monitoring-interval: 60s fixe l'intervalle entre deux sondes pour ce bloc ; par défaut 60 s, plancher forcé à 10 s (une faute de frappe du type 1s ne peut pas transformer ceci en bombardement du backend).
  • # nginx-control-monitoring-valid-http-code: 2xx, 3xx surcharge la règle par défaut décidant si une cible est UP ou DOWN. Par défaut (flag absent), une réponse 2xx/3xx/4xx compte comme UP et seul un 5xx (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 un 404 normal — sans surcharge, ce 404 reste 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é dans lib/monitor-status.js/test/monitor-status.test.js.
  • # nginx-control-monitoring-ignore-location: on (ou off/absent — équivalent à off), posé à l'intérieur d'une location, exclut uniquement cette location du monitoring sans toucher au flag du bloc ni aux autres locations. Utile quand plusieurs location d'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 en UPSTREAM (LB/HA) — chaque membre du pool suivi individuellement), hors locations marquées ignore-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 -k implicite, 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 son INSTANCE_ID (unique, persistant, page Système). Confiance faible côté serveur.
  • identified : INSTANCE_ID + user (e-mail) + jeton (Authorization: Bearer). Jeton : ENV INTEL_TOKEN (prioritaire) sinon token: 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 (1 brute-force, 2 scan, 3 flood, 4 scraping, 5 anomalie volumétrique par vhost, 6 anomalie 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, medium ou high (défaut medium).
  • 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) ou global — 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 atteint min_matches et que min_ips IP 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 : un blocklist_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) : avec scope: global, nombre minimum d'IP distinctes (entier ≥ 1, défaut 5).
  • blocklist_remediation_type (v12.63.0) : block (défaut, refus 403/444) ou challenge (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éfaut true) — 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.js envoie un email, avec ses propres règles d'activation par type (config/notifications.yml) — aucune notion de lu/non-lu.
  • lib/events.js est 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 crowdsec vs 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

  1. challenge_enable: true dans config/blocklists.yml (autres clés challenge_* documentées dans ce fichier).
  2. Démarrer le service : docker compose --profile challenge up -d nginx-challenge (définir NC_SECRET dans .env, ≥ 32 caractères, pour que les cookies survivent aux redémarrages).
  3. Dans chaque vhost protégé, après include snippets/blocklist-enforce.conf; : include snippets/challenge-gate.conf; (une seule fois par server{} ; le snippet est un fichier vide de directives tant que le challenge est désactivé, donc l'inclusion ne peut jamais casser nginx -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_request et error_page 401 sont posés au niveau server{} : une location qui définit ses propres error_page n'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/Host posé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'image nginx-challenge pour 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+K ou /) — 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 son inspect (pull, renommage, création, démarrage, retour arrière si échec) ; nginx et le dashboard ne sont que redémarrables (ils vivent dans votre docker-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 format combined ne 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, VACUUM si l'espace récupérable dépasse DB_COMPACT_MIN_FREE_MB (16) et DB_COMPACT_MIN_FREE_PCT (20) ; DB_COMPACT_ON_START=auto|always|off. Ensuite, checkpoint WAL + incremental_vacuum toutes 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 table geo $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.conf pour tout conf/ via include 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; } ou 444 selon block_action). Comme tous les fichiers de snippets/, il n'est jamais chargé automatiquement — c'est un opt-in par vhost, à ajouter dans chaque server{} à 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, dans blocklist-enforce.conf, une instruction access_log /var/log/nginx/blocklist-hits.log blocklist_hits if=$blocklist_ip; (chemin tel que le conteneur nginx le voit — jamais le DIR_LOGS interne au dashboard, qui n'existe pas dans le conteneur nginx et ferait échouer nginx -t) indépendante du bloc if ($blocklist_ip) { return ...; } existant — pas imbriquée dedans, et sans toucher à logging.conf ni au format de log partagé. Le paramètre if= rend la ligne conditionnelle nativement, côté nginx. Son log_format est 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 que blocklist-ips.conf), toujours présent même quand hit_logging_enable est à 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 mode dedicated — 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=txt dans 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 -t est exécuté sur la configuration générée avant nginx -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.conf et snippets/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.conf ne peut donc jamais faire échouer nginx -t simplement 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 (permission VIEW_CONFIGS)
  • POST /api/blocklists/refresh — rafraîchissement manuel immédiat (permission DEPLOY)
  • GET /api/blocklists/check?ip=... — dans quelle(s) liste(s) une IP se trouve (permission VIEW_CONFIGS)
  • GET /api/blocklists/hit-stats?hours=24 — hits, IP distinctes, répartition par source (permission VIEW_CONFIGS)
  • GET /api/blocklists/export — télécharger/visualiser une liste (permission VIEW_CONFIGS) :
    • ?source=<nom> (omis ou all = fusion de toutes les sources activées, l'ensemble exact écrit dans la table geo)
    • ?format=txt (une IP/CIDR par ligne, text/plain ; sans format : JSON structuré {source, type, count, generatedAt, ips})
    • &download=1 force 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 (source analyzer uniquement) expose les IP détectées même sans remediation: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 source type: 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 et features/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 un server_name sont agrégés en un seul vhost avec un bloc upstream {}, 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 up d'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 jamais nginx-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_name accepte plusieurs noms séparés par un espace ou une virgule.
  • location<NN>/location<NN>.proxy_pass (ou location<NN>.target, alias identique — pratique si vous réutilisez les mêmes labels que sur nginx-agent, voir « Hôtes Docker distants » plus bas, .target étant prioritaire si les deux sont posés) : NN est 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 de ssl_certificate : 80 en HTTP simple, 443 dès que du SSL est demandé (snippet ou auto) — 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=true seul 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écessite ssl_certificate.snippet, le nom d'un fichier existant dans snippets/ (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 le server_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 bloc ssl_certificate pointant 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 : comme auto, 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_minutes dans config/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 dans GET /api/docker-autoconfig/status (status: "ssl_error", ssl.error.code: "certbot_issuance_failed") au lieu d'un pending silencieux indéfini.
  • http_to_https_auto=true : ajoute un second bloc server{} 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 mode auto en 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 que ssl_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: true dans config/docker-autoconfig.yml pour 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: false masque 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 (voir MENU_DOCKER_AUTOCONFIG dans 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, cible proxy_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) : un server_name qui ne correspond à aucun motif de allowed_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 -t est exécuté avant tout nginx -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_*.conf ne 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 liste decisions (toutes les décisions mémorisées, y compris sans conteneur vivant) (permission VIEW_CONFIGS)
  • POST /api/docker-autoconfig/rescan — force un cycle de détection immédiat (permission DEPLOY)
  • POST /api/docker-autoconfig/approve — { "serverName": "..." }, approuve et applique (permission DEPLOY)
  • POST /api/docker-autoconfig/reject — { "serverName": "..." }, rejette (permission DEPLOY)
  • POST /api/docker-autoconfig/pause — { "serverName": "..." }, met en pause une approbation existante sans la perdre (permission DEPLOY)
  • POST /api/docker-autoconfig/resume — { "serverName": "..." }, reprend une publication en pause (permission DEPLOY)
  • POST /api/docker-autoconfig/decisions/remove — { "serverName": "..." }, révoque définitivement une décision mémorisée, avec ou sans conteneur vivant (permission DEPLOY)

É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éé par POST /api/agent/enroll (seule route publique de cette fonctionnalité, désactivable via config/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 jeton agt_<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 compris GET /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 agent ne 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 (avec sslCertificateSnippet, un fichier de snippets/) / 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_dns ne 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 directe scheme://host:port que 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-passe resolver 127.0.0.11 / set $backendNN n'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_name vé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 -t puis nginx -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: true dans config/agents.yml pour 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: false masque 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 (voir MENU_AGENTS dans 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 (permission VIEW_CONFIGS), chaque agent inclut vhosts : la fusion des derniers vhosts publiés (lastVhosts) et des décisions de pause enregistrées, avec ou sans vhost actuellement publié correspondant
  • POST /api/agents/<id>/approve | /reject | /revoke | /regenerate-token (permission DEPLOY)
  • POST /api/agents/<id>/vhosts/pause | /resume — { "serverNames": ["..."] } (permission DEPLOY)
  • DELETE /api/agents/<id> — uniquement rejected/revoked (permission DEPLOY)
  • POST /api/agent/manifest — Bearer token de l'agent, réservé à cette seule route
  • GET /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 CSS width: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 (Dockerfile des deux images installe tzdata) tournent en UTC par défaut si TZ n'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éfinir TZ sur le fuseau réel du serveur nginx (environment: TZ=Europe/Paris dans docker-compose.yml, pour le service nginx-dashboard et/ou nginx-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_SECRET gé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_TOKEN n'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 dans config/.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 (permissions 0600, é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'un docker inspect du 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) : :ro protè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 pour nginx -t / nginx -s reload et 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 de server_name jamais résolu en silence — voir « Auto-config Docker »
  • Générer des secrets : openssl rand -hex 32
  • Les images nginx-dashboard, nginx-analyzer et nginx-agent tournent toutes en root à l'intérieur de leur conteneur (aucun USER non-root dans leurs Dockerfile) — 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 simple USER nobody largement 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 groupe docker de 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 de USER seul.
  • APP_VERSION=dev par défaut sur une installation via docker-compose.yml si celui-ci n'a pas été modifié pour transmettre --build-arg APP_VERSION=... (voir Dockerfile) : 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-mono et @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_URL tous 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-status expose l'état courant (allowCreate, gitConfigured, enabled) pour que l'interface masque le bouton quand l'une des deux conditions manque. Seules les quatre sections sites/conf/snippets/streams sont concernées (pas ssl/, 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.yml est importé une fois ; il est ensuite ignoré.
  • Droits : lecture view_configs ; création, modification, exécution, import nginx_control.
  • Ajouter une action : une entrée dans lib/scheduler-tasks.js (libellés fr/en, schéma des paramètres, fonction run) — 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.yml et notifications.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'appel sendNotification('<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éfaut api.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 (permission DEPLOY).

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: true dans config/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.stop et analyzer.updateImage (zéro nouvelle clé), plus une nouvelle clé godns.restart pour "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.email inversées entre en.json et fr.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 dans en.json).
  • Reste de la page : titre, sous-titre, bandeau "non configuré" (avec le même découpage data-i18n autour 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és godns.*.

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 :

  1. Télécharge la dernière version de l'image configurée (docker pull équivalent, via l'API Docker).
  2. 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éé.
  3. 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.

  1. 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_key pour GeoIP), par exemple pour Cloudflare :
    dns_cloudflare_api_token = VOTRE_TOKEN_API
    
  2. 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/certs
    
    certs_host_path doit pointer vers le même dossier hôte que celui monté en ./certs dans docker-compose.yml (le même que certbot.yml y utilise) — voir « risque de conflit » plus bas.
  3. 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 :
    domain-a.tld, *.domain-a.tld
    domain-b.tld, *.domain-b.tld
    
    Chaque ligne devient sa propre commande certbot certonly, exécutée l'une après l'autre — reproduit exactement l'exemple à plusieurs certificats du docker-compose.yml racine, 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.

  1. Renseigner nginx-dashboard/config/analyzer.yml :
    enable: 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_data
    
    Comme pour webroot_host_path/certs_host_path (Certbot) et geoip_host_path (GeoIP), ces trois chemins doivent être les chemins hôte absolus des dossiers montés dans docker-compose.yml — le dashboard ne peut pas déduire ces chemins depuis l'intérieur de son propre conteneur. host_data_path est 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.
  2. 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).

  1. Créer un compte MaxMind gratuit : https://www.maxmind.com/en/geolite2/signup
  2. 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_data
    
    geoip_host_path doit être le chemin hôte absolu du dossier monté en ./geoip_data dans docker-compose.yml — le dashboard ne peut pas déduire ce chemin depuis l'intérieur de son propre conteneur (même limitation que webroot_host_path/certs_host_path pour Certbot).
  3. 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.

  1. Renseigner nginx-dashboard/config/error-pages.yml :
    enable: true
    container_image: tarampampam/error-pages:latest
    template_name: connection
    
    Liste des thèmes disponibles : https://github.com/tarampampam/error-pages#-templates
  2. 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 exact error-pages sur le port 8080, c'est pourquoi ce conteneur n'est pas préfixé nginx-dashboard-… comme les deux autres.
  3. 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