From e395fae4c6f43b07b2e7ebc63327555235235191 Mon Sep 17 00:00:00 2001 From: Cedric Date: Tue, 27 Oct 2026 15:59:35 +0100 Subject: [PATCH] Noms des sites configurables (poc_fqdn_suffix) : www-bwpoc.vlab.bzh, un seul niveau sous le wildcard Traefik Tests : 26 OK, 0 KO. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 3 ++- inventory/group_vars/all.yml | 5 +++- roles/apps/templates/www/index.html.j2 | 8 +++---- roles/bunkerweb/tasks/main.yml | 6 ++--- roles/bunkerweb/templates/compose.yml.j2 | 4 ++-- roles/client/tasks/main.yml | 1 + roles/common/tasks/main.yml | 4 ++-- tests/bwpoc-tests.sh | 29 ++++++++++++------------ 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 77492e8..1fdcabd 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,7 @@ Par défaut tout est en `bwpoc.internal` avec des certificats auto-signés. Pour ```yaml poc_domain: lab.naoned.net +poc_fqdn_suffix: ".lab.naoned.net" # ou "-bwpoc.vlab.bzh" pour www-bwpoc.vlab.bzh (un seul niveau sous un wildcard) poc_tls: letsencrypt poc_letsencrypt_email: vous@example.org poc_allowed_sources: ["203.0.113.10"] # IP autorisées sur les sites de poc_restricted_sites (bwui par défaut) @@ -256,7 +257,7 @@ Si 80/443 sont déjà pris par un Traefik, il termine le TLS public (ses propres http: routers: bwpoc: - rule: "Host(`www.bwpoc.vlab.bzh`) || Host(`shop.bwpoc.vlab.bzh`) || Host(`api.bwpoc.vlab.bzh`) || Host(`bwui.bwpoc.vlab.bzh`)" + rule: "Host(`www-bwpoc.vlab.bzh`) || Host(`shop-bwpoc.vlab.bzh`) || Host(`api-bwpoc.vlab.bzh`) || Host(`bwui-bwpoc.vlab.bzh`)" entryPoints: [websecure] service: bwpoc tls: { certResolver: letsencrypt } diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index 515af2a..e8b6369 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -7,7 +7,10 @@ # --- 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.vlab.bzh # A ADAPTER : bwpoc.internal pour un labo fermé (wildcard *.vlab.bzh -> Traefik) +poc_domain: vlab.bzh # A ADAPTER : bwpoc.internal pour un labo fermé +# Nom complet d'un site = . Cas général ".{{ poc_domain }}" (www.bwpoc.internal) ; +# ici "-bwpoc.vlab.bzh" (www-bwpoc.vlab.bzh), un seul niveau sous le wildcard *.vlab.bzh du Traefik. +poc_fqdn_suffix: "-bwpoc.{{ poc_domain }}" # --- TLS ---------------------------------------------------------------- # selfsigned : certificats auto-signés (labo fermé, domaine non public) diff --git a/roles/apps/templates/www/index.html.j2 b/roles/apps/templates/www/index.html.j2 index bfc09a4..5145dea 100644 --- a/roles/apps/templates/www/index.html.j2 +++ b/roles/apps/templates/www/index.html.j2 @@ -20,10 +20,10 @@

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
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

diff --git a/roles/bunkerweb/tasks/main.yml b/roles/bunkerweb/tasks/main.yml index d44284d..b6c0c6e 100644 --- a/roles/bunkerweb/tasks/main.yml +++ b/roles/bunkerweb/tasks/main.yml @@ -40,7 +40,7 @@ ansible.builtin.uri: url: "https://{{ ansible_host }}/get" headers: - Host: "api.{{ poc_domain }}" + Host: "api{{ poc_fqdn_suffix }}" validate_certs: false status_code: 200 register: bw_probe @@ -53,7 +53,7 @@ dest: "{{ playbook_dir }}/../credentials/ACCES.txt" mode: "0600" content: | - UI BunkerWeb : https://bwui.{{ poc_domain }}/ (WAF {{ ansible_host }}) + UI BunkerWeb : https://bwui{{ poc_fqdn_suffix }}/ (WAF {{ ansible_host }}) Identifiant : {{ bw_ui_admin_user }} Mot de passe : {{ bw_ui_admin_password }} delegate_to: localhost @@ -61,4 +61,4 @@ - name: Rappel des accès ansible.builtin.debug: - msg: "UI BunkerWeb : https://bwui.{{ poc_domain }}/ , identifiants dans credentials/ACCES.txt" + msg: "UI BunkerWeb : https://bwui{{ poc_fqdn_suffix }}/ , identifiants dans credentials/ACCES.txt" diff --git a/roles/bunkerweb/templates/compose.yml.j2 b/roles/bunkerweb/templates/compose.yml.j2 index eb70232..513e52f 100644 --- a/roles/bunkerweb/templates/compose.yml.j2 +++ b/roles/bunkerweb/templates/compose.yml.j2 @@ -48,13 +48,13 @@ services: environment: <<: [*db-env, *api-env] BUNKERWEB_INSTANCES: "bunkerweb" - SERVER_NAME: {{ val(bw_sites.keys() | map('regex_replace', '$', '.' ~ poc_domain) | join(' ')) }} + SERVER_NAME: {{ val(bw_sites.keys() | map('regex_replace', '$', poc_fqdn_suffix) | 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 %} +{% set fqdn = name ~ poc_fqdn_suffix %} # --- {{ fqdn }} --- {{ fqdn }}_USE_REVERSE_PROXY: "yes" {{ fqdn }}_REVERSE_PROXY_URL: "/" diff --git a/roles/client/tasks/main.yml b/roles/client/tasks/main.yml index 95aff39..2a02c32 100644 --- a/roles/client/tasks/main.yml +++ b/roles/client/tasks/main.yml @@ -32,6 +32,7 @@ # Généré par Ansible WAF_IP={{ waf_ip }} DOMAIN={{ poc_domain }} + SUFFIX={{ poc_fqdn_suffix }} APPS_IP={{ apps_ip }} PORT_WWW={{ apps_ports.www }} PORT_SHOP={{ apps_ports.shop }} diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index a5816f6..6c235d7 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -38,7 +38,7 @@ path: /etc/hosts marker: "# {mark} PoC BunkerWeb" block: | - {{ waf_ip }} www.{{ poc_domain }} shop.{{ poc_domain }} api.{{ poc_domain }} bwui.{{ poc_domain }} + {{ waf_ip }} www{{ poc_fqdn_suffix }} shop{{ poc_fqdn_suffix }} api{{ poc_fqdn_suffix }} bwui{{ poc_fqdn_suffix }} - name: Vérifier la présence du modèle hosts de cloud-init ansible.builtin.stat: @@ -50,5 +50,5 @@ 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 }} + {{ waf_ip }} www{{ poc_fqdn_suffix }} shop{{ poc_fqdn_suffix }} api{{ poc_fqdn_suffix }} bwui{{ poc_fqdn_suffix }} when: ci_hosts_tmpl.stat.exists diff --git a/tests/bwpoc-tests.sh b/tests/bwpoc-tests.sh index 8702a41..3f67be9 100755 --- a/tests/bwpoc-tests.sh +++ b/tests/bwpoc-tests.sh @@ -10,7 +10,7 @@ # --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 +# WAF_IP, DOMAIN, SUFFIX, 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). @@ -23,6 +23,7 @@ SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)" WAF_IP="${WAF_IP:?WAF_IP non défini (bwpoc-tests.env ou environnement)}" DOMAIN="${DOMAIN:-bwpoc.internal}" +SUFFIX="${SUFFIX:-.$DOMAIN}" # nom complet d'un site = $SUFFIX APPS_IP="${APPS_IP:-}" PORT_SHOP="${PORT_SHOP:-8082}" PORT_API="${PORT_API:-8083}" @@ -48,8 +49,8 @@ UA="Mozilla/5.0 (X11; Linux x86_64) bwpoc-tests" 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" + --resolve "$host$SUFFIX:443:$WAF_IP" --resolve "$host$SUFFIX:80:$WAF_IP" \ + -A "$UA" "$@" "https://$host$SUFFIX$path" } # Vérifie qu'un code fait partie de la liste attendue (séparateur |) @@ -88,32 +89,32 @@ if [ "$DIRECT" -eq 1 ]; then exit 0 fi -echo "${B}PoC BunkerWeb${N} : WAF $WAF_IP, domaine $DOMAIN" +echo "${B}PoC BunkerWeb${N} : WAF $WAF_IP, sites $SUFFIX" 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")" + "$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 --resolve "api$SUFFIX:80:$WAF_IP" "http://api$SUFFIX/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/")" + "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 10 --resolve "inconnu$SUFFIX:443:$WAF_IP" "https://inconnu$SUFFIX/")" 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') +HDRS=$(curl -sk -D - -o /dev/null --max-time 10 --resolve "api$SUFFIX:443:$WAF_IP" -A "$UA" "https://api$SUFFIX/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) +XFF=$(curl -sk --max-time 10 --resolve "api$SUFFIX:443:$WAF_IP" -A "$UA" "https://api$SUFFIX/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" \ + CE=$(curl -sk -D - -o /dev/null --max-time 10 -H "Accept-Encoding: $enc" --resolve "api$SUFFIX:443:$WAF_IP" -A "$UA" "https://api$SUFFIX/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 +if [[ "$RES" == *challenge* ]] || [[ "${RES%% *}" == 200 && $(curl -sk --max-time 10 --resolve "www$SUFFIX:443:$WAF_IP" "https://www$SUFFIX/" | 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)" @@ -135,12 +136,12 @@ 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")" + --resolve "api$SUFFIX:443:$WAF_IP" -A "$UA" -H 'Content-Type: text/plain' --data-binary @- "https://api$SUFFIX/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' ' ') + --resolve "api$SUFFIX:443:$WAF_IP" -A "$UA" "https://api$SUFFIX/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)..." @@ -151,10 +152,10 @@ 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/") +MYIP=$(curl -sk --max-time 5 --resolve "shop$SUFFIX:443:$WAF_IP" -A "$UA" -o /dev/null -w '%{local_ip}' "https://shop$SUFFIX/") cat <