Installation de Nginx Proxy Manager avec Docker

Installation de Nginx Proxy Manager avec Docker

Mise en place d'un reverse proxy Nginx Proxy Manager (NPM) sur la VM Docker du lab interne ANSSI : pile docker compose (App + MariaDB), création des proxy hosts pour les services Web internes, certificats auto-signés générés par NPM et bonnes pratiques HTTPS (Force SSL, HSTS, websockets).
Publié le
Statut
Terminé

Contexte et besoins

Dans le cadre de mon alternance, le lab interne héberge plusieurs services Web internes (un gestionnaire de mots de passe, un outil de gestion de parc, un service de monitoring, etc.). Chacun écoute sur un port différent et expose sa propre page de login. C’est inutilisable au quotidien : les navigateurs hurlent à chaque visite à cause des certificats par défaut, les apps mobiles refusent de se connecter, et l’équipe doit retenir un port différent pour chaque outil.

L’objectif de cette intervention : déployer un reverse proxy unique sur le lab qui (1) écoute sur les ports 80/443 standard, (2) route chaque sous-domaine *.lab.local vers le bon service interne, (3) fournisse un certificat HTTPS signé pour chaque service, et (4) centralise l’administration des proxy hosts. Nginx Proxy Manager (NPM) coche ces cases avec une interface Web simple, posée sur Nginx + Certbot pilotés par une API.

Rappel théorique sur le reverse proxy et HTTPS

Reverse proxy

Un reverse proxy est un serveur HTTP/HTTPS qui se place devant d’autres serveurs Web et leur transmet les requêtes. Du point de vue du client, il y a une seule URL et un seul certificat ; côté backend, la requête est routée selon le Host: ou le path. Avantages :

  • un seul point d’entrée : un seul couple ports/certificats pour tous les services ;
  • terminaison TLS centralisée : les services backend peuvent rester en HTTP en interne, le reverse proxy gère le HTTPS ;
  • load balancing possible vers plusieurs backends ;
  • filtrage commun (rate limiting, blocage de patterns d’exploitation) appliqué avant d’arriver aux applications.

NPM : architecture

NPM est livré comme deux conteneurs Docker :

  • app : Nginx + Node.js + Certbot, expose les ports 80/443 (proxy) et 81 (interface admin) ;
  • db : MariaDB pour la persistance des proxy hosts, certificats et utilisateurs.

Tout l’état (config Nginx générée, certificats, base de données) est stocké dans des volumes Docker, ce qui permet de migrer ou sauvegarder facilement.

Certificats auto-signés et confiance interne

Les services du lab restent internes : pas de publication sur Internet, pas de domaine public, donc Let’s Encrypt n’a pas de sens ici. NPM peut générer des certificats auto-signés directement depuis l’interface : il fabrique une autorité interne propre à NPM, signe un certificat pour chaque proxy host, et fournit le certificat racine à télécharger pour qu’on l’ajoute au magasin de confiance des postes admin.

Topologie

ÉquipementRôleAdresse
VM DOCKER-MICROSERVICES (Debian 12)Hôte Docker pour NPM192.168.10.150/24
Backend Web 1 (web-interne)Service Web interne sur HTTP/8000192.168.10.151:8000
Backend Web 2 (gestion-parc-interne)Service Web interne sur HTTP/80192.168.10.152:80
Backend Web 3 (monitoring-interne)Service Web interne sur HTTP/8080192.168.10.153:8080
DNS interne (dc.lab.local)Résolution *.lab.local192.168.10.250

Prérequis

  • VM DOCKER-MICROSERVICES avec Docker CE et le plugin Compose installés.
  • Trois enregistrements A côté DNS interne pointant vers l’IP de NPM :
    • web.lab.local → 192.168.10.150
    • gestion-parc.lab.local → 192.168.10.150
    • monitoring.lab.local → 192.168.10.150
  • Compte sudo sur la VM pour piloter Docker.

Préparation de la stack

BASH
sudo mkdir -p /opt/docker-compose/npm
cd /opt/docker-compose/npm
mkdir -p data letsencrypt mariadb
Cliquez pour développer et voir plus

Fichier .env

BASH
cat > .env <<'EOF'
DB_ROOT_PASSWORD=<passphrase générée>
DB_NPM_PASSWORD=<passphrase générée>
EOF

chmod 600 .env
Cliquez pour développer et voir plus

docker-compose.yml

YAML
services:
  app:
    image: jc21/nginx-proxy-manager:latest
    container_name: npm
    restart: unless-stopped
    ports:
      - "80:80"        # HTTP (redirection vers HTTPS)
      - "443:443"      # HTTPS (proxy)
      - "81:81"        # Interface admin
    environment:
      DB_MYSQL_HOST: db
      DB_MYSQL_PORT: 3306
      DB_MYSQL_USER: npmuser
      DB_MYSQL_PASSWORD: ${DB_NPM_PASSWORD}
      DB_MYSQL_NAME: npm
      DISABLE_IPV6: 'true'
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db
    networks:
      - npm

  db:
    image: mariadb:11
    container_name: npm-db
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: npm
      MARIADB_USER: npmuser
      MARIADB_PASSWORD: ${DB_NPM_PASSWORD}
    volumes:
      - ./mariadb:/var/lib/mysql
    networks:
      - npm

