Files
bunkerwebpoc/README.md
2026-10-27 15:59:35 +01:00

316 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PoC BunkerWeb sur Proxmox
Un environnement de test complet pour [BunkerWeb](https://www.bunkerweb.io/), 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](docs/architecture.png)
## 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`](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`](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](https://owasp.org/www-project-juice-shop/), appli volontairement vulnérable | ModSecurity + OWASP CRS 4 (SQLi, XSS, traversée), websockets, User-Agents malveillants |
| `api.<domaine>` | [go-httpbin](https://github.com/mccutchen/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.
```bash
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`](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`](inventory/group_vars/all.yml) : domaine, nœud, stockage, pont et VLAN, compte API, clé SSH publique.
3. [`inventory/group_vars/bunkerweb.yml`](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`) :
```bash
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 :
```bash
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) :
```bash
export PROXMOX_CLAUDE='bwpoc@pve'
export PROXMOX_PASSWORD='...'
```
## Déploiement
```bash
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 :
```bash
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/`](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` :
```yaml
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) :
```bash
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` :
```bash
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 :
```yaml
# inventory/group_vars/all.yml
apps_direct_access_sources: ["<IP de bw-client>"]
```
```bash
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`) :
```yaml
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)
```
4. `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) :
```yaml
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
```bash
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/>