2026-10-27 17:11:35 +01:00

PoC BunkerWeb sur Proxmox

Un environnement de test complet pour BunkerWeb, le reverse proxy WAF open source basé sur nginx, déployé en docker-compose sur des VM Proxmox, de bout en bout par Ansible.

L'objectif : voir concrètement ce que BunkerWeb bloque, laisse passer et journalise, sur des applis qui s'y prêtent, puis rejouer les mêmes attaques sans WAF pour comparer.

Architecture du PoC

Ce que contient le PoC

VM Rôle Conteneurs
bw-waf Point d'entrée unique, ports 80/443 (TCP + UDP pour HTTP/3) bunkerweb, bw-scheduler, bw-ui, bw-db (MariaDB 11), bw-redis
bw-apps Sites de test, joignables uniquement depuis le WAF (iptables DOCKER-USER) poc-www (nginx), poc-shop (OWASP Juice Shop), poc-api (go-httpbin)
bw-client Poste "attaquant" avec les outils et le script de tests curl, sqlmap, nmap, ab, bwpoc-tests

Versions épinglées dans inventory/group_vars/all.yml : BunkerWeb 1.6.17, Juice Shop v20.3.0, go-httpbin 2.26.0, nginx 1.30, Debian 13 (trixie) genericcloud.

Une variante tout-en-un (une seule VM) est fournie : inventory/single.yml. Le mode est détecté automatiquement : si WAF et applis sont le même hôte, BunkerWeb joint les applis par le réseau Docker bw-services et aucun port applicatif n'est publié.

Les sites de test

Chaque site met en avant un groupe de fonctionnalités :