networks:
  npm:
    driver: bridge
Cliquez pour développer et voir plus

Explications ligne par ligne :

  • ports 80/443/81 : les ports 80 et 443 doivent rester libres sur l’hôte, donc pas d’autre service Web sur la VM. Le 81 est l’interface admin, à restreindre au LAN au niveau pare-feu.
  • DB_MYSQL_HOST: db : le service NPM parle au service MariaDB par son nom Docker, sur le réseau npm. Pas besoin d’exposer MariaDB sur l’hôte.
  • DISABLE_IPV6: 'true' : le réseau du lab est en IPv4 only. Sans ça, NPM tente d’écouter en IPv6 et logue des erreurs.
  • volumes ./data et ./letsencrypt : persistance — la base SQLite des proxy hosts, la config Nginx générée, et les certificats émis. À sauvegarder.
  • mariadb:11 : version courante de MariaDB en 2026. Je fige la version majeure pour éviter qu’un upgrade transparent casse la compatibilité avec NPM.

Démarrage

BASH
docker compose pull
docker compose up -d
docker compose logs -f
Cliquez pour développer et voir plus

À l’issue du premier démarrage, je vérifie :

BASH
docker compose ps
# NAME      STATUS
# npm       Up    healthy
# npm-db    Up    healthy

# Test HTTP local
curl -I http://localhost:81
# HTTP/1.1 200 OK
Cliquez pour développer et voir plus

Premier accès et configuration

Login admin par défaut

J’ouvre http://192.168.10.150:81 depuis mon poste.

TEXT
Email    : admin@example.com
Password : changeme
Cliquez pour développer et voir plus

NPM force le changement de mot de passe au premier login : il refuse de continuer tant que changeme n’est pas remplacé. Je définis une passphrase 16+ caractères, stockée dans le gestionnaire de mots de passe interne.

Création d’un certificat auto-signé

Plutôt que de générer un certificat par proxy host, je prépare un seul certificat couvrant les sous-domaines internes. NPM accepte des SAN multiples sur un certificat auto-signé.

TEXT
SSL Certificates → Add SSL Certificate → Custom (auto-signed)
  Domain Names :
    web.lab.local
    gestion-parc.lab.local
    monitoring.lab.local
  
  → NPM génère la clé privée et signe le certificat avec sa CA interne.
Cliquez pour développer et voir plus

NPM produit le certificat dans les volumes (./letsencrypt/...) et propose le téléchargement du certificat racine au format .crt. Je le copie sur les postes admin du lab pour qu’ils fassent confiance aux certificats émis.

Création des proxy hosts

Service Web 1 (web-interne)

TEXT
Hosts → Proxy Hosts → Add Proxy Host

Details :
  Domain Names           : web.lab.local
  Scheme                 : http
  Forward Hostname / IP  : 192.168.10.151
  Forward Port           : 8000
  Cache Assets           : non
  Block Common Exploits  : oui
  Websockets Support     : oui

SSL :
  SSL Certificate        : (le certificat auto-signé créé plus haut)
  Force SSL              : oui (HTTP → HTTPS)
  HTTP/2 Support         : oui
  HSTS Enabled           : oui
  HSTS Subdomains        : non (on évite, pour ne pas figer toutes les autres entrées du domaine)

Advanced :
  (laisser vide)
Cliquez pour développer et voir plus

Explications ligne par ligne :

  • Forward Hostname/IP: 192.168.10.151 : adresse interne du backend. Plus propre encore : brancher backend et NPM sur un réseau Docker partagé quand le backend est aussi en Docker.
  • Block Common Exploits: oui : ajoute des règles Nginx qui bloquent des patterns d’exploitation classiques (/etc/passwd, <script> dans l’URL, etc.). Coût négligeable, gain réel.
  • Websockets Support: oui : nécessaire pour les services qui utilisent les WebSockets (notifications push, sync temps réel). Si on ne sait pas, on coche par sécurité.
  • Force SSL : redirige automatiquement le HTTP vers HTTPS. Évite qu’un client se connecte en clair par erreur.
  • HSTS Enabled : annonce au navigateur qu’il doit toujours utiliser HTTPS pour ce domaine. Sans HSTS Subdomains activé, l’instruction ne s’applique qu’au sous-domaine, ce qui est plus prudent au début.
  • HTTP/2 Support : multiplexage des requêtes, gain visible sur les pages avec beaucoup d’assets.

Services 2 et 3

Mêmes paramètres, en pointant vers les bons backends :

DomainForward
gestion-parc.lab.local192.168.10.152:80
monitoring.lab.local192.168.10.153:8080

Vérifications

Depuis le LAN

BASH
# Test HTTPS depuis un poste admin avec la racine NPM importée
curl -I https://web.lab.local
# HTTP/2 200
# server: nginx
# strict-transport-security: max-age=63072000; includeSubDomains; preload

