commit 099584c95c3ce26ac8e5b8163508c6f5c74069e0 Author: Claude Date: Tue Oct 27 11:11:07 2026 +0100 PoC BunkerWeb 1.6.17 : docker-compose sur Proxmox, déployé par Ansible Stack BunkerWeb (scheduler, UI, MariaDB, Redis), sites de test (nginx, Juice Shop, go-httpbin), filtrage DOCKER-USER, script de tests, README et schéma d'architecture. Co-Authored-By: Claude Opus 5.5 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c371f4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +credentials/ +collections/ +build/ +*.retry +.vault_pass diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c76a01a --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# Raccourcis du PoC BunkerWeb +INV ?= inventory/hosts.yml + +.PHONY: deps deploy vms base apps waf client tests destroy creds + +deps: ## Collections Ansible + ansible-galaxy collection install -r requirements.yml -p ./collections +deploy: ## Déploiement complet + ansible-playbook -i $(INV) playbooks/site.yml +vms: + ansible-playbook -i $(INV) playbooks/00-proxmox.yml +base: + ansible-playbook -i $(INV) playbooks/10-base.yml +apps: + ansible-playbook -i $(INV) playbooks/20-apps.yml +waf: + ansible-playbook -i $(INV) playbooks/30-bunkerweb.yml +client: + ansible-playbook -i $(INV) playbooks/40-client.yml +tests: + ansible-playbook -i $(INV) playbooks/50-tests.yml +destroy: + ansible-playbook -i $(INV) playbooks/99-destroy.yml +creds: ## Affiche les accès à l'UI + @cat credentials/ACCES.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8d01bb --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# 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.` | 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.` | [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.` | [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.` | 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 joignable en SSH **root** depuis le poste Ansible, avec accès Internet (image Debian, images Docker, listes noires communautaires). +- 3 IP libres sur le réseau des VM (1 en mode tout-en-un). Le playbook vérifie par ping qu'elles sont libres avant de créer les VM. +- Sur le poste Ansible : `ansible-core` 2.16 ou plus, une paire de clés SSH. + +```bash +git clone git@gitea.vlab.bzh:vLab-BZH/bunkerwebpoc.git && cd bunkerwebpoc +make deps # installe community.docker et ansible.posix dans ./collections +``` + +## Configuration + +Tout ce qui dépend de votre infra est marqué `A ADAPTER` : + +1. [`inventory/hosts.yml`](inventory/hosts.yml) : IP du nœud Proxmox, IP et VMID des 3 VM, CPU, RAM, disque. +2. [`inventory/group_vars/all.yml`](inventory/group_vars/all.yml) : domaine, passerelle, DNS, stockage Proxmox, pont, VLAN, 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. + +## Déploiement + +```bash +make deploy # = ansible-playbook playbooks/site.yml +``` + +| Étape | Playbook | Ce qu'elle fait | +|---|---|---| +| 1 | `00-proxmox.yml` | télécharge l'image Debian 13 genericcloud, crée le modèle cloud-init (VMID 9100), clone les VM, injecte IP et clé SSH, agrandit les disques, démarre | +| 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`). + +### 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 : + +``` +192.168.200.61 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._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@192.168.200.63 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=` | 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 192.168.200.63 +``` + +### 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: [192.168.200.63] +``` + +```bash +make apps +ssh debian@192.168.200.63 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. + +## Exploitation + +```bash +make waf # réapplique la conf BunkerWeb après modification de bunkerweb.yml +make destroy # supprime les VM (confirmation demandée), modèle conservé +ansible-playbook playbooks/99-destroy.yml -e destroy_template=true # supprime aussi le modèle +``` + +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 via qm + proxmox_vm/ clonage, cloud-init, démarrage + 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 : +- Fichiers d'intégration officiels (docker, MariaDB, UI) : +- Liste des réglages, extraite des `plugin.json` : +- OWASP Core Rule Set : diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..2e64d99 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +inventory = inventory/hosts.yml +roles_path = roles +collections_path = ./collections:~/.ansible/collections +host_key_checking = False +stdout_callback = default +result_format = yaml +retry_files_enabled = False +interpreter_python = auto_silent + +[ssh_connection] +pipelining = True diff --git a/docs/architecture.excalidraw b/docs/architecture.excalidraw new file mode 100644 index 0000000..ba51fed --- /dev/null +++ b/docs/architecture.excalidraw @@ -0,0 +1,2598 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "null", + "elements": [ + { + "id": "title", + "type": "text", + "x": 470, + "y": 20, + "width": 547, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": null, + "seed": 1536219028, + "version": 2, + "versionNonce": 1272381996, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752318, + "link": null, + "locked": false, + "text": "PoC BunkerWeb 1.6.17 sur Proxmox", + "fontSize": 32, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "PoC BunkerWeb 1.6.17 sur Proxmox", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "sub", + "type": "text", + "x": 380, + "y": 68, + "width": 864, + "height": 22.5, + "angle": 0, + "strokeColor": "#757575", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 70851500, + "version": 2, + "versionNonce": 1118996116, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "docker-compose sur 3 VM Debian 13, déployé par Ansible - le WAF est le seul chemin vers les applis", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "docker-compose sur 3 VM Debian 13, déployé par Ansible - le WAF est le seul chemin vers les applis", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "pve", + "type": "rectangle", + "x": 300, + "y": 120, + "width": 1360, + "height": 750, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#f8f9fa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 3 + }, + "seed": 1278788884, + "version": 2, + "versionNonce": 1552800940, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "pvet", + "type": "text", + "x": 320, + "y": 132, + "width": 259, + "height": 32.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 1148241452, + "version": 2, + "versionNonce": 1632408596, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "Proxmox VE - pve01", + "fontSize": 26, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Proxmox VE - pve01", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "bus", + "type": "rectangle", + "x": 320, + "y": 800, + "width": 1320, + "height": 44, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#d3f9d8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 3 + }, + "seed": 210174612, + "version": 2, + "versionNonce": 377841452, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "-wLdJuCSSU3isLGRUYsjf" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "vcli", + "type": "rectangle", + "x": 330, + "y": 260, + "width": 320, + "height": 300, + "angle": 0, + "strokeColor": "#f59e0b", + "backgroundColor": "#fff4e6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": { + "type": 3 + }, + "seed": 625942700, + "version": 2, + "versionNonce": 206107028, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "vclit", + "type": "text", + "x": 348, + "y": 272, + "width": 160, + "height": 27.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": null, + "seed": 1932627988, + "version": 2, + "versionNonce": 1848091052, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "bw-client - .63", + "fontSize": 22, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bw-client - .63", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "tools", + "type": "rectangle", + "x": 350, + "y": 320, + "width": 280, + "height": 56, + "angle": 0, + "strokeColor": "#f59e0b", + "backgroundColor": "#ffe8cc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": { + "type": 3 + }, + "seed": 1048455980, + "version": 2, + "versionNonce": 584949524, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "eHqZIridF0wrU6HRLPRVo" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "tests", + "type": "rectangle", + "x": 350, + "y": 400, + "width": 280, + "height": 80, + "angle": 0, + "strokeColor": "#f59e0b", + "backgroundColor": "#ffe8cc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": { + "type": 3 + }, + "seed": 271478164, + "version": 2, + "versionNonce": 983469100, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "ud-DFX9hc28YXTLCu3ycp" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "lcli", + "type": "arrow", + "x": 490, + "y": 560, + "width": 0, + "height": 240, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": null, + "seed": 577234348, + "version": 2, + "versionNonce": 763403412, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.5 + ], + [ + 0, + 239.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null, + "elbowed": false + }, + { + "id": "vwaf", + "type": "rectangle", + "x": 700, + "y": 180, + "width": 520, + "height": 560, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#fff5f5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": { + "type": 3 + }, + "seed": 78879508, + "version": 2, + "versionNonce": 1574184620, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "vwaft", + "type": "text", + "x": 718, + "y": 192, + "width": 137, + "height": 27.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": null, + "seed": 104222764, + "version": 2, + "versionNonce": 904391188, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "bw-waf - .61", + "fontSize": 22, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bw-waf - .61", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "bw", + "type": "rectangle", + "x": 730, + "y": 230, + "width": 460, + "height": 110, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aC", + "roundness": { + "type": 3 + }, + "seed": 1530337428, + "version": 2, + "versionNonce": 1819439404, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "yTLo2Vng9jiScCVL5wf8x" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "sched", + "type": "rectangle", + "x": 730, + "y": 420, + "width": 220, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffe3e3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aD", + "roundness": { + "type": 3 + }, + "seed": 1775463084, + "version": 2, + "versionNonce": 520718228, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "hguasmJb74910iJe48rlW" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "ui", + "type": "rectangle", + "x": 970, + "y": 420, + "width": 220, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffe3e3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aE", + "roundness": { + "type": 3 + }, + "seed": 1601103380, + "version": 2, + "versionNonce": 1440967596, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "WIA-EPcOdhCM-V6poW90H" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "db", + "type": "rectangle", + "x": 730, + "y": 600, + "width": 220, + "height": 80, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aF", + "roundness": { + "type": 3 + }, + "seed": 1072248108, + "version": 2, + "versionNonce": 2098951444, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "TymlawtgjzMOi0T6nTv_x" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "redis", + "type": "rectangle", + "x": 970, + "y": 600, + "width": 220, + "height": 80, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "#e9ecef", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aG", + "roundness": { + "type": 3 + }, + "seed": 1985020820, + "version": 2, + "versionNonce": 206640684, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "3UXlHuNY841IZz4g0ZNcQ" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "netu", + "type": "text", + "x": 790, + "y": 352, + "width": 231, + "height": 17.5, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aH", + "roundness": null, + "seed": 367112108, + "version": 2, + "versionNonce": 1838396052, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "bw-universe 10.20.30.0/24 (API)", + "fontSize": 14, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bw-universe 10.20.30.0/24 (API)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "netd", + "type": "text", + "x": 745, + "y": 548, + "width": 96, + "height": 17.5, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aI", + "roundness": null, + "seed": 1980985620, + "version": 2, + "versionNonce": 749039788, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "réseau bw-db", + "fontSize": 14, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "réseau bw-db", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a_sched", + "type": "arrow", + "x": 760, + "y": 420, + "width": 0, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": null, + "seed": 1004984876, + "version": 2, + "versionNonce": 1864908820, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + -0.5 + ], + [ + 0, + -79.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_schedt", + "type": "text", + "x": 768, + "y": 372, + "width": 35, + "height": 18.75, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aK", + "roundness": null, + "seed": 2116824724, + "version": 2, + "versionNonce": 646609708, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "API", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "API", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a_ui", + "type": "arrow", + "x": 1080, + "y": 340, + "width": 0, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 1826796716, + "version": 2, + "versionNonce": 969874836, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.5 + ], + [ + 0, + 79.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_uit", + "type": "text", + "x": 1090, + "y": 372, + "width": 83, + "height": 18.75, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aM", + "roundness": null, + "seed": 1330723860, + "version": 2, + "versionNonce": 1684682156, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "proxy bwui", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "proxy bwui", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a_sdb", + "type": "arrow", + "x": 840, + "y": 500, + "width": 0, + "height": 100, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": null, + "seed": 1988050732, + "version": 2, + "versionNonce": 381569812, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.5 + ], + [ + 0, + 99.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_udb", + "type": "arrow", + "x": 1020, + "y": 500, + "width": 120, + "height": 100, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 595106196, + "version": 2, + "versionNonce": 1936629804, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + -0.5, + 0.5 + ], + [ + -119.5, + 99.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_sred", + "type": "arrow", + "x": 940, + "y": 500, + "width": 100, + "height": 100, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aP", + "roundness": null, + "seed": 1629911468, + "version": 2, + "versionNonce": 946587796, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0.5 + ], + [ + 99.5, + 99.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "lwaf", + "type": "arrow", + "x": 960, + "y": 740, + "width": 0, + "height": 60, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aQ", + "roundness": null, + "seed": 98060052, + "version": 2, + "versionNonce": 729922220, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.5 + ], + [ + 0, + 59.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null, + "elbowed": false + }, + { + "id": "vapp", + "type": "rectangle", + "x": 1290, + "y": 180, + "width": 350, + "height": 560, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#e7f5ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aR", + "roundness": { + "type": 3 + }, + "seed": 402809900, + "version": 2, + "versionNonce": 311268884, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "vappt", + "type": "text", + "x": 1308, + "y": 192, + "width": 153, + "height": 27.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aS", + "roundness": null, + "seed": 719733908, + "version": 2, + "versionNonce": 1464698156, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "bw-apps - .62", + "fontSize": 22, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "bw-apps - .62", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "www", + "type": "rectangle", + "x": 1310, + "y": 240, + "width": 310, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#d0ebff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aT", + "roundness": { + "type": 3 + }, + "seed": 285015724, + "version": 2, + "versionNonce": 840545172, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "vZ2I-5RJHooP7DQTsH2ul" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "shop", + "type": "rectangle", + "x": 1310, + "y": 340, + "width": 310, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#d0ebff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU", + "roundness": { + "type": 3 + }, + "seed": 1213764116, + "version": 2, + "versionNonce": 1547435948, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_QqJdXGiIbXNZuILuYvGY" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "api", + "type": "rectangle", + "x": 1310, + "y": 440, + "width": 310, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#d0ebff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aV", + "roundness": { + "type": 3 + }, + "seed": 1958758700, + "version": 2, + "versionNonce": 356917524, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_rNV8XuTeLBAhBJAUkdOm" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "fw", + "type": "rectangle", + "x": 1310, + "y": 600, + "width": 310, + "height": 80, + "angle": 0, + "strokeColor": "#f59e0b", + "backgroundColor": "#fff3bf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aW", + "roundness": { + "type": 3 + }, + "seed": 1831153556, + "version": 2, + "versionNonce": 1651976748, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "qqyyGfg_SvBJDhoUUdnEh" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "lapp", + "type": "arrow", + "x": 1465, + "y": 740, + "width": 0, + "height": 60, + "angle": 0, + "strokeColor": "#868e96", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aX", + "roundness": null, + "seed": 1186349996, + "version": 2, + "versionNonce": 59301524, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0.5 + ], + [ + 0, + 59.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null, + "elbowed": false + }, + { + "id": "a_www", + "type": "arrow", + "x": 1190, + "y": 255, + "width": 120, + "height": 25, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aY", + "roundness": null, + "seed": 1501699348, + "version": 2, + "versionNonce": 2095645868, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0.5 + ], + [ + 119.5, + 24.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_shop", + "type": "arrow", + "x": 1190, + "y": 285, + "width": 120, + "height": 95, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aZ", + "roundness": null, + "seed": 218689068, + "version": 2, + "versionNonce": 1704455188, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0.5 + ], + [ + 119.5, + 94.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_api", + "type": "arrow", + "x": 1190, + "y": 315, + "width": 120, + "height": 165, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aa", + "roundness": null, + "seed": 1457871508, + "version": 2, + "versionNonce": 1362857772, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0.5 + ], + [ + 119.5, + 164.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_appt", + "type": "text", + "x": 1312, + "y": 538, + "width": 193, + "height": 37.5, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ab", + "roundness": null, + "seed": 2023901356, + "version": 2, + "versionNonce": 493439380, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "HTTP 8081-8083\ndepuis bw-waf uniquement", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "HTTP 8081-8083\ndepuis bw-waf uniquement", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "a_cli", + "type": "arrow", + "x": 650, + "y": 380, + "width": 80, + "height": 70, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 3, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ac", + "roundness": null, + "seed": 268757012, + "version": 2, + "versionNonce": 1071172012, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + -0.5 + ], + [ + 79.5, + -69.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_clit", + "type": "text", + "x": 345, + "y": 572, + "width": 133, + "height": 37.5, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ad", + "roundness": null, + "seed": 221008684, + "version": 2, + "versionNonce": 1580398356, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "attaques HTTPS\nvia /etc/hosts", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "attaques HTTPS\nvia /etc/hosts", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "admin", + "type": "rectangle", + "x": 40, + "y": 150, + "width": 220, + "height": 90, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "#f3f0ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ae", + "roundness": { + "type": 3 + }, + "seed": 1758905748, + "version": 2, + "versionNonce": 199930924, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "bfJhe4UvG8FsofCpAoNkZ" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "a_adm", + "type": "arrow", + "x": 260, + "y": 190, + "width": 470, + "height": 65, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 3, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "af", + "roundness": null, + "seed": 1225854380, + "version": 2, + "versionNonce": 74118292, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0.5 + ], + [ + 469.5, + 64.5 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_admt", + "type": "text", + "x": 330, + "y": 172, + "width": 245, + "height": 18.75, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ag", + "roundness": null, + "seed": 1452339988, + "version": 2, + "versionNonce": 56315564, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "HTTPS www / shop / api / bwui", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "HTTPS www / shop / api / bwui", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "ansible", + "type": "rectangle", + "x": 40, + "y": 600, + "width": 220, + "height": 100, + "angle": 0, + "strokeColor": "#0ca678", + "backgroundColor": "#e6fcf5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ah", + "roundness": { + "type": 3 + }, + "seed": 1299871788, + "version": 2, + "versionNonce": 1841775124, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "HyBQHnKMlKXlGiehADIur" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "a_ans", + "type": "arrow", + "x": 260, + "y": 650, + "width": 40, + "height": 0, + "angle": 0, + "strokeColor": "#0ca678", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ai", + "roundness": null, + "seed": 933851284, + "version": 2, + "versionNonce": 651467052, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "points": [ + [ + 0.5, + 0 + ], + [ + 39.5, + 0 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "a_anst", + "type": "text", + "x": 40, + "y": 712, + "width": 221, + "height": 37.5, + "angle": 0, + "strokeColor": "#0ca678", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aj", + "roundness": null, + "seed": 106074796, + "version": 2, + "versionNonce": 1363009428, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "SSH root : qm create / clone\nSSH debian : Docker, compose", + "fontSize": 15, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "SSH root : qm create / clone\nSSH debian : Docker, compose", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "sitest", + "type": "text", + "x": 300, + "y": 892, + "width": 652, + "height": 27.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ak", + "roundness": null, + "seed": 735460884, + "version": 2, + "versionNonce": 1371575212, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752319, + "link": null, + "locked": false, + "text": "Sites protégés (*.bwpoc.internal) et fonctionnalités testées", + "fontSize": 22, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Sites protégés (*.bwpoc.internal) et fonctionnalités testées", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "s1", + "type": "rectangle", + "x": 300, + "y": 935, + "width": 325, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#fff5f5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "al", + "roundness": { + "type": 3 + }, + "seed": 1380146476, + "version": 2, + "versionNonce": 386190612, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "XxyPlQj-5QnCZ1Zmf_BXE" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "s2", + "type": "rectangle", + "x": 645, + "y": 935, + "width": 325, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#fff5f5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "am", + "roundness": { + "type": 3 + }, + "seed": 1812814740, + "version": 2, + "versionNonce": 1648967212, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "4MHW2FcpXx1t6mSqSXVTO" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "s3", + "type": "rectangle", + "x": 990, + "y": 935, + "width": 325, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#fff5f5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "an", + "roundness": { + "type": 3 + }, + "seed": 716625836, + "version": 2, + "versionNonce": 814877332, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "e5UYkx6aghzm2w0I3MXB-" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "s4", + "type": "rectangle", + "x": 1335, + "y": 935, + "width": 325, + "height": 80, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#fff5f5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ao", + "roundness": { + "type": 3 + }, + "seed": 579127572, + "version": 2, + "versionNonce": 1633196204, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "kcDtjZHiLETGgDHdTT3HH" + } + ], + "updated": 1793095752319, + "link": null, + "locked": false + }, + { + "id": "-wLdJuCSSU3isLGRUYsjf", + "type": "text", + "x": 861.5, + "y": 810.75, + "width": 237, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ap", + "roundness": null, + "seed": 1889245204, + "version": 3, + "versionNonce": 1644755732, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "vmbr0 - 192.168.200.0/24", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "bus", + "originalText": "vmbr0 - 192.168.200.0/24", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eHqZIridF0wrU6HRLPRVo", + "type": "text", + "x": 386, + "y": 336.75, + "width": 208, + "height": 22.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aq", + "roundness": null, + "seed": 1662481812, + "version": 3, + "versionNonce": 1533472812, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "curl · sqlmap · nmap · ab", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tools", + "originalText": "curl · sqlmap · nmap · ab", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "ud-DFX9hc28YXTLCu3ycp", + "type": "text", + "x": 373.5, + "y": 417.5, + "width": 233, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ar", + "roundness": null, + "seed": 624665364, + "version": 3, + "versionNonce": 652245140, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bwpoc-tests\n20 contrôles automatisés", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tests", + "originalText": "bwpoc-tests\n20 contrôles automatisés", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "yTLo2Vng9jiScCVL5wf8x", + "type": "text", + "x": 796, + "y": 251.25, + "width": 328, + "height": 67.5, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "as", + "roundness": null, + "seed": 1031676052, + "version": 3, + "versionNonce": 267389612, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bunkerweb\nnginx + ModSecurity + OWASP CRS 4\n:80 / :443 (TCP + UDP HTTP/3)", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "bw", + "originalText": "bunkerweb\nnginx + ModSecurity + OWASP CRS 4\n:80 / :443 (TCP + UDP HTTP/3)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hguasmJb74910iJe48rlW", + "type": "text", + "x": 775, + "y": 437.5, + "width": 130, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "at", + "roundness": null, + "seed": 1939140116, + "version": 3, + "versionNonce": 787236372, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bw-scheduler\ngénère la conf", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "sched", + "originalText": "bw-scheduler\ngénère la conf", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "WIA-EPcOdhCM-V6poW90H", + "type": "text", + "x": 1017.5, + "y": 437.5, + "width": 125, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "au", + "roundness": null, + "seed": 678824852, + "version": 3, + "versionNonce": 963761452, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bw-ui :7000\ninterface web", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ui", + "originalText": "bw-ui :7000\ninterface web", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "TymlawtgjzMOi0T6nTv_x", + "type": "text", + "x": 788, + "y": 617.5, + "width": 104, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "av", + "roundness": null, + "seed": 777484564, + "version": 3, + "versionNonce": 790782868, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bw-db\nMariaDB 11", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "db", + "originalText": "bw-db\nMariaDB 11", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3UXlHuNY841IZz4g0ZNcQ", + "type": "text", + "x": 1003, + "y": 617.5, + "width": 154, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aw", + "roundness": null, + "seed": 548353684, + "version": 3, + "versionNonce": 357978028, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bw-redis\nbans et sessions", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "redis", + "originalText": "bw-redis\nbans et sessions", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "vZ2I-5RJHooP7DQTsH2ul", + "type": "text", + "x": 1391, + "y": 257.5, + "width": 148, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ax", + "roundness": null, + "seed": 1009992724, + "version": 3, + "versionNonce": 1303957780, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "poc-www - nginx\nvitrine :8081", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "www", + "originalText": "poc-www - nginx\nvitrine :8081", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_QqJdXGiIbXNZuILuYvGY", + "type": "text", + "x": 1366, + "y": 357.5, + "width": 198, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ay", + "roundness": null, + "seed": 920232340, + "version": 3, + "versionNonce": 600275500, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "poc-shop - Juice Shop\n:8082", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "shop", + "originalText": "poc-shop - Juice Shop\n:8082", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_rNV8XuTeLBAhBJAUkdOm", + "type": "text", + "x": 1374.5, + "y": 457.5, + "width": 181, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "az", + "roundness": null, + "seed": 400051988, + "version": 3, + "versionNonce": 2049281684, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "poc-api - go-httpbin\n:8083", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "api", + "originalText": "poc-api - go-httpbin\n:8083", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "qqyyGfg_SvBJDhoUUdnEh", + "type": "text", + "x": 1357.5, + "y": 617.5, + "width": 215, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b00", + "roundness": null, + "seed": 262491284, + "version": 3, + "versionNonce": 1336890540, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "iptables DOCKER-USER\nseul bw-waf passe", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "fw", + "originalText": "iptables DOCKER-USER\nseul bw-waf passe", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "bfJhe4UvG8FsofCpAoNkZ", + "type": "text", + "x": 73.5, + "y": 172.5, + "width": 153, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b01", + "roundness": null, + "seed": 1341561364, + "version": 3, + "versionNonce": 1159633940, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "Navigateur admin\nhosts vers .61", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "admin", + "originalText": "Navigateur admin\nhosts vers .61", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "HyBQHnKMlKXlGiehADIur", + "type": "text", + "x": 65.5, + "y": 627.5, + "width": 169, + "height": 45, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b02", + "roundness": null, + "seed": 1673672596, + "version": 3, + "versionNonce": 381148972, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "Contrôleur Ansible\nplaybooks 00 à 50", + "fontSize": 18, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ansible", + "originalText": "Contrôleur Ansible\nplaybooks 00 à 50", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "XxyPlQj-5QnCZ1Zmf_BXE", + "type": "text", + "x": 344.5, + "y": 955, + "width": 236, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b03", + "roundness": null, + "seed": 121513236, + "version": 3, + "versionNonce": 949614996, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "www - vitrine\nantibot JS, en-têtes, HTTPS", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "s1", + "originalText": "www - vitrine\nantibot JS, en-têtes, HTTPS", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "4MHW2FcpXx1t6mSqSXVTO", + "type": "text", + "x": 694, + "y": 955, + "width": 227, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b04", + "roundness": null, + "seed": 2145411732, + "version": 3, + "versionNonce": 827005356, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "shop - Juice Shop\nWAF SQLi / XSS / traversée", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "s2", + "originalText": "shop - Juice Shop\nWAF SQLi / XSS / traversée", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "e5UYkx6aghzm2w0I3MXB-", + "type": "text", + "x": 1032.5, + "y": 955, + "width": 240, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b05", + "roundness": null, + "seed": 1952378900, + "version": 3, + "versionNonce": 802006804, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "api - go-httpbin\ndébit, méthodes, bad behavior", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "s3", + "originalText": "api - go-httpbin\ndébit, méthodes, bad behavior", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "kcDtjZHiLETGgDHdTT3HH", + "type": "text", + "x": 1401.5, + "y": 955, + "width": 192, + "height": 40, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b06", + "roundness": null, + "seed": 1152372116, + "version": 3, + "versionNonce": 982713388, + "isDeleted": false, + "boundElements": null, + "updated": 1793095752325, + "link": null, + "locked": false, + "text": "bwui - UI BunkerWeb\nrapports, bans, réglages", + "fontSize": 16, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "s4", + "originalText": "bwui - UI BunkerWeb\nrapports, bans, réglages", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 0000000..1ba2c47 Binary files /dev/null and b/docs/architecture.png differ diff --git a/examples/split/apps.compose.yml b/examples/split/apps.compose.yml new file mode 100644 index 0000000..cf9bc88 --- /dev/null +++ b/examples/split/apps.compose.yml @@ -0,0 +1,41 @@ +# Ansible managed +# Sites de test du PoC BunkerWeb, mode split +# Ports publiés sur 192.168.200.62, filtrés par la chaîne iptables BWPOC : +# seul le WAF (192.168.200.61) peut les joindre. + +services: + www: + image: nginx:1.30-alpine + container_name: poc-www + restart: unless-stopped + read_only: true + tmpfs: + - /var/cache/nginx + - /run + - /tmp + volumes: + - ./www:/usr/share/nginx/html:ro + ports: + - "192.168.200.62:8081:80" + networks: [apps] + + shop: + image: bkimminich/juice-shop:v20.3.0 + container_name: poc-shop + restart: unless-stopped + ports: + - "192.168.200.62:8082:3000" + networks: [apps] + + api: + image: mccutchen/go-httpbin:2.26.0 + container_name: poc-api + restart: unless-stopped + read_only: true + ports: + - "192.168.200.62:8083:8080" + networks: [apps] + +networks: + apps: + name: poc-apps diff --git a/examples/split/bunkerweb.compose.yml b/examples/split/bunkerweb.compose.yml new file mode 100644 index 0000000..3163694 --- /dev/null +++ b/examples/split/bunkerweb.compose.yml @@ -0,0 +1,155 @@ +# Ansible managed +# Stack BunkerWeb 1.6.17 : WAF + scheduler + UI + MariaDB + Redis +# Mode split, applis sur 192.168.200.62 +x-db-env: &db-env + DATABASE_URI: "mariadb+pymysql://bunkerweb:CHANGEME@bw-db:3306/db" + +x-api-env: &api-env + API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24" + +services: + bunkerweb: + image: bunkerity/bunkerweb:1.6.17 + container_name: bunkerweb + restart: unless-stopped + ports: + - "80:8080/tcp" + - "443:8443/tcp" + - "443:8443/udp" # HTTP/3 (QUIC) + labels: + - "bunkerweb.INSTANCE=yes" + environment: + <<: [*db-env, *api-env] + networks: + - bw-universe + - bw-services + + bw-scheduler: + image: bunkerity/bunkerweb-scheduler:1.6.17 + container_name: bw-scheduler + restart: unless-stopped + depends_on: + - bunkerweb + - bw-db + - bw-redis + volumes: + - bw-storage:/data + environment: + <<: [*db-env, *api-env] + BUNKERWEB_INSTANCES: "bunkerweb" + SERVER_NAME: "www.bwpoc.internal shop.bwpoc.internal api.bwpoc.internal bwui.bwpoc.internal" + # --- Réglages globaux --- + MULTISITE: "yes" + DISABLE_DEFAULT_SERVER: "yes" + SERVE_FILES: "no" + GENERATE_SELF_SIGNED_SSL: "yes" + AUTO_REDIRECT_HTTP_TO_HTTPS: "yes" + USE_MODSECURITY: "yes" + USE_MODSECURITY_CRS: "yes" + MODSECURITY_CRS_VERSION: "4" + USE_BAD_BEHAVIOR: "yes" + BAD_BEHAVIOR_THRESHOLD: "10" + BAD_BEHAVIOR_COUNT_TIME: "1m" + BAD_BEHAVIOR_BAN_TIME: "5m" + USE_BLACKLIST: "yes" + BLACKLIST_USER_AGENT: "PoC-EvilBot" + USE_GZIP: "yes" + USE_BROTLI: "yes" + USE_REDIS: "yes" + REDIS_HOST: "bw-redis" + REDIS_PASSWORD: "CHANGEME" + SEND_ANONYMOUS_REPORT: "no" + # --- www.bwpoc.internal --- + www.bwpoc.internal_USE_REVERSE_PROXY: "yes" + www.bwpoc.internal_REVERSE_PROXY_URL: "/" + www.bwpoc.internal_REVERSE_PROXY_HOST: "http://192.168.200.62:8081" + www.bwpoc.internal_SELF_SIGNED_SSL_SUBJ: "/CN=www.bwpoc.internal/" + www.bwpoc.internal_USE_ANTIBOT: "javascript" + www.bwpoc.internal_LIMIT_REQ_RATE: "10r/s" + # --- shop.bwpoc.internal --- + shop.bwpoc.internal_USE_REVERSE_PROXY: "yes" + shop.bwpoc.internal_REVERSE_PROXY_URL: "/" + shop.bwpoc.internal_REVERSE_PROXY_HOST: "http://192.168.200.62:8082" + shop.bwpoc.internal_SELF_SIGNED_SSL_SUBJ: "/CN=shop.bwpoc.internal/" + shop.bwpoc.internal_REVERSE_PROXY_WS: "yes" + shop.bwpoc.internal_LIMIT_REQ_RATE: "30r/s" + shop.bwpoc.internal_COOKIE_FLAGS: "* SameSite=Lax" + # --- api.bwpoc.internal --- + api.bwpoc.internal_USE_REVERSE_PROXY: "yes" + api.bwpoc.internal_REVERSE_PROXY_URL: "/" + api.bwpoc.internal_REVERSE_PROXY_HOST: "http://192.168.200.62:8083" + api.bwpoc.internal_SELF_SIGNED_SSL_SUBJ: "/CN=api.bwpoc.internal/" + api.bwpoc.internal_ALLOWED_METHODS: "GET|POST|HEAD" + api.bwpoc.internal_MAX_CLIENT_SIZE: "1m" + api.bwpoc.internal_LIMIT_REQ_RATE: "5r/s" + api.bwpoc.internal_CUSTOM_CONF_MODSEC_poc-rule: "SecRule ARGS:poc \"@streq attack\" \"id:10001,phase:2,deny,status:403,log,msg:'PoC BunkerWeb : regle maison'\"" + # --- bwui.bwpoc.internal --- + bwui.bwpoc.internal_USE_REVERSE_PROXY: "yes" + bwui.bwpoc.internal_REVERSE_PROXY_URL: "/" + bwui.bwpoc.internal_REVERSE_PROXY_HOST: "http://bw-ui:7000" + bwui.bwpoc.internal_SELF_SIGNED_SSL_SUBJ: "/CN=bwui.bwpoc.internal/" + bwui.bwpoc.internal_USE_TEMPLATE: "ui" + networks: + - bw-universe + - bw-db + + bw-ui: + image: bunkerity/bunkerweb-ui:1.6.17 + container_name: bw-ui + restart: unless-stopped + depends_on: + - bw-db + volumes: + - bw-ui-data:/data # secrets Flask, clés TOTP et Biscuit + environment: + <<: *db-env + ADMIN_USERNAME: "admin" + ADMIN_PASSWORD: "CHANGEME" + TZ: "Europe/Paris" + networks: + - bw-universe + - bw-db + + bw-db: + image: mariadb:11 + container_name: bw-db + restart: unless-stopped + command: --max-allowed-packet=67108864 + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: "db" + MYSQL_USER: "bunkerweb" + MYSQL_PASSWORD: "CHANGEME" + volumes: + - bw-data:/var/lib/mysql + networks: + - bw-db + + bw-redis: + image: redis:8-alpine + container_name: bw-redis + restart: unless-stopped + command: ["redis-server", "--requirepass", "CHANGEME", "--save", "60", "1"] + volumes: + - bw-redis:/data + networks: + - bw-universe + +volumes: + bw-data: + bw-storage: + bw-ui-data: + bw-redis: + +networks: + bw-universe: + name: bw-universe + ipam: + driver: default + config: + - subnet: 10.20.30.0/24 # seul réseau autorisé à parler à l'API BunkerWeb + bw-services: + name: bw-services # réseau des applis (mode single), créé par Ansible + external: true + bw-db: + name: bw-db diff --git a/examples/split/bwpoc-firewall.sh b/examples/split/bwpoc-firewall.sh new file mode 100644 index 0000000..c5f4206 --- /dev/null +++ b/examples/split/bwpoc-firewall.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Ansible managed +# N'autorise que le WAF (et les sources listées) à joindre les ports publiés +# des applis. Les règles vont dans DOCKER-USER, consultée par Docker avant +# ses propres règles ; --ctorigdstport filtre sur le port publié (avant DNAT). +set -eu +IFACE="ens18" +PORTS="8081:8083" + +iptables -N BWPOC 2>/dev/null || iptables -F BWPOC +iptables -A BWPOC -s 192.168.200.61 -j RETURN +iptables -A BWPOC -j DROP + +RULE="-i $IFACE -p tcp -m conntrack --ctstate NEW --ctorigdstport $PORTS -j BWPOC" +# shellcheck disable=SC2086 +iptables -C DOCKER-USER $RULE 2>/dev/null || iptables -I DOCKER-USER $RULE diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml new file mode 100644 index 0000000..99ef749 --- /dev/null +++ b/inventory/group_vars/all.yml @@ -0,0 +1,62 @@ +--- +# ===================================================================== +# Variables communes du PoC BunkerWeb +# Les valeurs marquées "A ADAPTER" dépendent de votre infra. +# ===================================================================== + +# --- Domaine de test ------------------------------------------------- +# Les sites seront www., shop., api., bwui. +# ".internal" est réservé par l'ICANN pour l'usage privé : pas de collision possible. +poc_domain: bwpoc.internal # A ADAPTER si vous avez une zone DNS de labo + +# --- Réseau des VM (cloud-init) -------------------------------------- +poc_net_prefix: 24 # A ADAPTER +poc_net_gateway: 192.168.200.1 # A ADAPTER +poc_net_dns: [192.168.200.1] # A ADAPTER +poc_net_searchdomain: "{{ poc_domain }}" + +# --- Proxmox ---------------------------------------------------------- +pve_storage: local-lvm # A ADAPTER : stockage des disques VM +pve_bridge: vmbr0 # A ADAPTER +pve_vlan_tag: "" # vide = pas de VLAN, sinon ex. "200" +pve_template_vmid: 9100 +pve_template_name: debian13-cloud +pve_image_url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 +pve_image_path: /var/lib/vz/template/debian-13-genericcloud-amd64.qcow2 + +# --- Accès aux VM ----------------------------------------------------- +poc_vm_user: debian +poc_ssh_pubkey_file: "~/.ssh/id_ed25519.pub" # clé injectée par cloud-init + +# --- Versions (épinglées) -------------------------------------------- +bunkerweb_version: "1.6.17" +mariadb_image: "mariadb:11" +redis_image: "redis:8-alpine" +juiceshop_image: "bkimminich/juice-shop:v20.3.0" +httpbin_image: "mccutchen/go-httpbin:2.26.0" +nginx_image: "nginx:1.30-alpine" + +# --- Arborescence sur les VM ----------------------------------------- +poc_base_dir: /opt/bwpoc + +# --- Ports publiés par la VM applis (mode split) --------------------- +apps_ports: + www: 8081 + shop: 8082 + api: 8083 + +# Ports d'écoute dans les conteneurs (utilisés en mode single) +apps_internal_ports: + www: 80 + shop: 3000 + api: 8080 + +# Sources autorisées à joindre directement les applis, en plus du WAF. +# Ajoutez l'IP du client pour comparer "avec / sans WAF" (voir README). +apps_direct_access_sources: [] + +# Mode calculé : "single" si WAF et applis sont sur le même hôte. +poc_mode: "{{ 'single' if (groups['apps'] | first) == (groups['bunkerweb'] | first) else 'split' }}" +waf_ip: "{{ hostvars[groups['bunkerweb'] | first]['ansible_host'] }}" +apps_ip: "{{ hostvars[groups['apps'] | first]['ansible_host'] }}" +client_ip: "{{ hostvars[groups['client'] | first]['ansible_host'] }}" diff --git a/inventory/group_vars/bunkerweb.yml b/inventory/group_vars/bunkerweb.yml new file mode 100644 index 0000000..72b6548 --- /dev/null +++ b/inventory/group_vars/bunkerweb.yml @@ -0,0 +1,79 @@ +--- +# ===================================================================== +# Réglages BunkerWeb du PoC +# Chaque site met en avant un groupe de fonctionnalités (voir README). +# Noms de paramètres vérifiés sur les plugin.json de BunkerWeb 1.6.17. +# ===================================================================== + +# Secrets générés au premier lancement et stockés côté contrôleur +# dans ./credentials (ignoré par git). Remplaçables par ansible-vault. +bw_db_password: "{{ lookup('ansible.builtin.password', playbook_dir ~ '/../credentials/bw_db_password', chars=['ascii_letters', 'digits'], length=24) }}" +bw_ui_admin_user: admin +# La politique de l'UI impose majuscule, minuscule, chiffre et caractère spécial. +bw_ui_admin_password: "{{ lookup('ansible.builtin.password', playbook_dir ~ '/../credentials/bw_ui_admin_password', chars=['ascii_letters', 'digits'], length=20) }}!Aa9" +bw_redis_password: "{{ lookup('ansible.builtin.password', playbook_dir ~ '/../credentials/bw_redis_password', chars=['ascii_letters', 'digits'], length=24) }}" + +# Bannissement court pour pouvoir rejouer les tests (défaut BunkerWeb : 1d) +bw_ban_time: 5m + +# Réglages globaux (contexte global ou valeurs par défaut de tous les sites) +bw_global_settings: + MULTISITE: "yes" + DISABLE_DEFAULT_SERVER: "yes" # Host inconnu = connexion fermée (444) + SERVE_FILES: "no" + # TLS : certificats auto-signés par site (pas d'ACME en labo fermé) + GENERATE_SELF_SIGNED_SSL: "yes" + AUTO_REDIRECT_HTTP_TO_HTTPS: "yes" + # WAF : ModSecurity + OWASP CRS v4 actifs partout (défaut BunkerWeb) + USE_MODSECURITY: "yes" + USE_MODSECURITY_CRS: "yes" + MODSECURITY_CRS_VERSION: "4" + # Bad behavior : 10 codes 4xx en 1 min = bannissement + USE_BAD_BEHAVIOR: "yes" + BAD_BEHAVIOR_THRESHOLD: "10" + BAD_BEHAVIOR_COUNT_TIME: "1m" + BAD_BEHAVIOR_BAN_TIME: "{{ bw_ban_time }}" + # Listes noires : listes communautaires + un User-Agent maison, testable à coup sûr + USE_BLACKLIST: "yes" + BLACKLIST_USER_AGENT: "PoC-EvilBot" + # Compression + USE_GZIP: "yes" + USE_BROTLI: "yes" + # Redis : bans et sessions partagés (prêt pour ajouter une 2e instance) + USE_REDIS: "yes" + REDIS_HOST: "bw-redis" + REDIS_PASSWORD: "{{ bw_redis_password }}" + SEND_ANONYMOUS_REPORT: "no" + +# Un dictionnaire par site. "upstream" = clé de apps_ports (ou URL complète). +bw_sites: + # Vitrine statique : antibot (challenge JavaScript), en-têtes de sécurité, compression + www: + upstream: www + settings: + USE_ANTIBOT: "javascript" + LIMIT_REQ_RATE: "10r/s" + # OWASP Juice Shop : appli volontairement vulnérable, terrain de jeu du WAF CRS + shop: + upstream: shop + settings: + REVERSE_PROXY_WS: "yes" # socket.io + LIMIT_REQ_RATE: "30r/s" # SPA Angular, beaucoup d'assets + COOKIE_FLAGS: "* SameSite=Lax" # l'appli lit certains cookies en JS + # go-httpbin : API de test pour méthodes, tailles, limites, bad behavior, règle maison + api: + upstream: api + settings: + ALLOWED_METHODS: "GET|POST|HEAD" + MAX_CLIENT_SIZE: "1m" + LIMIT_REQ_RATE: "5r/s" + # Règle ModSecurity maison : ?poc=attack => 403 + custom_confs: + MODSEC_poc-rule: >- + SecRule ARGS:poc "@streq attack" + "id:10001,phase:2,deny,status:403,log,msg:'PoC BunkerWeb : regle maison'" + # Interface d'administration de BunkerWeb + bwui: + upstream: "http://bw-ui:7000" + settings: + USE_TEMPLATE: "ui" diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..67ad841 --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,40 @@ +--- +# Inventaire du PoC BunkerWeb. +# Mode "split" (défaut) : 3 VM distinctes (WAF, applis, client). +# Mode "single" : voir inventory/single.yml (tout sur une seule VM). +all: + children: + proxmox: + hosts: + pve01: + ansible_host: 192.168.200.10 # A ADAPTER : nœud Proxmox qui héberge les VM + ansible_user: root + poc_vms: + vars: + ansible_user: debian + ansible_become: true + children: + bunkerweb: + hosts: + bw-waf: + ansible_host: 192.168.200.61 # A ADAPTER + vm_id: 9101 + vm_cores: 2 + vm_memory: 4096 + vm_disk_gb: 20 + apps: + hosts: + bw-apps: + ansible_host: 192.168.200.62 # A ADAPTER + vm_id: 9102 + vm_cores: 2 + vm_memory: 4096 + vm_disk_gb: 20 + client: + hosts: + bw-client: + ansible_host: 192.168.200.63 # A ADAPTER + vm_id: 9103 + vm_cores: 1 + vm_memory: 1024 + vm_disk_gb: 10 diff --git a/inventory/single.yml b/inventory/single.yml new file mode 100644 index 0000000..9eef7f3 --- /dev/null +++ b/inventory/single.yml @@ -0,0 +1,29 @@ +--- +# Variante "tout-en-un" : WAF, applis et outils de test sur la même VM. +# ansible-playbook -i inventory/single.yml playbooks/site.yml +all: + children: + proxmox: + hosts: + pve01: + ansible_host: 192.168.200.10 # A ADAPTER + ansible_user: root + poc_vms: + vars: + ansible_user: debian + ansible_become: true + children: + bunkerweb: + hosts: + bw-poc: + ansible_host: 192.168.200.61 # A ADAPTER + vm_id: 9101 + vm_cores: 4 + vm_memory: 6144 + vm_disk_gb: 30 + apps: + hosts: + bw-poc: + client: + hosts: + bw-poc: diff --git a/playbooks/00-proxmox.yml b/playbooks/00-proxmox.yml new file mode 100644 index 0000000..895b71f --- /dev/null +++ b/playbooks/00-proxmox.yml @@ -0,0 +1,9 @@ +--- +# Crée le modèle Debian 13 cloud-init puis clone les VM du PoC. +# A sauter si vous fournissez vos propres VM (Debian 12/13 avec accès SSH). +- name: Provisionner les VM sur Proxmox + hosts: proxmox + gather_facts: false + roles: + - proxmox_template + - proxmox_vm diff --git a/playbooks/10-base.yml b/playbooks/10-base.yml new file mode 100644 index 0000000..8b8ccb0 --- /dev/null +++ b/playbooks/10-base.yml @@ -0,0 +1,6 @@ +--- +- name: Socle des VM (paquets, résolution locale, Docker) + hosts: poc_vms + roles: + - common + - docker diff --git a/playbooks/20-apps.yml b/playbooks/20-apps.yml new file mode 100644 index 0000000..cf9852a --- /dev/null +++ b/playbooks/20-apps.yml @@ -0,0 +1,5 @@ +--- +- name: Déployer les sites de test + hosts: apps + roles: + - apps diff --git a/playbooks/30-bunkerweb.yml b/playbooks/30-bunkerweb.yml new file mode 100644 index 0000000..c33618c --- /dev/null +++ b/playbooks/30-bunkerweb.yml @@ -0,0 +1,5 @@ +--- +- name: Déployer BunkerWeb + hosts: bunkerweb + roles: + - bunkerweb diff --git a/playbooks/40-client.yml b/playbooks/40-client.yml new file mode 100644 index 0000000..ef5dbbb --- /dev/null +++ b/playbooks/40-client.yml @@ -0,0 +1,5 @@ +--- +- name: Préparer le client de test + hosts: client + roles: + - client diff --git a/playbooks/50-tests.yml b/playbooks/50-tests.yml new file mode 100644 index 0000000..0907841 --- /dev/null +++ b/playbooks/50-tests.yml @@ -0,0 +1,15 @@ +--- +# Lance la batterie de tests depuis le client et affiche le résultat. +- name: Tests fonctionnels BunkerWeb + hosts: client + gather_facts: false + tasks: + - name: Exécuter bwpoc-tests + ansible.builtin.command: "{{ poc_base_dir }}/tests/bwpoc-tests.sh" + register: tests + changed_when: false + failed_when: false + + - name: Résultat + ansible.builtin.debug: + msg: "{{ tests.stdout_lines }}" diff --git a/playbooks/99-destroy.yml b/playbooks/99-destroy.yml new file mode 100644 index 0000000..c189d96 --- /dev/null +++ b/playbooks/99-destroy.yml @@ -0,0 +1,33 @@ +--- +# Supprime les VM du PoC (le modèle est conservé, sauf -e destroy_template=true). +- name: Détruire les VM du PoC + hosts: proxmox + gather_facts: false + vars_prompt: + - name: confirm + prompt: "Taper 'detruire' pour supprimer les VM {{ groups['poc_vms'] | unique | join(', ') }}" + private: false + tasks: + - name: Abandon sans confirmation + ansible.builtin.meta: end_play + when: confirm != 'detruire' + + - name: Arrêter et supprimer les VM + ansible.builtin.shell: | + if qm status {{ hostvars[item].vm_id }} >/dev/null 2>&1; then + qm stop {{ hostvars[item].vm_id }} --skiplock 1 || true + qm destroy {{ hostvars[item].vm_id }} --purge 1 --destroy-unreferenced-disks 1 + echo destroyed + fi + register: destroy + changed_when: "'destroyed' in destroy.stdout" + loop: "{{ groups['poc_vms'] | unique }}" + + - name: Supprimer le modèle + ansible.builtin.shell: | + if qm status {{ pve_template_vmid }} >/dev/null 2>&1; then + qm destroy {{ pve_template_vmid }} --purge 1 && echo destroyed + fi + register: tpl_destroy + changed_when: "'destroyed' in tpl_destroy.stdout" + when: destroy_template | default(false) | bool diff --git a/playbooks/render.yml b/playbooks/render.yml new file mode 100644 index 0000000..c5ddb85 --- /dev/null +++ b/playbooks/render.yml @@ -0,0 +1,36 @@ +--- +# Génère les fichiers compose dans build// sans rien déployer, +# pour les relire ou les utiliser à la main : ansible-playbook playbooks/render.yml +- name: Rendu local des fichiers compose + hosts: bunkerweb:apps + connection: local + gather_facts: false + vars: + ansible_become: false + render_dir: "{{ playbook_dir }}/../build/{{ poc_mode }}" + ansible_default_ipv4: { interface: ens18 } + tasks: + - name: Répertoires de sortie + ansible.builtin.file: + path: "{{ render_dir }}/{{ item }}" + state: directory + mode: "0755" + loop: [bunkerweb, apps] + run_once: true + + - name: Compose BunkerWeb + ansible.builtin.template: + src: ../roles/bunkerweb/templates/compose.yml.j2 + dest: "{{ render_dir }}/bunkerweb/compose.yml" + mode: "0600" + when: inventory_hostname in groups['bunkerweb'] + + - name: Compose applis et filtrage + ansible.builtin.template: + src: "../roles/apps/templates/{{ item.src }}" + dest: "{{ render_dir }}/apps/{{ item.dest }}" + mode: "0644" + loop: + - { src: compose.yml.j2, dest: compose.yml } + - { src: bwpoc-firewall.sh.j2, dest: bwpoc-firewall.sh } + when: inventory_hostname in groups['apps'] diff --git a/playbooks/site.yml b/playbooks/site.yml new file mode 100644 index 0000000..20572c2 --- /dev/null +++ b/playbooks/site.yml @@ -0,0 +1,12 @@ +--- +# Déploiement complet du PoC : ansible-playbook playbooks/site.yml +- name: Proxmox + ansible.builtin.import_playbook: 00-proxmox.yml +- name: Socle + ansible.builtin.import_playbook: 10-base.yml +- name: Applis + ansible.builtin.import_playbook: 20-apps.yml +- name: BunkerWeb + ansible.builtin.import_playbook: 30-bunkerweb.yml +- name: Client + ansible.builtin.import_playbook: 40-client.yml diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..0418557 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: community.docker + version: ">=3.10.0" + - name: ansible.posix diff --git a/roles/apps/defaults/main.yml b/roles/apps/defaults/main.yml new file mode 100644 index 0000000..b14c395 --- /dev/null +++ b/roles/apps/defaults/main.yml @@ -0,0 +1,2 @@ +--- +apps_dir: "{{ poc_base_dir }}/apps" diff --git a/roles/apps/files/bwpoc-firewall.service b/roles/apps/files/bwpoc-firewall.service new file mode 100644 index 0000000..bae66f8 --- /dev/null +++ b/roles/apps/files/bwpoc-firewall.service @@ -0,0 +1,13 @@ +[Unit] +Description=PoC BunkerWeb - filtrage des ports applis (DOCKER-USER) +After=docker.service +Requires=docker.service +PartOf=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/sbin/bwpoc-firewall.sh + +[Install] +WantedBy=multi-user.target docker.service diff --git a/roles/apps/handlers/main.yml b/roles/apps/handlers/main.yml new file mode 100644 index 0000000..0403aad --- /dev/null +++ b/roles/apps/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Recharger le filtrage applis + ansible.builtin.systemd_service: + name: bwpoc-firewall.service + state: restarted + daemon_reload: true diff --git a/roles/apps/tasks/main.yml b/roles/apps/tasks/main.yml new file mode 100644 index 0000000..2eb0cc7 --- /dev/null +++ b/roles/apps/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: Créer l'arborescence des applis + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + loop: + - "{{ apps_dir }}" + - "{{ apps_dir }}/www" + +- name: Déposer la vitrine statique + ansible.builtin.template: + src: "www/{{ item }}.j2" + dest: "{{ apps_dir }}/www/{{ item }}" + mode: "0644" + loop: + - index.html + - contact.html + +- name: Créer le réseau partagé bw-services (mode single) + ansible.builtin.shell: docker network inspect bw-services >/dev/null 2>&1 || { docker network create bw-services && echo created; } + register: apps_net + changed_when: "'created' in apps_net.stdout" + when: poc_mode == 'single' + +- name: Générer le compose des applis + ansible.builtin.template: + src: compose.yml.j2 + dest: "{{ apps_dir }}/compose.yml" + mode: "0644" + validate: docker compose -f %s config -q + +- name: Démarrer les applis + community.docker.docker_compose_v2: + project_src: "{{ apps_dir }}" + project_name: apps + pull: missing + remove_orphans: true + state: present + +- name: Filtrage des ports publiés (mode split) + when: poc_mode == 'split' + block: + - name: Installer iptables + ansible.builtin.apt: + name: iptables + state: present + + - name: Déposer le script de filtrage + ansible.builtin.template: + src: bwpoc-firewall.sh.j2 + dest: /usr/local/sbin/bwpoc-firewall.sh + mode: "0755" + notify: Recharger le filtrage applis + + - name: Déposer l'unité systemd + ansible.builtin.copy: + src: bwpoc-firewall.service + dest: /etc/systemd/system/bwpoc-firewall.service + mode: "0644" + notify: Recharger le filtrage applis + + - name: Activer le filtrage au démarrage + ansible.builtin.systemd_service: + name: bwpoc-firewall.service + enabled: true + state: started + daemon_reload: true diff --git a/roles/apps/templates/bwpoc-firewall.sh.j2 b/roles/apps/templates/bwpoc-firewall.sh.j2 new file mode 100644 index 0000000..619c40d --- /dev/null +++ b/roles/apps/templates/bwpoc-firewall.sh.j2 @@ -0,0 +1,18 @@ +#!/bin/sh +# {{ ansible_managed }} +# N'autorise que le WAF (et les sources listées) à joindre les ports publiés +# des applis. Les règles vont dans DOCKER-USER, consultée par Docker avant +# ses propres règles ; --ctorigdstport filtre sur le port publié (avant DNAT). +set -eu +IFACE="{{ ansible_default_ipv4.interface }}" +PORTS="{{ apps_ports.values() | min }}:{{ apps_ports.values() | max }}" + +iptables -N BWPOC 2>/dev/null || iptables -F BWPOC +{% for src in [waf_ip] + apps_direct_access_sources %} +iptables -A BWPOC -s {{ src }} -j RETURN +{% endfor %} +iptables -A BWPOC -j DROP + +RULE="-i $IFACE -p tcp -m conntrack --ctstate NEW --ctorigdstport $PORTS -j BWPOC" +# shellcheck disable=SC2086 +iptables -C DOCKER-USER $RULE 2>/dev/null || iptables -I DOCKER-USER $RULE diff --git a/roles/apps/templates/compose.yml.j2 b/roles/apps/templates/compose.yml.j2 new file mode 100644 index 0000000..bba1e35 --- /dev/null +++ b/roles/apps/templates/compose.yml.j2 @@ -0,0 +1,56 @@ +# {{ ansible_managed }} +# Sites de test du PoC BunkerWeb, mode {{ poc_mode }} +{% if poc_mode == 'split' %} +# Ports publiés sur {{ ansible_host }}, filtrés par la chaîne iptables BWPOC : +# seul le WAF ({{ waf_ip }}) peut les joindre. +{% else %} +# Aucun port publié : BunkerWeb joint les conteneurs par le réseau bw-services. +{% endif %} + +services: + www: + image: {{ nginx_image }} + container_name: poc-www + restart: unless-stopped + read_only: true + tmpfs: + - /var/cache/nginx + - /run + - /tmp + volumes: + - ./www:/usr/share/nginx/html:ro +{% if poc_mode == 'split' %} + ports: + - "{{ ansible_host }}:{{ apps_ports.www }}:80" +{% endif %} + networks: [apps] + + shop: + image: {{ juiceshop_image }} + container_name: poc-shop + restart: unless-stopped +{% if poc_mode == 'split' %} + ports: + - "{{ ansible_host }}:{{ apps_ports.shop }}:3000" +{% endif %} + networks: [apps] + + api: + image: {{ httpbin_image }} + container_name: poc-api + restart: unless-stopped + read_only: true +{% if poc_mode == 'split' %} + ports: + - "{{ ansible_host }}:{{ apps_ports.api }}:8080" +{% endif %} + networks: [apps] + +networks: + apps: +{% if poc_mode == 'single' %} + name: bw-services + external: true +{% else %} + name: poc-apps +{% endif %} diff --git a/roles/apps/templates/www/contact.html.j2 b/roles/apps/templates/www/contact.html.j2 new file mode 100644 index 0000000..ae19de5 --- /dev/null +++ b/roles/apps/templates/www/contact.html.j2 @@ -0,0 +1,26 @@ + + + + + + PoC BunkerWeb - contact + + + +

