feat(haproxy,torrent): declarative per-vhost waf_bypass + durable torrent exposure (ref #917)

haproxyctl: honour an opt-in `waf_bypass = true` field per vhost so the
generator emits `use_backend <backend>` (nginx_vhosts) instead of forcing
mitmproxy_inspector under waf_enabled. Makes the WAF-bypass of media/large-
upload services (nc/photoprism/matrix/torrent) declarative, auditable and
regen-durable instead of fragile hand-edits of the generated haproxy.cfg.
Normal vhosts are unaffected (verified in isolation).

secubox-torrent postinst: register torrent.<host>.<suffix> in haproxy.toml
(backend=nginx_vhosts, waf_bypass=true, ssl=true) idempotently and run a
drift-guarded `haproxyctl generate` — which refuses safely on a board that
still has un-migrated hand-edit vhosts (live cfg untouched) and succeeds on a
fresh board. Torrent bypasses sbxwaf by design, so no sbxwaf route is needed.

CLAUDE.md: WAF-bypass section updated — default stays 'never bypass'; the ONLY
sanctioned exception is the declarative waf_bypass=true in haproxy.toml, never
a hand-edit of the generated cfg.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-28 19:38:59 +02:00
parent 771b1da3fc
commit 940aa64fcf
4 changed files with 78 additions and 14 deletions

View File

@ -239,7 +239,8 @@ secubox-deb/
## 🔑 Règles impératives ## 🔑 Règles impératives
### Réseau / Sécurité ### Réseau / Sécurité
- **JAMAIS** de waf_bypass ni de port ouvert inutile - **JAMAIS** de bypass WAF hand-edité ni de port ouvert inutile (seul le
`waf_bypass = true` déclaratif du `haproxy.toml` est autorisé — cf. §WAF Bypass)
- nftables DEFAULT DROP — ouvrir explicitement seulement ce qui est nécessaire - nftables DEFAULT DROP — ouvrir explicitement seulement ce qui est nécessaire
- HAProxy en frontal TLS 1.3 pour toute exposition externe - HAProxy en frontal TLS 1.3 pour toute exposition externe
- AppArmor profile enforce pour chaque service - AppArmor profile enforce pour chaque service
@ -317,14 +318,20 @@ distinctes sans collision.
## 🔒 Security Policies — Héritées de secubox-openwrt, adaptées Debian ## 🔒 Security Policies — Héritées de secubox-openwrt, adaptées Debian
### WAF Bypass — Interdiction absolue ### WAF Bypass — Interdit par défaut, exception déclarative uniquement
* **JAMAIS de `waf_bypass` dans une config HAProxy** — tout le trafic DOIT passer * **Défaut : JAMAIS de bypass** — tout le trafic DOIT passer par mitmproxy/sbxwaf
par mitmproxy pour inspection pour inspection. Quand tu ajoutes un nouveau vhost, route systématiquement via
* Quand tu ajoutes un nouveau vhost, route systématiquement via le backend le backend `mitmproxy_inspector` dans HAProxy.
`mitmproxy_inspector` dans HAProxy * Si un service nécessite WebSocket ou long-polling, configure d'abord
* Si un service nécessite WebSocket ou long-polling, configure mitmproxy mitmproxy pour forward correctement — n'atteins le bypass qu'en dernier recours.
pour forward correctement — ne pas bypasser le WAF * **Seule exception sanctionnée : `waf_bypass = true` déclaratif par-vhost** dans
`/etc/secubox/haproxy.toml`, honoré par `haproxyctl` (backend `nginx_vhosts`,
droit vers nginx). Réservé aux services que la chaîne d'inspection casse
prouvablement — gros uploads / WebDAV / streaming média Range / fédération
(ex. `nc`, `photoprism`, `matrix`, `torrent`). Opt-in explicite, déclaratif,
auditable, versionné — **jamais un hand-edit du `haproxy.cfg` généré** (qui
serait écrasé au prochain `haproxyctl generate`) et jamais un défaut silencieux.
* Après ajout d'un backend HAProxy, mettre à jour `/srv/mitmproxy/haproxy-routes.json` * Après ajout d'un backend HAProxy, mettre à jour `/srv/mitmproxy/haproxy-routes.json`
ET `/srv/mitmproxy-in/haproxy-routes.json` : ET `/srv/mitmproxy-in/haproxy-routes.json` :
```json ```json

View File

@ -670,6 +670,12 @@ EOF
local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2) local domain=$(echo "$section" | grep -E '^domain\s*=' | cut -d'"' -f2)
local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2) local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2)
local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1") local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1")
# Opt-in, per-vhost WAF bypass. Sanctioned exception for services the
# inspection chain breaks (large uploads / WebDAV / media streaming /
# federation — nc, photoprism, matrix, torrent). Requires an explicit
# `waf_bypass = true` AND a real `backend` (nginx_vhosts) in the vhost
# block, so it is declarative and auditable — never a silent default.
local waf_bypass=$(echo "$section" | grep -E '^waf_bypass\s*=' | grep -q 'true' && echo "1" || echo "0")
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX} # WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi
@ -677,10 +683,10 @@ EOF
# short-circuited &&-chain (e.g. a non-SSL vhost) aborts generation. # short-circuited &&-chain (e.g. a non-SSL vhost) aborts generation.
if [ "$enabled" = "1" ] && [ -n "$domain" ]; then if [ "$enabled" = "1" ] && [ -n "$domain" ]; then
echo " acl host_$name hdr(host) -i $domain" echo " acl host_$name hdr(host) -i $domain"
if [ "$waf_enabled" = "1" ]; then if [ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ]; then
echo " use_backend mitmproxy_inspector if host_$name" echo " use_backend mitmproxy_inspector if host_$name"
else else
echo " use_backend $backend if host_$name" echo " use_backend ${backend:-nginx_vhosts} if host_$name"
fi fi
fi fi
done >> "$out" done >> "$out"
@ -720,16 +726,18 @@ EOF
local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2) local backend=$(echo "$section" | grep -E '^backend\s*=' | cut -d'"' -f2)
local ssl=$(echo "$section" | grep -E '^ssl\s*=' | grep -q 'true' && echo "1" || echo "0") local ssl=$(echo "$section" | grep -E '^ssl\s*=' | grep -q 'true' && echo "1" || echo "0")
local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1") local enabled=$(echo "$section" | grep -E '^enabled\s*=' | grep -q 'false' && echo "0" || echo "1")
# Per-vhost WAF bypass (declarative, opt-in) — see the http-in loop.
local waf_bypass=$(echo "$section" | grep -E '^waf_bypass\s*=' | grep -q 'true' && echo "1" || echo "0")
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX} # WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi
# if/then/fi (not `&& { }`) — set -e safe for non-SSL vhosts. # if/then/fi (not `&& { }`) — set -e safe for non-SSL vhosts.
if [ "$enabled" = "1" ] && [ "$ssl" = "1" ] && [ -n "$domain" ]; then if [ "$enabled" = "1" ] && [ "$ssl" = "1" ] && [ -n "$domain" ]; then
echo " acl host_$name hdr(host) -i $domain" echo " acl host_$name hdr(host) -i $domain"
if [ "$waf_enabled" = "1" ]; then if [ "$waf_enabled" = "1" ] && [ "$waf_bypass" != "1" ]; then
echo " use_backend mitmproxy_inspector if host_$name" echo " use_backend mitmproxy_inspector if host_$name"
else else
echo " use_backend $backend if host_$name" echo " use_backend ${backend:-nginx_vhosts} if host_$name"
fi fi
fi fi
done >> "$out" done >> "$out"