Site Appli Ce qu'on y teste
www.<domaine> vitrine nginx statique (page d'accueil + formulaire de contact) antibot JavaScript, en-têtes de sécurité, redirection HTTPS, XSS via formulaire dans le navigateur
shop.<domaine> OWASP Juice Shop, appli volontairement vulnérable ModSecurity + OWASP CRS 4 (SQLi, XSS, traversée), websockets, User-Agents malveillants
api.<domaine> go-httpbin, API qui renvoie ce qu'elle reçoit limitation de débit, méthodes autorisées, taille max du corps, bad behavior et bannissement, règle ModSecurity maison, compression, X-Forwarded-For
bwui.<domaine> interface web de BunkerWeb rapports d'attaques, bannissements, réglages, journaux

Le domaine par défaut est bwpoc.internal (TLD réservé par l'ICANN pour l'usage privé). Les certificats sont auto-signés par BunkerWeb, site par site.

Prérequis

  • Un nœud Proxmox VE 8 ou 9 (ici lancelot, 172.16.16.7) avec accès Internet (image Debian, images Docker, listes noires communautaires).
  • Un accès SSH root au nœud, utilisé une seule fois pour créer le modèle de VM et le pool bwpoc. Tout le reste passe par l'API avec un compte dédié (voir plus bas).
  • Un réseau de VM avec DHCP : ici le VLAN 200 "LAB", qui fournit IP, passerelle et DNS. Le poste Ansible doit pouvoir joindre ce VLAN en SSH.
  • Sur le poste Ansible : ansible-core 2.17 ou plus (exigé par community.proxmox), proxmoxer et requests en Python, une paire de clés SSH.
git clone git@gitea.vlab.bzh:vLab-BZH/bunkerwebpoc.git && cd bunkerwebpoc
make deps   # collections community.docker et community.proxmox, puis proxmoxer

Configuration

Tout ce qui dépend de votre infra est marqué A ADAPTER :

  1. inventory/hosts.yml : nœud Proxmox, VMID des 3 VM, CPU, RAM, disque. Pas d'IP : elles viennent du DHCP.
  2. inventory/group_vars/all.yml : domaine, nœud, stockage, pont et VLAN, compte API, clé SSH publique.
  3. inventory/group_vars/bunkerweb.yml : les réglages BunkerWeb, globaux et par site. C'est le fichier à modifier pour jouer avec les fonctionnalités.

Les mots de passe (MariaDB, Redis, admin de l'UI) sont générés au premier lancement dans credentials/ côté poste Ansible (ignoré par git). Pour les mettre sous ansible-vault, remplacez les lookup('password') de bunkerweb.yml par vos variables chiffrées.

Compte Proxmox dédié

Le clonage et le pilotage des VM passent par l'API avec l'utilisateur bwpoc@pve et un jeton, limité à un pool bwpoc. Par défaut, le PoC clone un modèle cloud-init Debian 13 existant (pve_template_vmid, 9001 sur lancelot : VLAN 200, agent QEMU), donc aucun SSH root n'est nécessaire. Avec pve_template_create: true, le modèle 9100 est fabriqué en SSH root (import d'une image disque par chemin, réservé à root@pam), une fois.

Privilèges nécessaires, regroupés dans un rôle BWPoC :

Privilège Pour quoi faire
VM.Allocate, VM.Clone créer les clones dans le pool, les supprimer
VM.Config.CPU, VM.Config.Memory, VM.Config.Disk, VM.Config.Network, VM.Config.Cloudinit, VM.Config.Options, VM.Config.HWType, VM.Config.CDROM régler CPU, RAM, disque, cloud-init, démarrage auto, tags
VM.PowerMgmt démarrer et arrêter les VM
VM.Audit, Pool.Audit lire l'état des VM et du pool
VM.GuestAgent.Audit (PVE 9) ou VM.Monitor (PVE 8) lire l'IP DHCP par l'agent QEMU

Et les ACL, au plus juste :

Chemin Rôle Pourquoi
/pool/bwpoc BWPoC les VM du PoC (et le modèle 9100 s'il est fabriqué par le PoC)
/storage/zfs PVEDatastoreUser allouer les disques des clones
/sdn/zones/localnetwork/vmbr0/200 PVESDNUser brancher les VM sur le VLAN 200 (et seulement lui)
/nodes/lancelot PVEAuditor lecture du nœud par le module Ansible

En root sur lancelot (sur PVE 8, remplacer VM.GuestAgent.Audit par VM.Monitor) :

pveum pool add bwpoc --comment "PoC BunkerWeb"
pveum role add BWPoC --privs "VM.Allocate,VM.Clone,VM.Audit,VM.PowerMgmt,VM.Config.CPU,VM.Config.Memory,VM.Config.Disk,VM.Config.Network,VM.Config.Cloudinit,VM.Config.Options,VM.Config.HWType,VM.Config.CDROM,VM.GuestAgent.Audit,Pool.Audit"
pveum user add bwpoc@pve --comment "PoC BunkerWeb (Ansible)"
pveum user token add bwpoc@pve ansible --privsep 0   # affiche le secret une seule fois
pveum acl modify /pool/bwpoc --users bwpoc@pve --roles BWPoC
pveum acl modify /storage/zfs --users bwpoc@pve --roles PVEDatastoreUser
pveum acl modify /sdn/zones/localnetwork/vmbr0/200 --users bwpoc@pve --roles PVESDNUser
pveum acl modify /nodes/lancelot --users bwpoc@pve --roles PVEAuditor

Si le réseau "LAB" est un VNet SDN plutôt qu'un tag sur vmbr0, l'ACL réseau devient /sdn/zones/<zone>/LAB et, dans all.yml, pve_bridge: LAB avec pve_vlan_tag: "".

Les secrets restent hors du dépôt, dans l'environnement. Au choix, le jeton :

export PVE_TOKEN_SECRET='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

ou l'identifiant et le mot de passe du compte (le jeton est prioritaire s'il est défini) :

export PROXMOX_CLAUDE='bwpoc@pve'
export PROXMOX_PASSWORD='...'

Déploiement

make deploy        # = ansible-playbook playbooks/site.yml
Étape Playbook Ce qu'elle fait
1 00-proxmox.yml si pve_template_create: true, en root (une fois) : fabrique le modèle 9100 depuis l'image Debian 13 genericcloud. Puis par l'API : crée le pool bwpoc, clone le modèle (9001 par défaut), cloud-init en DHCP + clé SSH, agrandit les disques, démarre, régénère le machine-id par l'agent QEMU (sinon tous les clones obtiennent la même IP DHCP), lit l'IP obtenue par l'agent QEMU et l'écrit dans inventory/host_vars/<vm>/dhcp.yml
2 10-base.yml paquets de base, agent QEMU, résolution locale des noms du PoC vers le WAF, Docker CE depuis le dépôt officiel
3 20-apps.yml stack des applis, filtrage DOCKER-USER (mode split)
4 30-bunkerweb.yml stack BunkerWeb, attente qu'api réponde au travers du WAF, écriture de credentials/ACCES.txt
5 40-client.yml outils d'attaque et script de tests sur bw-client

Vous avez déjà vos VM (Debian 12 ou 13, accès SSH avec sudo) ? Renseignez-les dans l'inventaire et sautez l'étape 1 : make base apps waf client.

Pour relire les fichiers compose générés sans rien déployer :

ansible-playbook playbooks/render.yml   # écrit build/split/... ou build/single/...

Un exemple de rendu (secrets remplacés par CHANGEME) est versionné dans examples/split/ (bunkerweb.compose.yml, apps.compose.yml, bwpoc-firewall.sh).

Adresses IP et DHCP

Les VM démarrent sur le VLAN 200 "LAB" en DHCP. Le modèle embarque l'agent QEMU : le playbook lit l'IP de chaque VM par l'API Proxmox, l'enregistre dans inventory/host_vars/<vm>/dhcp.yml (ignoré par git) et recharge l'inventaire. Les playbooks suivants, et vos relances (make waf...), utilisent ces fichiers.

Comme l'IP du WAF sert au filtrage DOCKER-USER et aux /etc/hosts, réservez les baux DHCP des trois VM (adresses MAC visibles dans Proxmox). Si une IP change quand même, relancez make vms base apps waf client : les fichiers dhcp.yml et les règles sont régénérés.

Accéder aux sites depuis votre poste

Les VM du PoC résolvent déjà les noms (bloc dans /etc/hosts). Sur votre poste, ajoutez la ligne suivante à /etc/hosts, ou déclarez les enregistrements dans votre DNS de labo :

<IP de bw-waf>  www.bwpoc.internal shop.bwpoc.internal api.bwpoc.internal bwui.bwpoc.internal

Puis ouvrez https://bwui.bwpoc.internal/ (certificat auto-signé à accepter). Identifiants : make creds.

Comment BunkerWeb est configuré ici

Intégration Docker officielle (pas l'autoconf) : toute la configuration est portée par les variables d'environnement du bw-scheduler, qui génère la configuration nginx et la pousse à l'instance bunkerweb par son API (réseau bw-universe 10.20.30.0/24, seul autorisé par API_WHITELIST_IP). Ce choix permet de protéger des backends situés sur une autre VM, ce que les labels de l'autoconf ne savent pas faire.

MULTISITE=yes : chaque réglage peut être préfixé par le nom du site (api.bwpoc.internal_LIMIT_REQ_RATE=5r/s). Dans Ansible, il suffit de remplir le dictionnaire bw_sites :

bw_sites:
  api:
    upstream: api                # clé de apps_ports, ou URL complète
    settings:
      ALLOWED_METHODS: "GET|POST|HEAD"
      MAX_CLIENT_SIZE: "1m"
      LIMIT_REQ_RATE: "5r/s"
    custom_confs:                # devient api.<domaine>_CUSTOM_CONF_MODSEC_poc-rule
      MODSEC_poc-rule: >-
        SecRule ARGS:poc "@streq attack" "id:10001,phase:2,deny,status:403,log,msg:'PoC'"

Les réglages posés par variables d'environnement apparaissent dans l'UI en lecture seule (méthode scheduler). Les modifications faites dans l'UI sont stockées en base et s'ajoutent par-dessus.

Redis est activé (USE_REDIS=yes) : bans, sessions et compteurs sont partagés, ce qui prépare l'ajout d'une seconde instance bunkerweb derrière un VIP (keepalived) sans changer le reste.

Tester les fonctionnalités

Depuis bw-client (ou n'importe quel poste avec curl et jq, le script utilise --resolve et n'a pas besoin de DNS) :

make tests                              # depuis le poste Ansible
ssh debian@<IP de bw-client> bwpoc-tests   # ou directement sur le client

Le script enchaîne une vingtaine de contrôles et affiche OK, KO ou INFO :

Fonctionnalité Réglage Requête Attendu
HTTPS forcé AUTO_REDIRECT_HTTP_TO_HTTPS http://api/get 301 vers https
Serveur par défaut fermé DISABLE_DEFAULT_SERVER Host inconnu connexion fermée (444)
En-têtes de sécurité STRICT_TRANSPORT_SECURITY, CONTENT_SECURITY_POLICY... api/get en-têtes présents, Server masqué
Proxy transparent USE_REVERSE_PROXY api/headers le backend voit X-Forwarded-For
Compression USE_GZIP, USE_BROTLI api/html Content-Encoding: br ou gzip
Antibot USE_ANTIBOT=javascript www/ en curl redirection vers /challenge
WAF SQLi ModSecurity + CRS 4 shop/rest/products/search?q=')) OR 1=1-- 403
WAF XSS idem shop/?q=<script>alert(1)</script> 403
WAF traversée idem shop/?file=../../../../etc/passwd 403
WAF injection de commande idem api/get?cmd=;cat /etc/passwd 403
Règle maison CUSTOM_CONF_MODSEC api/get?poc=attack 403
User-Agent maison BLACKLIST_USER_AGENT -A PoC-EvilBot 403
User-Agent communautaire BLACKLIST_COMMUNITY_LISTS -A sqlmap/1.8 403 (info : dépend du téléchargement de la liste)
Méthodes ALLOWED_METHODS PUT api/put 405
Taille du corps MAX_CLIENT_SIZE=1m POST de 2 Mo 413
Limitation de débit LIMIT_REQ_RATE=5r/s 40 requêtes en parallèle une partie en 429
Bad behavior BAD_BEHAVIOR_THRESHOLD=10 en 1 min 12 × /status/404 IP bannie (403) sur api seulement

Les derniers tests bannissent volontairement votre IP du site api pendant 5 minutes (bw_ban_time). Pour lever le ban : menu Bans de l'UI, ou sur bw-waf :

docker exec bw-scheduler bwcli unban <IP du client>

Avec et sans WAF

Pour mesurer l'apport du WAF, autorisez le client à joindre les applis en direct, puis rejouez les attaques :

# inventory/group_vars/all.yml
apps_direct_access_sources: ["<IP de bw-client>"]
make apps
ssh debian@<IP de bw-client> bwpoc-tests --direct

La SQLi de Juice Shop renvoie alors la liste complète des produits (200) au lieu d'un 403, et le PUT passe.

Aller plus loin à la main

  • sqlmap contre Juice Shop, d'abord avec son User-Agent par défaut (bloqué par la liste communautaire), puis avec --random-agent pour voir le CRS prendre le relais : sqlmap -u "https://shop.bwpoc.internal/rest/products/search?q=test" --random-agent --batch
  • Antibot dans le navigateur : passez www en captcha ou cookie dans bunkerweb.yml, relancez make waf.
  • Faux positifs : naviguez dans Juice Shop (connexion, panier, avis). Ce que le CRS bloque à tort apparaît dans l'UI (Reports) avec l'ID de règle ; on l'exclut par un custom_confs de type MODSEC_CRS (exemple : SecRuleRemoveById 942100).
  • Mode détection seule : MODSECURITY_SEC_RULE_ENGINE: "DetectionOnly" sur un site, pour observer avant de bloquer, comme on le ferait en production.
  • Journaux : docker logs -f bunkerweb sur bw-waf, et l'audit ModSecurity dans /var/log/bunkerweb/modsec_audit.log du conteneur.

Exposer le PoC sur un domaine public (Let's Encrypt)

Par défaut tout est en bwpoc.internal avec des certificats auto-signés. Pour un vrai domaine (exemple : lab.naoned.net) :

  1. DNS : www, shop, api et bwui du domaine pointent sur l'IP publique NATée vers le WAF.
  2. Pare-feu : 80 et 443 (TCP, plus 443/UDP pour HTTP/3) vers l'IP du WAF ; réservation DHCP sur la MAC du WAF pour que l'IP ne bouge pas.
  3. Variables (inventaire ou -e) :
poc_domain: lab.naoned.net
poc_fqdn_suffix: ".lab.naoned.net"      # ou "-bwpoc.vlab.bzh" pour www-bwpoc.vlab.bzh (un seul niveau sous un wildcard)
poc_tls: letsencrypt
poc_letsencrypt_email: vous@example.org
poc_allowed_sources: ["203.0.113.10"]   # IP autorisées sur les sites de poc_restricted_sites (bwui par défaut)
  1. make waf client tests.

La restriction par IP est une règle ModSecurity par site (id 10002) qui laisse passer le challenge ACME. Le client de test est toujours autorisé. Ajoutez shop à poc_restricted_sites pour ne pas exposer Juice Shop à tout Internet.

Derrière un reverse proxy existant (Traefik)

Si 80/443 sont déjà pris par un Traefik, il termine le TLS public (ses propres certificats Let's Encrypt) et relaie vers le WAF en HTTPS (certificat auto-signé du PoC) : garder poc_tls: selfsigned et déclarer son IP dans poc_trusted_proxies pour que BunkerWeb voie l'IP réelle des clients (sinon un bannissement viserait Traefik, donc tout le monde). Exemple de configuration dynamique Traefik (fichier) :

http:
  routers:
    bwpoc:
      rule: "Host(`www-bwpoc.vlab.bzh`) || Host(`shop-bwpoc.vlab.bzh`) || Host(`api-bwpoc.vlab.bzh`) || Host(`bwui-bwpoc.vlab.bzh`)"
      entryPoints: [websecure]
      service: bwpoc
      tls: { certResolver: letsencrypt }
  services:
    bwpoc:
      loadBalancer:
        passHostHeader: true
        serversTransport: bwpoc-insecure
        servers: [{ url: "https://192.168.200.155:443" }]
  serversTransports:
    bwpoc-insecure:
      insecureSkipVerify: true

Exploitation

make waf           # réapplique la conf BunkerWeb après modification de bunkerweb.yml
make destroy       # supprime les VM par l'API (confirmation demandée), modèle conservé
ansible-playbook playbooks/99-destroy.yml -e destroy_template=true   # supprime aussi le modèle, s'il a été fabriqué par le PoC

Sur bw-waf, la stack vit dans /opt/bwpoc/bunkerweb (docker compose ps, logs, pull...). Sur bw-apps, dans /opt/bwpoc/apps.

Limites assumées du PoC

  • Certificats auto-signés : pour du Let's Encrypt en labo fermé, passer par le challenge DNS (AUTO_LETS_ENCRYPT, LETS_ENCRYPT_CHALLENGE=dns) avec votre fournisseur DNS.
  • Une seule instance BunkerWeb : pas de haute disponibilité, Redis est là pour la suite.
  • Les fonctions PRO de BunkerWeb (anti DDoS, monitoring Prometheus, cache, rapports) ne sont pas couvertes.
  • Le géoblocage (BLACKLIST_COUNTRY) et les DNSBL ne se testent pas depuis des IP privées.

Arborescence

inventory/            hosts.yml (3 VM), single.yml (tout-en-un), group_vars/
playbooks/            00 à 50 + site.yml, render.yml, 99-destroy.yml
roles/
  proxmox_template/   modèle Debian 13 cloud-init + agent QEMU, pool (root, une fois)
  proxmox_vm/         clonage par l'API (bwpoc@pve), DHCP, lecture des IP
  common/             paquets, agent QEMU, /etc/hosts
  docker/             Docker CE + compose (dépôt officiel)
  apps/               compose des applis, vitrine, filtrage DOCKER-USER
  bunkerweb/          compose BunkerWeb (template Jinja)
  client/             outils et script de tests
tests/bwpoc-tests.sh  batterie de tests fonctionnels
examples/split/       compose générés, pour lecture
docs/                 schéma d'architecture (.excalidraw et .png)

Références

S
Description
PoC BunkerWeb (WAF) : docker-compose + déploiement Ansible sur Proxmox
Readme
3.2 MiB
0 Stars 2 Watchers 0 Forks
Languages
Jinja 50.1%
Shell 45.2%
Makefile 4.7%