Contact

+

Formulaire factice envoyé en GET : la vitrine est statique, c'est BunkerWeb qui + inspecte les paramètres. Essayez un message contenant + <script>alert(1)</script> ou ' OR 1=1 --.

+
+ + + + + +
+

Retour

+ + diff --git a/roles/apps/templates/www/index.html.j2 b/roles/apps/templates/www/index.html.j2 new file mode 100644 index 0000000..bfc09a4 --- /dev/null +++ b/roles/apps/templates/www/index.html.j2 @@ -0,0 +1,36 @@ + + + + + + PoC BunkerWeb - vitrine + + + +

PoC BunkerWeb

+

Vous voyez cette page : vous avez passé le challenge antibot JavaScript + de BunkerWeb. Un curl sur cette même URL reste bloqué sur /challenge.

+ +

Sites du PoC

+ + + + + + +
SiteApplicationFonctionnalités mises en avant
wwwnginx statiqueantibot, en-têtes de sécurité, HTTPS forcé
shopOWASP Juice ShopModSecurity + OWASP CRS 4, websockets, User-Agents malveillants
apigo-httpbinlimitation de débit, méthodes, taille max, bad behavior, règle ModSecurity maison
bwuiUI BunkerWebrapports, bannissements, réglages
+ +

Essais rapides dans le navigateur

