mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 19:43:27 +00:00
Compare commits
5 Commits
ce636273a6
...
9d1b0abade
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d1b0abade | |||
| 41d78ef455 | |||
| fbd474b2c3 | |||
| c47e454532 | |||
| e12790efbd |
|
|
@ -0,0 +1,56 @@
|
||||||
|
<!-- SPDX-License-Identifier: LicenseRef-CMSD-1.0 -->
|
||||||
|
# Spec — HAProxy complete dynamic vhost auto-discovery
|
||||||
|
|
||||||
|
*2026-06-17 · landed for later (per user) · found while fixing #626/#627*
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
`haproxyctl generate` must produce a COMPLETE config so regen never drops
|
||||||
|
hand-maintained vhosts. Today ~10 vhosts live only in the hand-edited
|
||||||
|
`/etc/haproxy/haproxy.cfg` — not in `haproxy.toml` or `cfg.d/`:
|
||||||
|
|
||||||
|
- `kbin.gk2.secubox.in` → `toolbox_landing` (backend also live-only)
|
||||||
|
- `matrix`, `gitea`, `peertube`, `photoprism` (.gk2.secubox.in) → `nginx_vhosts`
|
||||||
|
|
||||||
|
These bypass the WAF today (route direct, not through `mitmproxy_inspector`).
|
||||||
|
A clean regen omits them. PR #627 added a **drift guard** so regen refuses to
|
||||||
|
clobber when its output has fewer vhosts/backends than live — safe, but not
|
||||||
|
complete.
|
||||||
|
|
||||||
|
## Design (approved direction: auto-discover from modules/LXC)
|
||||||
|
1. **Drop-in registry.** Generator aggregates `haproxy.toml [vhosts.*]` **plus**
|
||||||
|
`/etc/secubox/vhosts.d/*.toml` — one file per module/LXC, dropped by that
|
||||||
|
module's postinst (self-registration). New modules appear automatically.
|
||||||
|
2. **Per-vhost routing intent** replaces the blanket `waf_enabled` override
|
||||||
|
(which currently forces *every* vhost through the WAF):
|
||||||
|
```toml
|
||||||
|
[vhost]
|
||||||
|
domain = "peertube.gk2.secubox.in"
|
||||||
|
backend = "nginx_vhosts" # or a module/LXC backend
|
||||||
|
ssl = true
|
||||||
|
inspect = false # true → mitmproxy_inspector (WAF); false → direct
|
||||||
|
```
|
||||||
|
3. **One-time migration.** Seed `vhosts.d/` from the ~10 drifted live entries
|
||||||
|
with their real backends + `inspect=false`; register `toolbox_landing` as a
|
||||||
|
known backend. After this, regen output == live → drift guard passes.
|
||||||
|
4. **Module-registration helper** for postinsts (e.g. `haproxyctl vhost register
|
||||||
|
--from-file` or a tiny library) so each LXC/module declares its vhost.
|
||||||
|
5. **Keep the drift guard** as the transition safety net.
|
||||||
|
|
||||||
|
## Validation gate (non-negotiable)
|
||||||
|
Never apply a regenerated cfg to production until a diff proves it reproduces all
|
||||||
|
~100 live vhosts/backends 1:1 (drift-guard counts match).
|
||||||
|
|
||||||
|
## Related: finish the traversal-footgun sweep (#623)
|
||||||
|
The systemic `install -d -m 0750 .../secubox` footgun is broader than first
|
||||||
|
swept: **multi-arg** forms like `install -d -m 0750 /run/secubox /var/lib/secubox
|
||||||
|
…` (e.g. secubox-haproxy, fixed in #627) were missed by the earlier grep
|
||||||
|
(`…/secubox/[a-z]` required a leaf). Re-sweep with a pattern that catches bare
|
||||||
|
`/var/{lib,log,cache}/secubox` arguments, and add a **tmpfiles.d + periodic
|
||||||
|
guard** so the shared parents self-heal to `0755` regardless of which package
|
||||||
|
clobbers them (this is the root of the recurring kbin/toolbox 500s).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- Generator no longer crashes (set -e + dup-backend fixed, #627).
|
||||||
|
- Drift guard prevents clobbering (#627).
|
||||||
|
- Error pages live + served from `/etc/haproxy/secubox-errors/` (#627).
|
||||||
|
- This auto-discovery rework + the #623 re-sweep are the remaining work.
|
||||||
|
|
@ -6,7 +6,20 @@ secubox-haproxy (1.3.1-1~bookworm1) bookworm; urgency=medium
|
||||||
with live status + manual retry. {400,403,408,500}.http: branded static.
|
with live status + manual retry. {400,403,408,500}.http: branded static.
|
||||||
- haproxyctl generator: wire errorfile directives (durable across regen)
|
- haproxyctl generator: wire errorfile directives (durable across regen)
|
||||||
and persist retries 3 + option redispatch in defaults.
|
and persist retries 3 + option redispatch in defaults.
|
||||||
- Ship pages to /etc/haproxy/errors/; postinst regen-safe applies them.
|
- Ship pages to /etc/haproxy/secubox-errors/ (own dir; /etc/haproxy/errors
|
||||||
|
is owned by the haproxy package). errorfile wired there.
|
||||||
|
* fix(generator): `haproxyctl generate` was broken (exited 1, produced no
|
||||||
|
backends) — root causes: (a) `set -e` + `[ ] && [ ] && { }` vhost-loop
|
||||||
|
chains aborted generation on the first non-SSL vhost; (b) duplicate
|
||||||
|
`backend mitmproxy_inspector` (auto + user TOML) → fatal. Converted the
|
||||||
|
chains to if/then/fi and dedup user backends already emitted.
|
||||||
|
* fix(generator): drift guard — refuse to install a generated cfg with fewer
|
||||||
|
vhosts/backends than the live one (prevents a successful regen from silently
|
||||||
|
dropping hand-maintained vhosts like kbin/gitea/matrix not yet in
|
||||||
|
haproxy.toml). Surfaces the drift instead of clobbering.
|
||||||
|
* fix(postinst): keep /run/secubox + /var/lib/secubox at 0755 (was set 0750,
|
||||||
|
breaking directory traversal for other secubox-* daemons — kbin/toolbox 500).
|
||||||
|
Only the haproxy-private leaves are 0750.
|
||||||
|
|
||||||
-- Gerald Kerma <devel@cybermind.fr> Tue, 17 Jun 2026 09:00:00 +0200
|
-- Gerald Kerma <devel@cybermind.fr> Tue, 17 Jun 2026 09:00:00 +0200
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ case "$1" in
|
||||||
configure)
|
configure)
|
||||||
id -u secubox >/dev/null 2>&1 || \
|
id -u secubox >/dev/null 2>&1 || \
|
||||||
adduser --system --group --no-create-home --home /var/lib/secubox --shell /usr/sbin/nologin secubox
|
adduser --system --group --no-create-home --home /var/lib/secubox --shell /usr/sbin/nologin secubox
|
||||||
install -d -o secubox -g secubox -m 750 /run/secubox /var/lib/secubox /var/lib/secubox/haproxy /var/lib/secubox/haproxy/config_backups
|
# Shared parents stay 0755 (traversable by every secubox-* daemon — setting
|
||||||
|
# them 0750 here broke kbin/toolbox by blocking traversal, #626). Only the
|
||||||
|
# haproxy-private leaves are restricted.
|
||||||
|
install -d -o secubox -g secubox -m 755 /run/secubox /var/lib/secubox
|
||||||
|
install -d -o secubox -g secubox -m 750 /var/lib/secubox/haproxy /var/lib/secubox/haproxy/config_backups
|
||||||
|
chmod 0755 /var/lib/secubox /run/secubox 2>/dev/null || true
|
||||||
# Create /etc/haproxy if not present (haproxy is Recommends, not Depends)
|
# Create /etc/haproxy if not present (haproxy is Recommends, not Depends)
|
||||||
# Required for systemd namespace setup
|
# Required for systemd namespace setup
|
||||||
install -d -m 755 /etc/haproxy
|
install -d -m 755 /etc/haproxy
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ override_dh_auto_install:
|
||||||
[ -d www ] && cp -r www/. debian/secubox-haproxy/usr/share/secubox/www/ || true
|
[ -d www ] && cp -r www/. debian/secubox-haproxy/usr/share/secubox/www/ || true
|
||||||
install -d debian/secubox-haproxy/usr/share/secubox/menu.d
|
install -d debian/secubox-haproxy/usr/share/secubox/menu.d
|
||||||
[ -d menu.d ] && cp -r menu.d/. debian/secubox-haproxy/usr/share/secubox/menu.d/ || true
|
[ -d menu.d ] && cp -r menu.d/. debian/secubox-haproxy/usr/share/secubox/menu.d/ || true
|
||||||
# Smart self-healing HAProxy error pages (#626) — wired via errorfile in haproxyctl
|
# Smart self-healing HAProxy error pages (#626) — wired via errorfile in
|
||||||
install -d debian/secubox-haproxy/etc/haproxy/errors
|
# haproxyctl. Own dir (NOT /etc/haproxy/errors, which the haproxy package owns).
|
||||||
[ -d errors ] && install -m 644 errors/*.http debian/secubox-haproxy/etc/haproxy/errors/ || true
|
install -d debian/secubox-haproxy/etc/haproxy/secubox-errors
|
||||||
|
[ -d errors ] && install -m 644 errors/*.http debian/secubox-haproxy/etc/haproxy/secubox-errors/ || true
|
||||||
# Modular nginx config
|
# Modular nginx config
|
||||||
install -d debian/secubox-haproxy/etc/nginx/secubox.d
|
install -d debian/secubox-haproxy/etc/nginx/secubox.d
|
||||||
[ -f nginx/haproxy.conf ] && cp nginx/haproxy.conf debian/secubox-haproxy/etc/nginx/secubox.d/ || true
|
[ -f nginx/haproxy.conf ] && cp nginx/haproxy.conf debian/secubox-haproxy/etc/nginx/secubox.d/ || true
|
||||||
|
|
|
||||||
|
|
@ -626,13 +626,13 @@ defaults
|
||||||
option dontlognull
|
option dontlognull
|
||||||
option forwardfor
|
option forwardfor
|
||||||
# Smart self-healing error pages (#626) — shipped by secubox-haproxy.
|
# Smart self-healing error pages (#626) — shipped by secubox-haproxy.
|
||||||
errorfile 400 /etc/haproxy/errors/400.http
|
errorfile 400 /etc/haproxy/secubox-errors/400.http
|
||||||
errorfile 403 /etc/haproxy/errors/403.http
|
errorfile 403 /etc/haproxy/secubox-errors/403.http
|
||||||
errorfile 408 /etc/haproxy/errors/408.http
|
errorfile 408 /etc/haproxy/secubox-errors/408.http
|
||||||
errorfile 500 /etc/haproxy/errors/500.http
|
errorfile 500 /etc/haproxy/secubox-errors/500.http
|
||||||
errorfile 502 /etc/haproxy/errors/502.http
|
errorfile 502 /etc/haproxy/secubox-errors/502.http
|
||||||
errorfile 503 /etc/haproxy/errors/503.http
|
errorfile 503 /etc/haproxy/secubox-errors/503.http
|
||||||
errorfile 504 /etc/haproxy/errors/504.http
|
errorfile 504 /etc/haproxy/secubox-errors/504.http
|
||||||
|
|
||||||
frontend stats
|
frontend stats
|
||||||
bind *:${stats_port}
|
bind *:${stats_port}
|
||||||
|
|
@ -671,16 +671,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 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")
|
||||||
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
|
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
|
||||||
[ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ] && continue
|
if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi
|
||||||
|
|
||||||
[ "$enabled" = "1" ] && [ -n "$domain" ] && {
|
# NB: use if/then/fi, not `[ ] && [ ] && { }` — under `set -e` a
|
||||||
|
# short-circuited &&-chain (e.g. a non-SSL vhost) aborts generation.
|
||||||
|
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" ]; 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 if host_$name"
|
||||||
fi
|
fi
|
||||||
}
|
fi
|
||||||
done >> "$out"
|
done >> "$out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -719,16 +721,17 @@ EOF
|
||||||
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")
|
||||||
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
|
# WebUI strict-regex already covers admin.${SECUBOX_HOSTNAME}.${SECUBOX_DOMAIN_SUFFIX}
|
||||||
[ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ] && continue
|
if [ -n "$_admin_skip_domain" ] && [ "$domain" = "$_admin_skip_domain" ]; then continue; fi
|
||||||
|
|
||||||
[ "$enabled" = "1" ] && [ "$ssl" = "1" ] && [ -n "$domain" ] && {
|
# if/then/fi (not `&& { }`) — set -e safe for non-SSL vhosts.
|
||||||
|
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" ]; 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 if host_$name"
|
||||||
fi
|
fi
|
||||||
}
|
fi
|
||||||
done >> "$out"
|
done >> "$out"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -738,7 +741,8 @@ EOF
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# WAF inspector backend (mitmproxy LXC container)
|
# WAF inspector backend (mitmproxy LXC container)
|
||||||
[ "$waf_enabled" = "1" ] && cat >> "$out" << EOF
|
# if/then/fi (not `&& cat`) — under set -e a false test here aborts generation.
|
||||||
|
if [ "$waf_enabled" = "1" ]; then cat >> "$out" << EOF
|
||||||
# WAF Inspector Backend (mitmproxy LXC at $waf_ip:$waf_port)
|
# WAF Inspector Backend (mitmproxy LXC at $waf_ip:$waf_port)
|
||||||
backend mitmproxy_inspector
|
backend mitmproxy_inspector
|
||||||
mode http
|
mode http
|
||||||
|
|
@ -748,6 +752,7 @@ backend mitmproxy_inspector
|
||||||
server waf ${waf_ip}:${waf_port} check
|
server waf ${waf_ip}:${waf_port} check
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
# WebUI direct backend (issue #44 — for the strict-regex ACL above)
|
# WebUI direct backend (issue #44 — for the strict-regex ACL above)
|
||||||
# Only emit if SECUBOX_HOSTNAME is set (i.e., the strict ACL was injected).
|
# Only emit if SECUBOX_HOSTNAME is set (i.e., the strict ACL was injected).
|
||||||
|
|
@ -763,6 +768,9 @@ EOF
|
||||||
if [ -f "$CONF_PATH" ]; then
|
if [ -f "$CONF_PATH" ]; then
|
||||||
grep '^\[backends\.' "$CONF_PATH" | while read -r line; do
|
grep '^\[backends\.' "$CONF_PATH" | while read -r line; do
|
||||||
local name=$(echo "$line" | sed 's/\[backends\.//;s/\]//')
|
local name=$(echo "$line" | sed 's/\[backends\.//;s/\]//')
|
||||||
|
# Skip backends already emitted (mitmproxy_inspector/webui_direct are
|
||||||
|
# auto-generated above) to avoid duplicate-backend fatal errors.
|
||||||
|
if grep -q "^backend $name\$" "$out"; then continue; fi
|
||||||
local section=$(sed -n "/^\[backends\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
|
local section=$(sed -n "/^\[backends\.$name\]/,/^\[/p" "$CONF_PATH" | head -n -1)
|
||||||
local mode=$(echo "$section" | grep -E '^mode\s*=' | cut -d'"' -f2 || echo "http")
|
local mode=$(echo "$section" | grep -E '^mode\s*=' | cut -d'"' -f2 || echo "http")
|
||||||
local balance=$(echo "$section" | grep -E '^balance\s*=' | cut -d'"' -f2 || echo "roundrobin")
|
local balance=$(echo "$section" | grep -E '^balance\s*=' | cut -d'"' -f2 || echo "roundrobin")
|
||||||
|
|
@ -775,11 +783,11 @@ backend $name
|
||||||
EOF
|
EOF
|
||||||
local i=0
|
local i=0
|
||||||
echo "$servers" | while read -r srv; do
|
echo "$servers" | while read -r srv; do
|
||||||
[ -n "$srv" ] && {
|
if [ -n "$srv" ]; then
|
||||||
srv=$(echo "$srv" | tr -d ' ')
|
srv=$(echo "$srv" | tr -d ' ')
|
||||||
echo " server srv$i $srv check" >> "$out"
|
echo " server srv$i $srv check" >> "$out"
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
}
|
fi
|
||||||
done
|
done
|
||||||
echo "" >> "$out"
|
echo "" >> "$out"
|
||||||
done
|
done
|
||||||
|
|
@ -834,6 +842,22 @@ EOF
|
||||||
# to the live cfg and only validated at the end, leaving the file in a
|
# to the live cfg and only validated at the end, leaving the file in a
|
||||||
# broken state on failure (recurring "broken-by-vhost-add" backups).
|
# broken state on failure (recurring "broken-by-vhost-add" backups).
|
||||||
if haproxy -c -f "$out" 2>/dev/null; then
|
if haproxy -c -f "$out" 2>/dev/null; then
|
||||||
|
# Drift guard (#626): refuse to overwrite a live config that has MORE
|
||||||
|
# vhosts/backends than we just generated — that means the live cfg holds
|
||||||
|
# entries absent from our inputs (haproxy.toml / cfg.d). Without this,
|
||||||
|
# a successful regen would silently drop hand-maintained vhosts (kbin,
|
||||||
|
# gitea, …). `local x=$(...)` masks grep's exit so set -e stays happy.
|
||||||
|
if [ -f "$CONFIG_DIR/haproxy.cfg" ]; then
|
||||||
|
local _nh=$(grep -c '^[[:space:]]*acl host_' "$out")
|
||||||
|
local _oh=$(grep -c '^[[:space:]]*acl host_' "$CONFIG_DIR/haproxy.cfg")
|
||||||
|
local _nb=$(grep -c '^backend ' "$out")
|
||||||
|
local _ob=$(grep -c '^backend ' "$CONFIG_DIR/haproxy.cfg")
|
||||||
|
if [ "${_nh:-0}" -lt "${_oh:-0}" ] || [ "${_nb:-0}" -lt "${_ob:-0}" ]; then
|
||||||
|
error "Drift guard: generated cfg has fewer vhosts/backends than live (acl ${_nh}<${_oh} or backend ${_nb}<${_ob}) — refusing to clobber. Migrate the missing entries into haproxy.toml/cfg.d first."
|
||||||
|
rm -f "$out"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
install -m 0644 -o root -g root "$out" "$CONFIG_DIR/haproxy.cfg"
|
install -m 0644 -o root -g root "$out" "$CONFIG_DIR/haproxy.cfg"
|
||||||
rm -f "$out"
|
rm -f "$out"
|
||||||
log "Configuration generated, validated and installed: $CONFIG_DIR/haproxy.cfg"
|
log "Configuration generated, validated and installed: $CONFIG_DIR/haproxy.cfg"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user