- modèle par défaut 9100 debian13-bwpoc (dérivé de 9001 + qemu-guest-agent), fabrication en SSH root seulement si pve_template_create: true - régénération du machine-id par l'agent QEMU (sinon même IP DHCP pour tous les clones) - pool bwpoc créé par l'API, 99-destroy par l'API - correctif ansible-core 2.21 (copie de hostvars), clé SSH id_ed25519 ou id_rsa Déploiement validé sur lancelot : 50-tests 26 OK, 0 KO. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
16 KiB
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.
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-core2.17 ou plus (exigé parcommunity.proxmox),proxmoxeretrequestsen 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 :
inventory/hosts.yml: nœud Proxmox, VMID des 3 VM, CPU, RAM, disque. Pas d'IP : elles viennent du DHCP.inventory/group_vars/all.yml: domaine, nœud, stockage, pont et VLAN, compte API, clé SSH publique.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-agentpour 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
wwwencaptchaoucookiedansbunkerweb.yml, relancezmake 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_confsde typeMODSEC_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 bunkerwebsurbw-waf, et l'audit ModSecurity dans/var/log/bunkerweb/modsec_audit.logdu conteneur.
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
- Documentation BunkerWeb : https://docs.bunkerweb.io/latest/
- Fichiers d'intégration officiels (docker, MariaDB, UI) : https://github.com/bunkerity/bunkerweb/tree/v1.6.17/misc/integrations
- Liste des réglages, extraite des
plugin.json: https://github.com/bunkerity/bunkerweb/tree/v1.6.17/src/common/core - OWASP Core Rule Set : https://coreruleset.org/