View File

@ -1,3 +1,16 @@
secubox-torrent (2.1.3-1~bookworm1) bookworm; urgency=medium
* Make the HAProxy exposure durable (was a hand-edit a `haproxyctl generate`
would wipe). postinst now registers the vhost in /etc/secubox/haproxy.toml
declaratively with `waf_bypass = true` (routes straight to nginx_vhosts —
torrent's Range streaming / large transfers break through the mitmproxy
inspection chain, same class as nc/photoprism/matrix). Regen is invoked but
drift-guarded (refuses on a board with un-migrated hand-edit vhosts, leaving
the live cfg intact; succeeds on a fresh board). Requires the companion
haproxyctl change that honours the per-vhost waf_bypass flag. (ref #917)
-- Gerald KERMA <devel@cybermind.fr> Tue, 28 Jul 2026 20:15:00 +0200
secubox-torrent (2.1.2-1~bookworm1) bookworm; urgency=high secubox-torrent (2.1.2-1~bookworm1) bookworm; urgency=high
* Fix remove/files/stream 500 + broken stats: WebTorrent 2.x's client.get() * Fix remove/files/stream 500 + broken stats: WebTorrent 2.x's client.get()

View File

@ -37,8 +37,7 @@ case "$1" in
fi fi
# Symlink the public vhost into sites-enabled (idempotent). Proxies # Symlink the public vhost into sites-enabled (idempotent). Proxies
# torrent.gk2.secubox.in -> torrent LXC at 10.100.0.160:8090. Operator # torrent.<host>.<suffix> -> torrent LXC at 10.100.0.160:8090.
# wires the HAProxy SNI ACL + ACME cert + sbxwaf route separately.
if [ -f /etc/nginx/sites-available/torrent.conf ] && [ -d /etc/nginx/sites-enabled ]; then if [ -f /etc/nginx/sites-available/torrent.conf ] && [ -d /etc/nginx/sites-enabled ]; then
ln -sf ../sites-available/torrent.conf /etc/nginx/sites-enabled/torrent.conf ln -sf ../sites-available/torrent.conf /etc/nginx/sites-enabled/torrent.conf
fi fi
@ -46,6 +45,43 @@ case "$1" in
if command -v nginx >/dev/null 2>&1 && nginx -t >/dev/null 2>&1; then if command -v nginx >/dev/null 2>&1 && nginx -t >/dev/null 2>&1; then
systemctl reload nginx 2>/dev/null || true systemctl reload nginx 2>/dev/null || true
fi fi
# Register the HAProxy exposure DURABLY (was previously a hand-edit that a
# `haproxyctl generate` would wipe). Torrent is a WAF-bypass vhost by design
# — like nc/photoprism/matrix, the mitmproxy inspection chain breaks its
# Range streaming / large transfers — so it declares `waf_bypass = true`
# (the sanctioned, auditable opt-in honoured by haproxyctl) and routes
# straight to the host nginx (nginx_vhosts), never through sbxwaf. This
# writes the declaration into /etc/secubox/haproxy.toml; the actual regen is
# drift-guarded (it refuses when the board still holds un-migrated hand-edit
# vhosts, leaving the live cfg untouched, and succeeds on a fresh board).
if [ -f /etc/secubox/haproxy.toml ]; then
# Canonical vhost name/domain from the board identity (falls back to gk2).
if [ -f /etc/default/secubox ]; then
. /etc/default/secubox 2>/dev/null || true
fi
SBX_HOST="${SECUBOX_HOSTNAME:-gk2}"; SBX_SUFFIX="${SECUBOX_DOMAIN_SUFFIX:-secubox.in}"
T_DOMAIN="torrent.${SBX_HOST}.${SBX_SUFFIX}"
T_NAME="torrent_$(echo "${SBX_HOST}.${SBX_SUFFIX}" | tr '.' '_')"
if ! grep -q "^\[vhosts\.${T_NAME}\]" /etc/secubox/haproxy.toml; then
{
printf '\n[vhosts.%s]\n' "$T_NAME"
printf 'domain = "%s"\n' "$T_DOMAIN"
printf 'backend = "nginx_vhosts"\n'
printf 'waf_bypass = true\n'
printf 'ssl = true\n'
printf 'ssl_redirect = true\n'
printf 'enabled = true\n'
} >> /etc/secubox/haproxy.toml
fi
if command -v haproxyctl >/dev/null 2>&1; then
if haproxyctl generate >/dev/null 2>&1; then
systemctl reload haproxy 2>/dev/null || true
else
echo "secubox-torrent: haproxy.toml updated; deferred regen (drift-guarded — run 'haproxyctl generate' once the board's hand-edit vhosts are migrated)" >&2
fi
fi
fi
;; ;;
esac esac