From f0f61548d207780fa500e6af3be13ce460cbcee3 Mon Sep 17 00:00:00 2001 From: Cedric Date: Tue, 27 Oct 2026 16:40:08 +0100 Subject: [PATCH] =?UTF-8?q?Doc=20et=20exemples=20=C3=A0=20jour=20:=20publi?= =?UTF-8?q?cation=20en=20*-bwpoc.vlab.bzh=20derri=C3=A8re=20Traefik,=20mod?= =?UTF-8?q?=C3=A8le=209100=20avec=20agent=20QEMU?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README : noms des sites, accès via Traefik, configuration retenue sur vLab, variante BunkerWeb en frontal avec Let's Encrypt, prérequis du modèle - examples/split régénérés depuis la configuration actuelle (secrets en CHANGEME) - schéma : libellé des sites en *-bwpoc.vlab.bzh Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 48 ++++++++++++------ docs/architecture.excalidraw | 4 +- examples/split/apps.compose.yml | 10 ++-- examples/split/bunkerweb.compose.yml | 74 ++++++++++++++++------------ examples/split/bwpoc-firewall.sh | 2 +- 5 files changed, 82 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 1fdcabd..0607dd3 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,17 @@ 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 | +| `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. +Le nom complet d'un site est ``. Sur le labo vLab, le PoC est publié derrière le Traefik de `*.vlab.bzh` : **`www-bwpoc.vlab.bzh`, `shop-bwpoc.vlab.bzh`, `api-bwpoc.vlab.bzh` et `bwui-bwpoc.vlab.bzh`** (un seul niveau sous le wildcard, Let's Encrypt géré par Traefik, voir [Exposer le PoC](#exposer-le-poc-sur-un-domaine-public)). Pour un labo fermé, `poc_domain: bwpoc.internal` et `poc_fqdn_suffix: ".bwpoc.internal"` (TLD réservé par l'ICANN pour l'usage privé) donnent `www.bwpoc.internal`... Sauf avec `poc_tls: letsencrypt`, BunkerWeb présente ses propres certificats auto-signés, site par site. ## Prérequis - Un nœud Proxmox VE 8 ou 9 (ici `lancelot`, 172.16.16.7) avec accès Internet (image Debian, images Docker, listes noires communautaires). -- Un accès SSH **root** au nœud, utilisé **une seule fois** pour créer le modèle de VM et le pool `bwpoc`. Tout le reste passe par l'API avec un compte dédié (voir plus bas). +- Un modèle cloud-init Debian 13 **avec qemu-guest-agent installé** et un machine-id vierge (sur lancelot : 9100 `debian13-bwpoc`, dérivé de 9001). À défaut, `pve_template_create: true` le fabrique depuis l'image officielle, ce qui demande un accès SSH root au nœud une seule fois. Tout le reste passe par l'API. - Un réseau de VM avec DHCP : ici le VLAN 200 "LAB", qui fournit IP, passerelle et DNS. Le poste Ansible doit pouvoir joindre ce VLAN en SSH. - Sur le poste Ansible : `ansible-core` 2.17 ou plus (exigé par `community.proxmox`), `proxmoxer` et `requests` en Python, une paire de clés SSH. @@ -55,7 +55,7 @@ Les mots de passe (MariaDB, Redis, admin de l'UI) sont générés au premier lan ## Compte Proxmox dédié -Le clonage et le pilotage des VM passent par l'API avec l'utilisateur `bwpoc@pve` et un jeton, limité à un pool `bwpoc`. Par défaut, le PoC clone un modèle cloud-init Debian 13 existant (`pve_template_vmid`, 9001 sur lancelot : VLAN 200, agent QEMU), donc aucun SSH root n'est nécessaire. Avec `pve_template_create: true`, le modèle 9100 est fabriqué en SSH root (import d'une image disque par chemin, réservé à `root@pam`), une fois. +Le clonage et le pilotage des VM passent par l'API avec l'utilisateur `bwpoc@pve` et un jeton, limité à un pool `bwpoc`. Par défaut, le PoC clone un modèle cloud-init existant (`pve_template_vmid`, 9100 `debian13-bwpoc` sur lancelot : le 9001 de base + qemu-guest-agent + `cloud-init clean --machine-id`), donc aucun SSH root n'est nécessaire. Sans agent QEMU dans le modèle, le playbook ne peut pas lire les IP DHCP. Avec `pve_template_create: true`, le modèle 9100 est fabriqué en SSH root (import d'une image disque par chemin, réservé à `root@pam`), une fois. Privilèges nécessaires, regroupés dans un rôle `BWPoC` : @@ -136,19 +136,19 @@ Comme l'IP du WAF sert au filtrage `DOCKER-USER` et aux `/etc/hosts`, **réserve ### 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 : +Sur vLab, les noms publics passent par Traefik : ouvrez `https://bwui-bwpoc.vlab.bzh/` (réservée aux IP privées, voir `poc_allowed_sources`). Identifiants : `make creds` (utilisateur `admin`). + +Les VM du PoC résolvent les noms vers le WAF directement (bloc dans `/etc/hosts`), ce qui permet aux tests de contourner Traefik. En labo fermé (`bwpoc.internal`), faites de même sur votre poste : ``` 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` : +`MULTISITE=yes` : chaque réglage peut être préfixé par le nom du site (`api-bwpoc.vlab.bzh_LIMIT_REQ_RATE=5r/s`). Dans Ansible, il suffit de remplir le dictionnaire `bw_sites` : ```yaml bw_sites: @@ -158,7 +158,7 @@ bw_sites: ALLOWED_METHODS: "GET|POST|HEAD" MAX_CLIENT_SIZE: "1m" LIMIT_REQ_RATE: "5r/s" - custom_confs: # devient api._CUSTOM_CONF_MODSEC_poc-rule + 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'" ``` @@ -223,15 +223,31 @@ La SQLi de Juice Shop renvoie alors la liste complète des produits (200) au lie ### 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` + `sqlmap -u "https://shop-bwpoc.vlab.bzh/rest/products/search?q=test" --random-agent --batch` (uniquement contre votre propre PoC) - **Antibot** dans le navigateur : passez `www` en `captcha` ou `cookie` dans `bunkerweb.yml`, relancez `make waf`. - **Faux positifs** : naviguez dans Juice Shop (connexion, panier, avis). Ce que le CRS bloque à tort apparaît dans l'UI (*Reports*) avec l'ID de règle ; on l'exclut par un `custom_confs` de type `MODSEC_CRS` (exemple : `SecRuleRemoveById 942100`). - **Mode détection seule** : `MODSECURITY_SEC_RULE_ENGINE: "DetectionOnly"` sur un site, pour observer avant de bloquer, comme on le ferait en production. - **Journaux** : `docker logs -f bunkerweb` sur `bw-waf`, et l'audit ModSecurity dans `/var/log/bunkerweb/modsec_audit.log` du conteneur. -## Exposer le PoC sur un domaine public (Let's Encrypt) +## Exposer le PoC sur un domaine public -Par défaut tout est en `bwpoc.internal` avec des certificats auto-signés. Pour un vrai domaine (exemple : `lab.naoned.net`) : +### Configuration retenue sur vLab : derrière Traefik, `*-bwpoc.vlab.bzh` + +`*.vlab.bzh` est un wildcard DNS qui pointe sur le Traefik du labo (172.16.16.21 côté LAN), qui gère seul Let's Encrypt. Un nom à deux niveaux (`www.bwpoc.vlab.bzh`) n'étant pas bien pris en charge par ce Traefik, les sites sont nommés avec un tiret. Réglages actuels de [`all.yml`](inventory/group_vars/all.yml) : + +```yaml +poc_domain: vlab.bzh +poc_fqdn_suffix: "-bwpoc.{{ poc_domain }}" # www-bwpoc.vlab.bzh, shop-bwpoc.vlab.bzh... +poc_tls: selfsigned # le TLS public est porté par Traefik +poc_trusted_proxies: ["172.16.16.21"] # IP réelle des clients via X-Forwarded-For +poc_allowed_sources: ["172.16.0.0/12", "192.168.0.0/16"] # UI réservée au LAN +``` + +Côté Traefik, un routeur par nom (ou un seul avec les 4 `Host`) vers `https://:443` avec `insecureSkipVerify`, voir l'exemple plus bas. Réservez le bail DHCP de bw-waf pour que cette IP ne bouge pas. + +### Variante : BunkerWeb en frontal avec Let's Encrypt + +Si le WAF reçoit directement 80/443 (exemple : `lab.naoned.net`) : 1. DNS : `www`, `shop`, `api` et `bwui` du domaine pointent sur l'IP publique NATée vers le WAF. 2. Pare-feu : 80 et 443 (TCP, plus 443/UDP pour HTTP/3) vers l'IP du WAF ; réservation DHCP sur la MAC du WAF pour que l'IP ne bouge pas. @@ -258,7 +274,7 @@ http: routers: bwpoc: rule: "Host(`www-bwpoc.vlab.bzh`) || Host(`shop-bwpoc.vlab.bzh`) || Host(`api-bwpoc.vlab.bzh`) || Host(`bwui-bwpoc.vlab.bzh`)" - entryPoints: [websecure] + entryPoints: [websecure] # sur vLab : extranetfree service: bwpoc tls: { certResolver: letsencrypt } services: diff --git a/docs/architecture.excalidraw b/docs/architecture.excalidraw index 0c0830e..df6f461 100644 --- a/docs/architecture.excalidraw +++ b/docs/architecture.excalidraw @@ -1774,13 +1774,13 @@ "updated": 1793096641458, "link": null, "locked": false, - "text": "Sites protégés (*.bwpoc.internal) et fonctionnalités testées", + "text": "Sites protégés (*-bwpoc.vlab.bzh) 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", + "originalText": "Sites protégés (*-bwpoc.vlab.bzh) et fonctionnalités testées", "autoResize": true, "lineHeight": 1.25 }, diff --git a/examples/split/apps.compose.yml b/examples/split/apps.compose.yml index cf9bc88..89e031b 100644 --- a/examples/split/apps.compose.yml +++ b/examples/split/apps.compose.yml @@ -1,7 +1,7 @@ # 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. +# Ports publiés sur 192.168.200.161, filtrés par la chaîne iptables BWPOC : +# seul le WAF (192.168.200.155) peut les joindre. services: www: @@ -16,7 +16,7 @@ services: volumes: - ./www:/usr/share/nginx/html:ro ports: - - "192.168.200.62:8081:80" + - "192.168.200.161:8081:80" networks: [apps] shop: @@ -24,7 +24,7 @@ services: container_name: poc-shop restart: unless-stopped ports: - - "192.168.200.62:8082:3000" + - "192.168.200.161:8082:3000" networks: [apps] api: @@ -33,7 +33,7 @@ services: restart: unless-stopped read_only: true ports: - - "192.168.200.62:8083:8080" + - "192.168.200.161:8083:8080" networks: [apps] networks: diff --git a/examples/split/bunkerweb.compose.yml b/examples/split/bunkerweb.compose.yml index 3163694..8b66415 100644 --- a/examples/split/bunkerweb.compose.yml +++ b/examples/split/bunkerweb.compose.yml @@ -1,6 +1,6 @@ # Ansible managed # Stack BunkerWeb 1.6.17 : WAF + scheduler + UI + MariaDB + Redis -# Mode split, applis sur 192.168.200.62 +# Mode split, applis sur 192.168.200.161 x-db-env: &db-env DATABASE_URI: "mariadb+pymysql://bunkerweb:CHANGEME@bw-db:3306/db" @@ -37,13 +37,21 @@ services: environment: <<: [*db-env, *api-env] BUNKERWEB_INSTANCES: "bunkerweb" - SERVER_NAME: "www.bwpoc.internal shop.bwpoc.internal api.bwpoc.internal bwui.bwpoc.internal" + SERVER_NAME: "www-bwpoc.vlab.bzh shop-bwpoc.vlab.bzh api-bwpoc.vlab.bzh bwui-bwpoc.vlab.bzh" # --- Réglages globaux --- MULTISITE: "yes" DISABLE_DEFAULT_SERVER: "yes" SERVE_FILES: "no" GENERATE_SELF_SIGNED_SSL: "yes" + AUTO_LETS_ENCRYPT: "no" + EMAIL_LETS_ENCRYPT: "" + LETS_ENCRYPT_CHALLENGE: "http" + USE_LETS_ENCRYPT_STAGING: "no" AUTO_REDIRECT_HTTP_TO_HTTPS: "yes" + USE_REAL_IP: "yes" + REAL_IP_FROM: "172.16.16.21" + REAL_IP_HEADER: "X-Forwarded-For" + REAL_IP_RECURSIVE: "yes" USE_MODSECURITY: "yes" USE_MODSECURITY_CRS: "yes" MODSECURITY_CRS_VERSION: "4" @@ -59,36 +67,38 @@ services: 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" + # --- www-bwpoc.vlab.bzh --- + www-bwpoc.vlab.bzh_USE_REVERSE_PROXY: "yes" + www-bwpoc.vlab.bzh_REVERSE_PROXY_URL: "/" + www-bwpoc.vlab.bzh_REVERSE_PROXY_HOST: "http://192.168.200.161:8081" + www-bwpoc.vlab.bzh_SELF_SIGNED_SSL_SUBJ: "/CN=www-bwpoc.vlab.bzh/" + www-bwpoc.vlab.bzh_USE_ANTIBOT: "javascript" + www-bwpoc.vlab.bzh_LIMIT_REQ_RATE: "10r/s" + # --- shop-bwpoc.vlab.bzh --- + shop-bwpoc.vlab.bzh_USE_REVERSE_PROXY: "yes" + shop-bwpoc.vlab.bzh_REVERSE_PROXY_URL: "/" + shop-bwpoc.vlab.bzh_REVERSE_PROXY_HOST: "http://192.168.200.161:8082" + shop-bwpoc.vlab.bzh_SELF_SIGNED_SSL_SUBJ: "/CN=shop-bwpoc.vlab.bzh/" + shop-bwpoc.vlab.bzh_REVERSE_PROXY_WS: "yes" + shop-bwpoc.vlab.bzh_LIMIT_REQ_RATE: "30r/s" + shop-bwpoc.vlab.bzh_COOKIE_FLAGS: "* SameSite=Lax" + # --- api-bwpoc.vlab.bzh --- + api-bwpoc.vlab.bzh_USE_REVERSE_PROXY: "yes" + api-bwpoc.vlab.bzh_REVERSE_PROXY_URL: "/" + api-bwpoc.vlab.bzh_REVERSE_PROXY_HOST: "http://192.168.200.161:8083" + api-bwpoc.vlab.bzh_SELF_SIGNED_SSL_SUBJ: "/CN=api-bwpoc.vlab.bzh/" + api-bwpoc.vlab.bzh_ALLOWED_METHODS: "GET|POST|HEAD" + api-bwpoc.vlab.bzh_MAX_CLIENT_SIZE: "1m" + api-bwpoc.vlab.bzh_LIMIT_REQ_RATE: "5r/s" + api-bwpoc.vlab.bzh_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.vlab.bzh --- + bwui-bwpoc.vlab.bzh_USE_REVERSE_PROXY: "yes" + bwui-bwpoc.vlab.bzh_REVERSE_PROXY_URL: "/" + bwui-bwpoc.vlab.bzh_REVERSE_PROXY_HOST: "http://bw-ui:7000" + bwui-bwpoc.vlab.bzh_SELF_SIGNED_SSL_SUBJ: "/CN=bwui-bwpoc.vlab.bzh/" + bwui-bwpoc.vlab.bzh_USE_TEMPLATE: "ui" + # Accès réservé (poc_allowed_sources + client de test), challenge ACME toujours permis + bwui-bwpoc.vlab.bzh_CUSTOM_CONF_MODSEC_poc-allowlist: "SecRule REMOTE_ADDR \"!@ipMatch 172.16.0.0/12,192.168.0.0/16,192.168.200.130\" \"id:10002,phase:1,deny,status:403,log,msg:'PoC BunkerWeb : source non autorisee',chain\"\nSecRule REQUEST_URI \"!@beginsWith /.well-known/acme-challenge/\" \"t:none\"" networks: - bw-universe - bw-db diff --git a/examples/split/bwpoc-firewall.sh b/examples/split/bwpoc-firewall.sh index c5f4206..72f9f46 100644 --- a/examples/split/bwpoc-firewall.sh +++ b/examples/split/bwpoc-firewall.sh @@ -8,7 +8,7 @@ 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 -s 192.168.200.155 -j RETURN iptables -A BWPOC -j DROP RULE="-i $IFACE -p tcp -m conntrack --ctstate NEW --ctorigdstport $PORTS -j BWPOC"