diff --git a/packages/secubox-waf-ng/debian/changelog b/packages/secubox-waf-ng/debian/changelog index cd33c90c..f8ed8e55 100644 --- a/packages/secubox-waf-ng/debian/changelog +++ b/packages/secubox-waf-ng/debian/changelog @@ -1,3 +1,18 @@ +secubox-waf-ng (1.1.0-1~bookworm1) bookworm; urgency=medium + + * #896 WAF-drift reconcile: ship a SINGLE HARDENED unit (secubox-waf-ng.service, + :8085, User=secubox-waf, full sandbox) that replaces BOTH the crash-looping + worker@ fan-out template (retired — it panicked on the root-only cookie-audit + log and, via RuntimeDirectory=secubox as secubox-waf, re-chowned the shared + /run/secubox and broke every secubox-user socket bind) AND the hand-created + unhardened root :8085 override the board was running. + * NO RuntimeDirectory=secubox (sbxwaf only CONNECTS to waker.sock) → kills the + re-chown landmine. postinst asserts perms a non-root sbxwaf needs + (haproxy-routes.json 0644, cookie-audit ledger writable by secubox-waf), + disables leftover worker@ units, and drops the /etc override. + + -- Gerald KERMA Tue, 21 Jul 2026 10:00:00 +0200 + secubox-waf-ng (1.0.0-1~bookworm1) bookworm; urgency=medium * Initial packaging of the Go WAF reverse-proxy engine (#744 Task 8.1). diff --git a/packages/secubox-waf-ng/debian/postinst b/packages/secubox-waf-ng/debian/postinst index e3769c05..519a9f07 100755 --- a/packages/secubox-waf-ng/debian/postinst +++ b/packages/secubox-waf-ng/debian/postinst @@ -59,6 +59,20 @@ case "$1" in mkdir -p /var/log/secubox/cookie-audit chown secubox-waf:secubox-waf /var/log/secubox/cookie-audit chmod 0750 /var/log/secubox/cookie-audit + # The ledger FILE must be writable by the non-root sbxwaf that appends to it. + # A pre-existing file owned by another user (e.g. secubox from an old addon) + # makes sbxwaf panic on open → crash-loop (#896 reconcile). Chown if present. + if [ -e /var/log/secubox/cookie-audit/server.jsonl ]; then + chown secubox-waf:secubox-waf /var/log/secubox/cookie-audit/server.jsonl || true + fi + + # haproxy-routes.json must be READABLE by the non-root sbxwaf (#896 reconcile): + # root-only 0600 makes it load 0 routes → 421 everything. Routes are + # [host,port] pairs, not secret → 0644 (the norm; the profiles actuator + # preserves this mode on its atomic writes). + if [ -e /etc/secubox/waf/haproxy-routes.json ]; then + chmod 0644 /etc/secubox/waf/haproxy-routes.json || true + fi # ── 4. Cache directories ────────────────────────────────────────────────── # Same shared-parent pattern: /var/cache/secubox must stay 0755. @@ -81,32 +95,32 @@ case "$1" in aa-enforce /etc/apparmor.d/usr.sbin.sbxwaf 2>/dev/null || true fi - # ── 6. Systemd: enable and start workers ───────────────────────────────── + # ── 6. Systemd: single hardened unit (#896 reconcile) ──────────────────── + # The crash-looping worker@ fan-out is retired. Retire any leftover worker@ + # instances, drop the hand-created /etc override that shadowed the packaged + # unit, then enable + (re)start the single hardened secubox-waf-ng.service. if [ -d /run/systemd/system ]; then + # Disable + stop any surviving worker@ instances (they re-chowned the + # shared /run/secubox and broke sibling sockets — see the unit header). + for unit in secubox-waf-ng-worker@1.service secubox-waf-ng-worker@2.service; do + systemctl disable --now "$unit" 2>/dev/null || true + done + + # A previous cutover left a hand-created override at + # /etc/systemd/system/secubox-waf-ng.service that shadows this packaged + # /lib unit. Remove it so the packaged (hardened) unit takes effect. Only + # a plain file (not a symlink an operator may have set deliberately). + if [ -f /etc/systemd/system/secubox-waf-ng.service ] && \ + [ ! -L /etc/systemd/system/secubox-waf-ng.service ]; then + rm -f /etc/systemd/system/secubox-waf-ng.service + fi + systemctl daemon-reload || true - - # Enable + start both worker instances. - # Guard: only when dpkg is not in --simulate / no-systemd build env. - for instance in 1 2; do - unit="secubox-waf-ng-worker@${instance}.service" - systemctl enable "$unit" 2>/dev/null || true - systemctl start "$unit" 2>/dev/null || true - done - fi - - # ── 7. On UPGRADE: restart any running enabled workers ─────────────────── - # dpkg upgrade SIGTERMs the units BEFORE this postinst runs, so - # `try-restart` (a no-op on a stopped unit) would leave workers DEAD. - # Mirror the toolbox postinst fix: ENABLED units get a full `restart`. - if [ -n "${2:-}" ] && [ -d /run/systemd/system ]; then - for unit in secubox-waf-ng-worker@1.service \ - secubox-waf-ng-worker@2.service; do - if systemctl is-enabled --quiet "$unit" 2>/dev/null; then - systemctl restart "$unit" 2>/dev/null || true - else - systemctl try-restart "$unit" 2>/dev/null || true - fi - done + systemctl enable secubox-waf-ng.service 2>/dev/null || true + # Fresh install starts it; upgrade restarts it (dpkg SIGTERMs the unit + # before this postinst runs, so `restart` — not `try-restart` — is needed + # to bring it back). HAProxy srv0 → 127.0.0.1:8085 keeps pointing at it. + systemctl restart secubox-waf-ng.service 2>/dev/null || true fi ;; diff --git a/packages/secubox-waf-ng/debian/rules b/packages/secubox-waf-ng/debian/rules index f0711fb1..eed721af 100755 --- a/packages/secubox-waf-ng/debian/rules +++ b/packages/secubox-waf-ng/debian/rules @@ -52,16 +52,14 @@ override_dh_auto_clean: rm -rf _gocache _gopath override_dh_installsystemd: - # Install the worker template unit manually (dh_installsystemd expects the - # unit file at debian/..service or debian/.service; - # ours lives in systemd/ so we install it explicitly, mirroring the toolbox - # package pattern for override_dh_installsystemd). + # #896 reconcile: ship the SINGLE hardened unit (secubox-waf-ng.service, + # :8085) — it replaces the crash-looping worker@ fan-out template, which is + # NO LONGER installed (a reinstall must not resurrect the re-chown landmine; + # the postinst disables + removes any leftover worker@ units on the board). install -d debian/secubox-waf-ng/lib/systemd/system - install -m 0644 systemd/secubox-waf-ng-worker@.service \ + install -m 0644 systemd/secubox-waf-ng.service \ debian/secubox-waf-ng/lib/systemd/system/ - # Let dh_installsystemd generate the enable helpers for @1 and @2. - # The postinst's `systemctl enable --now` handles activation. - dh_installsystemd --name=secubox-waf-ng-worker@ + dh_installsystemd --name=secubox-waf-ng execute_after_dh_auto_install: install -d debian/secubox-waf-ng/usr/sbin diff --git a/packages/secubox-waf-ng/systemd/secubox-waf-ng.service b/packages/secubox-waf-ng/systemd/secubox-waf-ng.service new file mode 100644 index 00000000..317339c5 --- /dev/null +++ b/packages/secubox-waf-ng/systemd/secubox-waf-ng.service @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# SecuBox-Deb :: secubox-waf-ng — Go WAF single reverse-proxy unit (#896 reconcile) +# +# Reconcile of the WAF-ng drift (2026-07-21): the board ran a HAND-CREATED, +# UNHARDENED root unit on :8085 (HAProxy srv0 -> 127.0.0.1:8085) while the +# package shipped only a worker@ fan-out template that crash-looped (it panicked +# on the cookie-audit log — writable only by root — and, running as secubox-waf +# with RuntimeDirectory=secubox, each crash-restart re-chowned the SHARED +# /run/secubox, breaking every secubox-user service's socket bind). This single +# unit REPLACES both: one hardened, non-root sbxwaf on :8085, reproducible from +# source. See docs / issue #896. +# +# KEY DIFFERENCES from the (retired) worker@ template: +# * single instance on :8085 (matches the live HAProxy backend), NOT an +# nft-DNAT fan-out to :8081/:8082 (that never actually ran); +# * single --vhost-signals file (secubox-sleeper's reader falls back to the +# legacy non-@ path); +# * NO RuntimeDirectory=secubox — sbxwaf only CONNECTS to the waker socket +# (/run/secubox/waker.sock) as a client; it creates nothing under +# /run/secubox, so it must not take ownership of the shared dir. /run/secubox +# stays in ReadWritePaths so the client connect works under ProtectSystem. +# +# PREREQUISITES the postinst asserts (so a non-root sbxwaf can read them): +# * /etc/secubox/waf/haproxy-routes.json readable (0644 — [host,port] pairs, +# not secret); root-only 0600 makes sbxwaf load 0 routes -> 421 everything; +# * /var/log/secubox/cookie-audit/server.jsonl writable by secubox-waf. + +[Unit] +Description=SecuBox WAF-NG Go reverse-proxy (single instance, port 8085) +Documentation=https://github.com/CyberMind-FR/secubox-deb/issues/896 +After=network.target +ConditionPathExists=/usr/sbin/sbxwaf + +[Service] +Type=simple +User=secubox-waf +Group=secubox-waf + +ExecStart=/usr/sbin/sbxwaf \ + --listen 127.0.0.1:8085 \ + --routes /etc/secubox/waf/haproxy-routes.json \ + --rules /etc/secubox/waf/waf-rules.json \ + --on-demand-vhosts /etc/secubox/waf/on-demand-vhosts.json \ + --vhost-signals /var/cache/secubox/waf/vhost-signals.json \ + --threat-log /var/log/secubox/waf/waf-threats.log \ + --cookie-audit-log /var/log/secubox/cookie-audit/server.jsonl \ + --media-cache-dir /var/cache/secubox/waf/media \ + --crowdsec-url http://10.100.0.1:8080 + +# --crowdsec-url is the real CrowdSec LAPI (LXC-bridge gateway 10.100.0.1:8080), +# NOT 127.0.0.1:8080 (the nft DNAT VIP that would self-loop into this proxy). + +Restart=on-failure +RestartSec=5 + +# ── Hardening ───────────────────────────────────────────────────────────────── +# Same posture as the worker@ template (secubox-mesh convention, §6 CSPN), minus +# RuntimeDirectory (see header). /run/secubox is READ-WRITE so the client connect +# to waker.sock works under ProtectSystem=strict. +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +PrivateDevices=yes +ReadOnlyPaths=/etc/secubox +ReadWritePaths=/var/log/secubox/waf /var/log/secubox/cookie-audit /var/cache/secubox/waf /run/secubox + +CapabilityBoundingSet= +AmbientCapabilities= +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX + +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +RestrictNamespaces=yes +LockPersonality=yes +RestrictSUIDSGID=yes +RestrictRealtime=yes + +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources +MemoryDenyWriteExecute=yes + +MemoryHigh=256M +MemoryMax=384M +TasksMax=512 +RuntimeMaxSec=12h + +[Install] +WantedBy=multi-user.target