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.
Important
Un certificat auto-signé est valide cryptographiquement (la clé privée correspond à la clé publique signée), il n’est juste pas reconnu par défaut. Une fois la racine ajoutée à la liste des CA approuvées des postes du lab, les navigateurs ne lèvent plus d’avertissement. C’est la solution acceptable pour un usage strictement interne.
Topologie
| Équipement | Rôle | Adresse |
|---|---|---|
VM DOCKER-MICROSERVICES (Debian 12) | Hôte Docker pour NPM | 192.168.10.150/24 |
Backend Web 1 (web-interne) | Service Web interne sur HTTP/8000 | 192.168.10.151:8000 |
Backend Web 2 (gestion-parc-interne) | Service Web interne sur HTTP/80 | 192.168.10.152:80 |
Backend Web 3 (monitoring-interne) | Service Web interne sur HTTP/8080 | 192.168.10.153:8080 |
DNS interne (dc.lab.local) | Résolution *.lab.local | 192.168.10.250 |
Prérequis
- VM
DOCKER-MICROSERVICESavec 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.150gestion-parc.lab.local → 192.168.10.150monitoring.lab.local → 192.168.10.150
- Compte sudo sur la VM pour piloter Docker.
Préparation de la stack
sudo mkdir -p /opt/docker-compose/npm
cd /opt/docker-compose/npm
mkdir -p data letsencrypt mariadbFichier .env
cat > .env <<'EOF'
DB_ROOT_PASSWORD=<passphrase générée>
DB_NPM_PASSWORD=<passphrase générée>
EOF
chmod 600 .envPrudence
Le .env contient les mots de passe MariaDB. chmod 600 est obligatoire ; idéalement, ces secrets sont stockés dans le gestionnaire de mots de passe interne et copiés au déploiement.
docker-compose.yml
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: bridgeExplications 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éseaunpm. 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 ./dataet./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
docker compose pull
docker compose up -d
docker compose logs -fÀ l’issue du premier démarrage, je vérifie :
docker compose ps
# NAME STATUS
# npm Up healthy
# npm-db Up healthy
# Test HTTP local
curl -I http://localhost:81
# HTTP/1.1 200 OKPremier accès et configuration
Login admin par défaut
J’ouvre http://192.168.10.150:81 depuis mon poste.
Email : admin@example.com
Password : changemeNPM 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é.
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.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.
Astuce
Sur Windows, le certificat racine s’importe via certmgr.msc dans Autorités de certification racines de confiance. Sur Linux Debian/Ubuntu, on copie le .crt dans /usr/local/share/ca-certificates/ et on lance update-ca-certificates. Sur les navigateurs Firefox, c’est un magasin de certificats indépendant à alimenter dans les Préférences → Vie privée et sécurité.
Création des proxy hosts
Service Web 1 (web-interne)
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)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. SansHSTS Subdomainsactivé, 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 :
| Domain | Forward |
|---|---|
gestion-parc.lab.local | 192.168.10.152:80 |
monitoring.lab.local | 192.168.10.153:8080 |
Vérifications
Depuis le LAN
# 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 certificateLe 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
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 GMTLe 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 :
Source : segment d'admin uniquement
Destination : 192.168.10.150:81
Action : Accept
Source : Any (autres segments)
Destination : 192.168.10.150:81
Action : RejectEt je vérifie depuis un poste hors segment d’admin :
curl -m 5 http://192.168.10.150:81
# Connection timed outL’admin est inaccessible depuis les segments non autorisés, comme attendu.
Problèmes rencontrés et solutions
| Symptôme | Cause | Correction |
|---|---|---|
docker compose up -d échoue avec port 80 already in use | Apache ou un autre Nginx tourne sur l’hôte | Arrê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 racine | Magasin de certificats Firefox indépendant du système | Importer la racine NPM dans Préférences Firefox → Vie privée → Certificats en plus du magasin OS |
Le backend renvoie 502 Bad Gateway | NPM atteint le Forward IP:Port mais le service est down ou écoute sur une autre IP | Vé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’ouverture | Websockets Support non coché sur le proxy host | Cocher l’option, sauvegarder, le proxy host est mis à jour à chaud |
| HSTS bloque l’accès en HTTP même après désactivation | HSTS reste valide côté navigateur jusqu’à expiration | Vider 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émarrage | Conteneur backend démarré après NPM, IP changée | Brancher backend + NPM sur un réseau Docker partagé pour résoudre par nom (backend:port) |
Compétences du bloc 1 mobilisées
| Compétence officielle | Mobilisation concrète |
|---|---|
| Mettre à disposition aux utilisateurs un service informatique | Centralisation des accès Web internes derrière un seul reverse proxy en HTTPS. |
| Installer et configurer des éléments d’infrastructure | Stack 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 service | Restriction 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 service | Tests 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 unmax-agecourt, 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
- Nginx Proxy Manager — Documentation , Jamie Curnow.
- Nginx Proxy Manager — Setup with Docker , Jamie Curnow.
- Nginx — HTTP Strict Transport Security , Nginx Inc.
- Mozilla — HTTP Strict Transport Security (HSTS) , MDN Web Docs.
- ANSSI — Recommandations de sécurité relatives à TLS , ANSSI.
- ANSSI — Recommandations relatives à l’administration sécurisée des systèmes d’information , ANSSI.