+ +

Servi par {{ nginx_image }} derrière BunkerWeb {{ bunkerweb_version }}.

+ + diff --git a/roles/bunkerweb/defaults/main.yml b/roles/bunkerweb/defaults/main.yml new file mode 100644 index 0000000..c455c3a --- /dev/null +++ b/roles/bunkerweb/defaults/main.yml @@ -0,0 +1,2 @@ +--- +bw_dir: "{{ poc_base_dir }}/bunkerweb" diff --git a/roles/bunkerweb/tasks/main.yml b/roles/bunkerweb/tasks/main.yml new file mode 100644 index 0000000..d44284d --- /dev/null +++ b/roles/bunkerweb/tasks/main.yml @@ -0,0 +1,64 @@ +--- +- name: Créer le répertoire de la stack BunkerWeb + ansible.builtin.file: + path: "{{ bw_dir }}" + state: directory + owner: root + group: root + mode: "0750" + +- name: Créer le réseau partagé bw-services + ansible.builtin.shell: docker network inspect bw-services >/dev/null 2>&1 || { docker network create bw-services && echo created; } + register: bw_net + changed_when: "'created' in bw_net.stdout" + +- name: Générer le compose BunkerWeb + ansible.builtin.template: + src: compose.yml.j2 + dest: "{{ bw_dir }}/compose.yml" + owner: root + group: root + mode: "0600" # contient les secrets + validate: docker compose -f %s config -q + +- name: Démarrer la stack BunkerWeb + community.docker.docker_compose_v2: + project_src: "{{ bw_dir }}" + project_name: bunkerweb + pull: missing + remove_orphans: true + state: present + register: bw_compose + +- name: Attendre que BunkerWeb réponde sur 443 + ansible.builtin.wait_for: + host: "{{ ansible_host }}" + port: 443 + timeout: 300 + +- name: Attendre que le site api réponde au travers du WAF + ansible.builtin.uri: + url: "https://{{ ansible_host }}/get" + headers: + Host: "api.{{ poc_domain }}" + validate_certs: false + status_code: 200 + register: bw_probe + until: bw_probe.status == 200 + retries: 30 + delay: 10 + +- name: Écrire le récapitulatif des accès côté contrôleur (credentials/ACCES.txt) + ansible.builtin.copy: + dest: "{{ playbook_dir }}/../credentials/ACCES.txt" + mode: "0600" + content: | + UI BunkerWeb : https://bwui.{{ poc_domain }}/ (WAF {{ ansible_host }}) + Identifiant : {{ bw_ui_admin_user }} + Mot de passe : {{ bw_ui_admin_password }} + delegate_to: localhost + become: false + +- name: Rappel des accès + ansible.builtin.debug: + msg: "UI BunkerWeb : https://bwui.{{ poc_domain }}/ , identifiants dans credentials/ACCES.txt" diff --git a/roles/bunkerweb/templates/compose.yml.j2 b/roles/bunkerweb/templates/compose.yml.j2 new file mode 100644 index 0000000..10a84e1 --- /dev/null +++ b/roles/bunkerweb/templates/compose.yml.j2 @@ -0,0 +1,133 @@ +# {{ ansible_managed }} +# Stack BunkerWeb {{ bunkerweb_version }} : WAF + scheduler + UI + MariaDB + Redis +# Mode {{ poc_mode }}{% if poc_mode == 'split' %}, applis sur {{ apps_ip }}{% endif %} + +{% macro upstream(name, site) -%} +{%- if site.upstream is match('^https?://') -%} +{{ site.upstream }} +{%- elif poc_mode == 'single' -%} +http://poc-{{ site.upstream }}:{{ apps_internal_ports[site.upstream] }} +{%- else -%} +http://{{ apps_ip }}:{{ apps_ports[site.upstream] }} +{%- endif -%} +{%- endmacro %} +{% macro val(v) %}{{ (v | string | replace('$', '$$')) | to_json }}{% endmacro %} +x-db-env: &db-env + DATABASE_URI: {{ val('mariadb+pymysql://bunkerweb:' ~ bw_db_password ~ '@bw-db:3306/db') }} + +x-api-env: &api-env + API_WHITELIST_IP: "127.0.0.0/8 10.20.30.0/24" + +services: + bunkerweb: + image: bunkerity/bunkerweb:{{ bunkerweb_version }} + container_name: bunkerweb + restart: unless-stopped + ports: + - "80:8080/tcp" + - "443:8443/tcp" + - "443:8443/udp" # HTTP/3 (QUIC) + labels: + - "bunkerweb.INSTANCE=yes" + environment: + <<: [*db-env, *api-env] + networks: + - bw-universe + - bw-services + + bw-scheduler: + image: bunkerity/bunkerweb-scheduler:{{ bunkerweb_version }} + container_name: bw-scheduler + restart: unless-stopped + depends_on: + - bunkerweb + - bw-db + - bw-redis + volumes: + - bw-storage:/data + environment: + <<: [*db-env, *api-env] + BUNKERWEB_INSTANCES: "bunkerweb" + SERVER_NAME: {{ val(bw_sites.keys() | map('regex_replace', '$', '.' ~ poc_domain) | join(' ')) }} + # --- Réglages globaux --- +{% for k, v in bw_global_settings.items() %} + {{ k }}: {{ val(v) }} +{% endfor %} +{% for name, site in bw_sites.items() %} +{% set fqdn = name ~ '.' ~ poc_domain %} + # --- {{ fqdn }} --- + {{ fqdn }}_USE_REVERSE_PROXY: "yes" + {{ fqdn }}_REVERSE_PROXY_URL: "/" + {{ fqdn }}_REVERSE_PROXY_HOST: {{ val(upstream(name, site)) }} + {{ fqdn }}_SELF_SIGNED_SSL_SUBJ: {{ val('/CN=' ~ fqdn ~ '/') }} +{% for k, v in (site.settings | default({})).items() %} + {{ fqdn }}_{{ k }}: {{ val(v) }} +{% endfor %} +{% for k, v in (site.custom_confs | default({})).items() %} + {{ fqdn }}_CUSTOM_CONF_{{ k }}: {{ val(v) }} +{% endfor %} +{% endfor %} + networks: + - bw-universe + - bw-db + + bw-ui: + image: bunkerity/bunkerweb-ui:{{ bunkerweb_version }} + container_name: bw-ui + restart: unless-stopped + depends_on: + - bw-db + volumes: + - bw-ui-data:/data # secrets Flask, clés TOTP et Biscuit + environment: + <<: *db-env + ADMIN_USERNAME: {{ val(bw_ui_admin_user) }} + ADMIN_PASSWORD: {{ val(bw_ui_admin_password) }} + TZ: "Europe/Paris" + networks: + - bw-universe + - bw-db + + bw-db: + image: {{ mariadb_image }} + container_name: bw-db + restart: unless-stopped + command: --max-allowed-packet=67108864 + environment: + MYSQL_RANDOM_ROOT_PASSWORD: "yes" + MYSQL_DATABASE: "db" + MYSQL_USER: "bunkerweb" + MYSQL_PASSWORD: {{ val(bw_db_password) }} + volumes: + - bw-data:/var/lib/mysql + networks: + - bw-db + + bw-redis: + image: {{ redis_image }} + container_name: bw-redis + restart: unless-stopped + command: ["redis-server", "--requirepass", {{ val(bw_redis_password) }}, "--save", "60", "1"] + volumes: + - bw-redis:/data + networks: + - bw-universe + +volumes: + bw-data: + bw-storage: + bw-ui-data: + bw-redis: + +networks: + bw-universe: + name: bw-universe + ipam: + driver: default + config: + - subnet: 10.20.30.0/24 # seul réseau autorisé à parler à l'API BunkerWeb + bw-services: + name: bw-services # réseau des applis (mode single), créé par Ansible + external: true + bw-db: + name: bw-db diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml new file mode 100644 index 0000000..95aff39 --- /dev/null +++ b/roles/client/tasks/main.yml @@ -0,0 +1,44 @@ +--- +- name: Installer les outils de test + ansible.builtin.apt: + name: + - curl + - jq + - sqlmap + - nmap + - apache2-utils # ab + - dnsutils + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Créer le répertoire des tests + ansible.builtin.file: + path: "{{ poc_base_dir }}/tests" + state: directory + mode: "0755" + +- name: Déposer le script de tests + ansible.builtin.copy: + src: "{{ playbook_dir }}/../tests/bwpoc-tests.sh" + dest: "{{ poc_base_dir }}/tests/bwpoc-tests.sh" + mode: "0755" + +- name: Déposer la configuration des tests + ansible.builtin.copy: + dest: "{{ poc_base_dir }}/tests/bwpoc-tests.env" + mode: "0644" + content: | + # Généré par Ansible + WAF_IP={{ waf_ip }} + DOMAIN={{ poc_domain }} + APPS_IP={{ apps_ip }} + PORT_WWW={{ apps_ports.www }} + PORT_SHOP={{ apps_ports.shop }} + PORT_API={{ apps_ports.api }} + +- name: Lien pratique dans le PATH + ansible.builtin.file: + src: "{{ poc_base_dir }}/tests/bwpoc-tests.sh" + dest: /usr/local/bin/bwpoc-tests + state: link diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..a5816f6 --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: Attendre la fin de cloud-init + ansible.builtin.command: cloud-init status --wait + changed_when: false + failed_when: false + +- name: Installer les paquets de base + ansible.builtin.apt: + name: + - qemu-guest-agent + - ca-certificates + - curl + - gnupg + - jq + - python3-yaml + - python3-debian # requis par deb822_repository + - chrony + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Activer l'agent QEMU + ansible.builtin.systemd_service: + name: qemu-guest-agent + state: started + enabled: true + failed_when: false # inactif hors Proxmox + +- name: Fuseau horaire Europe/Paris + ansible.builtin.file: + src: /usr/share/zoneinfo/Europe/Paris + dest: /etc/localtime + state: link + force: true + +- name: Résolution locale des sites du PoC (vers le WAF) + ansible.builtin.blockinfile: + path: /etc/hosts + marker: "# {mark} PoC BunkerWeb" + block: | + {{ waf_ip }} www.{{ poc_domain }} shop.{{ poc_domain }} api.{{ poc_domain }} bwui.{{ poc_domain }} + +- name: Vérifier la présence du modèle hosts de cloud-init + ansible.builtin.stat: + path: /etc/cloud/templates/hosts.debian.tmpl + register: ci_hosts_tmpl + +- name: Persister la résolution si cloud-init gère /etc/hosts + ansible.builtin.blockinfile: + path: /etc/cloud/templates/hosts.debian.tmpl + marker: "# {mark} PoC BunkerWeb" + block: | + {{ waf_ip }} www.{{ poc_domain }} shop.{{ poc_domain }} api.{{ poc_domain }} bwui.{{ poc_domain }} + when: ci_hosts_tmpl.stat.exists diff --git a/roles/docker/handlers/main.yml b/roles/docker/handlers/main.yml new file mode 100644 index 0000000..29644e5 --- /dev/null +++ b/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Redémarrer Docker + ansible.builtin.systemd_service: + name: docker + state: restarted diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..e70bf56 --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,51 @@ +--- +# Installation de Docker CE depuis le dépôt officiel (Debian 13 trixie) +- name: Retirer les paquets Docker de la distribution + ansible.builtin.apt: + name: [docker.io, docker-compose, docker-doc, podman-docker, containerd, runc] + state: absent + +- name: Déclarer le dépôt Docker + ansible.builtin.deb822_repository: + name: docker + types: [deb] + uris: https://download.docker.com/linux/debian + suites: ["{{ ansible_distribution_release }}"] + components: [stable] + architectures: [amd64] + signed_by: https://download.docker.com/linux/debian/gpg + state: present + +- name: Installer Docker CE et le plugin compose + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: true + +- name: Limiter la taille des journaux des conteneurs + ansible.builtin.copy: + dest: /etc/docker/daemon.json + mode: "0644" + content: | + { + "log-driver": "json-file", + "log-opts": { "max-size": "20m", "max-file": "3" } + } + notify: Redémarrer Docker + +- name: Démarrer Docker + ansible.builtin.systemd_service: + name: docker + state: started + enabled: true + +- name: Ajouter l'utilisateur au groupe docker + ansible.builtin.user: + name: "{{ poc_vm_user }}" + groups: docker + append: true diff --git a/roles/proxmox_template/tasks/main.yml b/roles/proxmox_template/tasks/main.yml new file mode 100644 index 0000000..63d6401 --- /dev/null +++ b/roles/proxmox_template/tasks/main.yml @@ -0,0 +1,45 @@ +--- +# Modèle Debian 13 cloud-init, créé une seule fois sur le nœud Proxmox. +- name: Le modèle existe-t-il déjà ? + ansible.builtin.command: qm status {{ pve_template_vmid }} + register: tpl_status + changed_when: false + failed_when: false + +- name: Créer le modèle + when: tpl_status.rc != 0 + block: + - name: Télécharger l'image Debian genericcloud + ansible.builtin.get_url: + url: "{{ pve_image_url }}" + dest: "{{ pve_image_path }}" + mode: "0644" + + - name: Créer la VM modèle + ansible.builtin.command: >- + qm create {{ pve_template_vmid }} + --name {{ pve_template_name }} + --ostype l26 --cpu host --cores 2 --memory 2048 + --machine q35 + --scsihw virtio-scsi-single + --net0 virtio,bridge={{ pve_bridge }}{{ (',tag=' ~ pve_vlan_tag) if pve_vlan_tag | string | length > 0 else '' }} + --serial0 socket --vga serial0 + --agent enabled=1 + changed_when: true + + - name: Importer le disque système + ansible.builtin.command: >- + qm set {{ pve_template_vmid }} + --scsi0 {{ pve_storage }}:0,import-from={{ pve_image_path }},discard=on,iothread=1,ssd=1 + changed_when: true + + - name: Ajouter le lecteur cloud-init et l'ordre de démarrage + ansible.builtin.command: >- + qm set {{ pve_template_vmid }} + --ide2 {{ pve_storage }}:cloudinit + --boot order=scsi0 + changed_when: true + + - name: Convertir en modèle + ansible.builtin.command: qm template {{ pve_template_vmid }} + changed_when: true diff --git a/roles/proxmox_vm/tasks/main.yml b/roles/proxmox_vm/tasks/main.yml new file mode 100644 index 0000000..da52a33 --- /dev/null +++ b/roles/proxmox_vm/tasks/main.yml @@ -0,0 +1,13 @@ +--- +# Clone et configure les VM du PoC à partir du modèle (exécuté sur le nœud Proxmox). +- name: Déposer la clé SSH publique pour cloud-init + ansible.builtin.copy: + content: "{{ lookup('ansible.builtin.file', poc_ssh_pubkey_file) }}\n" + dest: /root/bwpoc_authorized_keys + mode: "0600" + +- name: Créer chaque VM + ansible.builtin.include_tasks: vm.yml + loop: "{{ groups['poc_vms'] | unique }}" + loop_control: + loop_var: vm_name diff --git a/roles/proxmox_vm/tasks/vm.yml b/roles/proxmox_vm/tasks/vm.yml new file mode 100644 index 0000000..b40c0bb --- /dev/null +++ b/roles/proxmox_vm/tasks/vm.yml @@ -0,0 +1,56 @@ +--- +- name: "{{ vm_name }} : lecture des paramètres" + ansible.builtin.set_fact: + vm: "{{ hostvars[vm_name] }}" + +- name: "{{ vm_name }} : la VM existe-t-elle ?" + ansible.builtin.command: qm status {{ vm.vm_id }} + register: vm_status + changed_when: false + failed_when: false + +- name: "{{ vm_name }} : création" + when: vm_status.rc != 0 + block: + - name: "{{ vm_name }} : l'IP {{ vm.ansible_host }} doit être libre" + ansible.builtin.command: ping -c 2 -W 1 {{ vm.ansible_host }} + register: ip_ping + changed_when: false + failed_when: ip_ping.rc == 0 + + - name: "{{ vm_name }} : clonage du modèle" + ansible.builtin.command: >- + qm clone {{ pve_template_vmid }} {{ vm.vm_id }} + --name {{ vm_name }} --full 1 --storage {{ pve_storage }} + changed_when: true + + - name: "{{ vm_name }} : ressources et cloud-init" + ansible.builtin.command: >- + qm set {{ vm.vm_id }} + --cores {{ vm.vm_cores }} --memory {{ vm.vm_memory }} + --onboot 1 --tags bwpoc + --ciuser {{ poc_vm_user }} + --sshkeys /root/bwpoc_authorized_keys + --ipconfig0 ip={{ vm.ansible_host }}/{{ poc_net_prefix }},gw={{ poc_net_gateway }} + --nameserver "{{ poc_net_dns | join(' ') }}" + --searchdomain {{ poc_net_searchdomain }} + --ciupgrade 0 + changed_when: true + + - name: "{{ vm_name }} : agrandissement du disque à {{ vm.vm_disk_gb }} Go" + ansible.builtin.command: qm resize {{ vm.vm_id }} scsi0 {{ vm.vm_disk_gb }}G + changed_when: true + +- name: "{{ vm_name }} : démarrage" + ansible.builtin.command: qm start {{ vm.vm_id }} + register: vm_start + changed_when: vm_start.rc == 0 + failed_when: vm_start.rc != 0 and 'already running' not in vm_start.stderr + +- name: "{{ vm_name }} : attente du SSH" + ansible.builtin.wait_for: + host: "{{ vm.ansible_host }}" + port: 22 + timeout: 300 + delegate_to: localhost + become: false diff --git a/tests/bwpoc-tests.sh b/tests/bwpoc-tests.sh new file mode 100755 index 0000000..9c6459d --- /dev/null +++ b/tests/bwpoc-tests.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# ===================================================================== +# Tests fonctionnels du PoC BunkerWeb +# Lance une série de requêtes légitimes et malveillantes au travers du +# WAF et compare le code HTTP obtenu au comportement attendu. +# +# Usage : bwpoc-tests.sh [--direct] [--only ] +# --direct rejoue les attaques directement sur les applis, sans WAF +# (nécessite apps_direct_access_sources, voir README) +# --only ne lance que les tests dont le titre contient +# +# Variables (fichier bwpoc-tests.env à côté du script, ou environnement) : +# WAF_IP, DOMAIN, APPS_IP, PORT_WWW, PORT_SHOP, PORT_API +# +# ATTENTION : les derniers tests provoquent volontairement le +# bannissement de votre IP sur api. (BAD_BEHAVIOR_BAN_TIME). +# ===================================================================== +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" +# shellcheck source=/dev/null +[ -f "$SCRIPT_DIR/bwpoc-tests.env" ] && . "$SCRIPT_DIR/bwpoc-tests.env" + +WAF_IP="${WAF_IP:-192.168.200.61}" +DOMAIN="${DOMAIN:-bwpoc.internal}" +APPS_IP="${APPS_IP:-192.168.200.62}" +PORT_SHOP="${PORT_SHOP:-8082}" +PORT_API="${PORT_API:-8083}" + +DIRECT=0 +ONLY="" +while [ $# -gt 0 ]; do + case "$1" in + --direct) DIRECT=1 ;; + --only) ONLY="$2"; shift ;; + -h|--help) sed -n '2,18p' "$0"; exit 0 ;; + *) echo "Option inconnue : $1" >&2; exit 2 ;; + esac + shift +done + +if [ -t 1 ]; then G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; B=$'\e[1m'; N=$'\e[0m'; else G=; R=; Y=; B=; N=; fi +PASS=0; FAIL=0; INFO=0 +UA="Mozilla/5.0 (X11; Linux x86_64) bwpoc-tests" + +# Requête au travers du WAF : --resolve évite tout besoin de DNS. +# Affiche " ". +waf() { + local host="$1" path="$2"; shift 2 + curl -sk -o /dev/null -w '%{http_code} %{redirect_url}' --max-time 10 \ + --resolve "$host.$DOMAIN:443:$WAF_IP" --resolve "$host.$DOMAIN:80:$WAF_IP" \ + -A "$UA" "$@" "https://$host.$DOMAIN$path" +} + +# Vérifie qu'un code fait partie de la liste attendue (séparateur |) +check() { + local title="$1" expected="$2" got="$3" level="${4:-strict}" + if [ -n "$ONLY" ] && [[ "$title" != *"$ONLY"* ]]; then return; fi + local code="${got%% *}" + if [[ "|$expected|" == *"|$code|"* ]]; then + printf ' %s[OK]%s %-58s %s\n' "$G" "$N" "$title" "$code"; PASS=$((PASS + 1)) + elif [ "$level" = info ]; then + printf ' %s[INFO]%s %-58s %s (attendu %s)\n' "$Y" "$N" "$title" "$code" "$expected"; INFO=$((INFO + 1)) + else + printf ' %s[KO]%s %-58s %s (attendu %s)\n' "$R" "$N" "$title" "$code" "$expected"; FAIL=$((FAIL + 1)) + fi +} + +section() { printf '\n%s== %s ==%s\n' "$B" "$1" "$N"; } + +# --------------------------------------------------------------------- +if [ "$DIRECT" -eq 1 ]; then + section "Sans WAF : attaques envoyées directement à $APPS_IP" + for t in \ + "SQLi Juice Shop (recherche produits)|http://$APPS_IP:$PORT_SHOP/rest/products/search?q=%27))%20OR%201%3D1--" \ + "XSS réfléchie|http://$APPS_IP:$PORT_SHOP/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E" \ + "Méthode PUT sur l'API|http://$APPS_IP:$PORT_API/put"; do + title="${t%%|*}"; url="${t#*|}" + if [[ "$title" == *PUT* ]]; then + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 -X PUT "$url") + else + code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url") + fi + printf ' %-60s %s\n' "$title" "$code" + done + echo + echo "Code 000 = port filtré (seul le WAF est autorisé). Sinon, comparez avec le passage par le WAF." + exit 0 +fi + +echo "${B}PoC BunkerWeb${N} : WAF $WAF_IP, domaine $DOMAIN" + +section "Transport et socle" +check "HTTP redirigé vers HTTPS (api)" "301|302|307|308" \ + "$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 --resolve "api.$DOMAIN:80:$WAF_IP" "http://api.$DOMAIN/get")" +check "Host inconnu refusé (DISABLE_DEFAULT_SERVER)" "000|400|403|404|444" \ + "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 --resolve "inconnu.$DOMAIN:443:$WAF_IP" "https://inconnu.$DOMAIN/")" +check "Requête légitime sur l'API" "200" "$(waf api /get)" + +section "En-têtes et compression" +HDRS=$(curl -sk -D - -o /dev/null --max-time 10 --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/get" | tr -d '\r' | tr 'A-Z' 'a-z') +for h in strict-transport-security x-frame-options x-content-type-options content-security-policy referrer-policy; do + if grep -q "^$h:" <<<"$HDRS"; then check "En-tête $h ajouté" "yes" "yes"; else check "En-tête $h ajouté" "yes" "no"; fi +done +if grep -q "^server:" <<<"$HDRS"; then check "En-tête Server masqué" "masqué" "présent" info; else check "En-tête Server masqué" "masqué" "masqué"; fi +XFF=$(curl -sk --max-time 10 --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/headers" | jq -r '.headers["X-Forwarded-For"][0] // empty' 2>/dev/null) +if [ -n "$XFF" ]; then check "Le backend reçoit X-Forwarded-For ($XFF)" "yes" "yes"; else check "Le backend reçoit X-Forwarded-For" "yes" "no"; fi +for enc in br gzip; do + CE=$(curl -sk -D - -o /dev/null --max-time 10 -H "Accept-Encoding: $enc" --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/html" \ + | tr -d '\r' | awk -F': ' 'tolower($1)=="content-encoding"{print $2}') + check "Compression $enc" "$enc" "${CE:-aucune}" +done + +section "Antibot (www)" +RES=$(waf www /) +if [[ "$RES" == *challenge* ]] || [[ "${RES%% *}" == 200 && $(curl -sk --max-time 10 --resolve "www.$DOMAIN:443:$WAF_IP" "https://www.$DOMAIN/" | grep -ci challenge) -gt 0 ]]; then + check "Un client sans JavaScript reste bloqué sur /challenge" "yes" "yes" +else + check "Un client sans JavaScript reste bloqué sur /challenge" "yes" "no ($RES)" +fi + +section "WAF ModSecurity + OWASP CRS 4 (shop)" +check "Page d'accueil Juice Shop" "200" "$(waf shop /)" +check "SQLi : recherche produits ')) OR 1=1--" "403" "$(waf shop "/rest/products/search?q=%27))%20OR%201%3D1--")" +check "XSS : " "403" "$(waf shop "/?q=%3Cscript%3Ealert(1)%3C%2Fscript%3E")" +check "Traversée de répertoire ../../etc/passwd" "403" "$(waf shop "/?file=..%2F..%2F..%2F..%2Fetc%2Fpasswd")" +check "Injection de commande ;cat /etc/passwd" "403" "$(waf api "/get?cmd=%3Bcat%20%2Fetc%2Fpasswd")" +check "Règle ModSecurity maison ?poc=attack" "403" "$(waf api "/get?poc=attack")" + +section "Listes noires" +check "User-Agent maison PoC-EvilBot (BLACKLIST_USER_AGENT)" "403" "$(waf shop / -A "PoC-EvilBot/1.0")" +check "User-Agent sqlmap (liste communautaire)" "403" "$(waf shop / -A "sqlmap/1.8#stable (https://sqlmap.org)")" info + +section "Contraintes protocolaires (api)" +check "Méthode PUT interdite (ALLOWED_METHODS)" "405" "$(waf api /put -X PUT -d x=1)" +check "Corps de 2 Mo refusé (MAX_CLIENT_SIZE=1m)" "413" \ + "$(head -c 2097152 /dev/zero | tr '\0' 'a' | curl -sk -o /dev/null -w '%{http_code}' --max-time 20 \ + --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" -H 'Content-Type: text/plain' --data-binary @- "https://api.$DOMAIN/post")" + +section "Limitation de débit puis bad behavior (api)" +echo " Envoi de 40 requêtes en parallèle (LIMIT_REQ_RATE=5r/s)..." +CODES=$(seq 1 40 | xargs -P 20 -I{} curl -sk -o /dev/null -w '%{http_code}\n' --max-time 10 \ + --resolve "api.$DOMAIN:443:$WAF_IP" -A "$UA" "https://api.$DOMAIN/get?n={}" | sort | uniq -c | tr '\n' ' ') +echo " Répartition des codes : $CODES" +if grep -qE '\b429\b' <<<"$CODES"; then check "Des requêtes ont été limitées (429)" "yes" "yes"; else check "Des requêtes ont été limitées (429)" "yes" "no"; fi +echo " Envoi de 12 requêtes en erreur 404 (BAD_BEHAVIOR_THRESHOLD=10)..." +for i in $(seq 1 12); do waf api "/status/404?i=$i" >/dev/null; done +sleep 2 +check "IP bannie : requête légitime désormais refusée" "403" "$(waf api /get)" +check "Bannissement limité au service api (shop répond)" "200" "$(waf shop /)" + +# --------------------------------------------------------------------- +printf '\n%sBilan%s : %s%d OK%s, %s%d KO%s, %d info\n' "$B" "$N" "$G" "$PASS" "$N" "$R" "$FAIL" "$N" "$INFO" +MYIP=$(curl -sk --max-time 5 --resolve "shop.$DOMAIN:443:$WAF_IP" -A "$UA" -o /dev/null -w '%{local_ip}' "https://shop.$DOMAIN/") +cat <