# Sans la racine importée (poste invité), le certificat n'est pas validé :
curl -I https://web.lab.local
# curl: (60) SSL certificate problem: self-signed certificate
Cliquez pour développer et voir plus

Le premier curl (avec la racine NPM importée) retourne HTTP/2 200. Le second (poste sans la racine) signale une self-signed certificate : c’est attendu, et c’est ce qui justifie la diffusion de la racine via GPO ou configuration management à tous les postes admin.

Inspection du certificat

BASH
echo | openssl s_client -connect web.lab.local:443 -servername web.lab.local 2>/dev/null | \
    openssl x509 -noout -subject -issuer -dates

# subject=CN = web.lab.local
# issuer=CN = Nginx Proxy Manager (auto-signed CA)
# notBefore=Feb 13 12:34:56 2026 GMT
# notAfter=Feb 13 12:34:56 2027 GMT
Cliquez pour développer et voir plus

Le certificat est valide 1 an. Le subject correspond bien au domaine du proxy host.

Sécurité de l’interface admin

L’interface NPM (port 81) ne doit être accessible que depuis le segment d’admin. J’ajoute une règle de pare-feu côté passerelle :

TEXT
Source       : segment d'admin uniquement
Destination  : 192.168.10.150:81
Action       : Accept

Source       : Any (autres segments)
Destination  : 192.168.10.150:81
Action       : Reject
Cliquez pour développer et voir plus

Et je vérifie depuis un poste hors segment d’admin :

BASH
curl -m 5 http://192.168.10.150:81
# Connection timed out
Cliquez pour développer et voir plus

L’admin est inaccessible depuis les segments non autorisés, comme attendu.

Problèmes rencontrés et solutions

SymptômeCauseCorrection
docker compose up -d échoue avec port 80 already in useApache ou un autre Nginx tourne sur l’hôteArrêter le service en conflit (systemctl stop apache2 puis disable), ou changer le mapping de ports NPM
Le navigateur affiche encore l’avertissement après import de la racineMagasin de certificats Firefox indépendant du systèmeImporter la racine NPM dans Préférences Firefox → Vie privée → Certificats en plus du magasin OS
Le backend renvoie 502 Bad GatewayNPM atteint le Forward IP:Port mais le service est down ou écoute sur une autre IPVérifier curl http://<IP>:<port> côté NPM, vérifier que le service écoute bien sur l’interface attendue
Les WebSockets se ferment dès l’ouvertureWebsockets Support non coché sur le proxy hostCocher l’option, sauvegarder, le proxy host est mis à jour à chaud
HSTS bloque l’accès en HTTP même après désactivationHSTS reste valide côté navigateur jusqu’à expirationVider le HSTS du navigateur (chrome://net-internals/#hsts) ou attendre max-age
502 Bad Gateway qui apparaît sur un seul service après un redémarrageConteneur backend démarré après NPM, IP changéeBrancher backend + NPM sur un réseau Docker partagé pour résoudre par nom (backend:port)

Compétences du bloc 1 mobilisées

Compétence officielleMobilisation concrète
Mettre à disposition aux utilisateurs un service informatiqueCentralisation des accès Web internes derrière un seul reverse proxy en HTTPS.
Installer et configurer des éléments d’infrastructureStack docker compose avec NPM et MariaDB, persistance par volumes, configuration via interface Web.
Mettre en place et vérifier les niveaux d’habilitation associés à un serviceRestriction du port 81 d’admin au segment d’admin via le pare-feu, mot de passe admin renouvelé au premier login.
Réaliser les tests d’intégration et d’acceptation d’un serviceTests croisés (HTTPS depuis un poste avec la racine importée, depuis un poste sans, inspection du certificat, accès au port admin depuis un autre segment).

Bilan

Trois services Web internes du lab sont publiés en HTTPS sur des sous-domaines *.lab.local, derrière un seul reverse proxy NPM, avec un certificat auto-signé partagé et la racine NPM importée dans le magasin de confiance des postes admin. La maintenance des proxy hosts est centralisée dans une seule interface, plus besoin de manipuler des fichiers de config Nginx un à un.

Trois enseignements :

  • l’auto-signé en interne est tout à fait acceptable tant qu’on maîtrise la diffusion de la racine sur les postes — c’est la même mécanique qu’une PKI d’entreprise, en plus simple ;
  • le HSTS est puissant mais doit être activé en connaissance de cause : si on se trompe de domaine ou qu’on doit revenir en arrière, on est bloqué pendant la durée du max-age. À tester d’abord avec un max-age court, puis monter à 2 ans une fois confiant ;
  • la mécanique de Block Common Exploits + WebSockets cochés est un bon défaut : ça écarte la majorité des cas où un service web ne fonctionne pas correctement derrière un reverse proxy.

Pour la suite, je veux brancher l’authentification SSO (Authelia/Authentik) devant NPM pour ajouter une couche MFA sur tous les services publiés, et monitorer les certificats émis (date d’expiration, alerte 30 jours avant) pour ne pas se faire surprendre par un renouvellement oublié.

Sources

Commencer la recherche

Saisissez des mots-clés pour rechercher des articles

↑↓
ESC
⌘K Raccourci