Compare commits

..

22 Commits

Author SHA1 Message Date
39d7002b7a docs: record T0 build+deploy (core 1.1.10/toolbox 2.7.18 + #421 dirs-guard /run self-heal)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-24 10:12:49 +02:00
b985db14ff fix(core/#421): dirs-guard re-asserts /run/secubox 1777 root:root (RuntimeDirectory churn)
90+ services declare RuntimeDirectory=secubox, so systemd re-chowns the shared
socket parent to secubox:secubox 0755 on each start, locking out non-secubox
socket creators. Removing explicit chowns (#494) wasn't enough; the per-minute
dirs-guard now self-heals /run/secubox centrally instead of editing 90+ units.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:04:31 +02:00
1001d24180 fix(#494): stop ALL units chowning the shared /run/secubox parent (systemic)
/run/secubox parent to a module user, clobbering the canonical tmpfiles rule
(1777 root root) and 502'ing cross-user socket traversal:

- secubox-hub.service ExecStartPre (the active cascading culprit — fired on
  every (re)start, right after secubox-runtime re-applied 1777 root root)
- secubox-eye-remote / metrics / metablogizer postinsts (chown parent)
- secubox-eye-square postinst (chowned BOTH /run/secubox AND /var/log/secubox
  to secubox-eye-square — worst, #511 class)
- secubox-p2p postinst (chown parent + /var/log/secubox)

All now keep only mkdir as a fallback; the 1777 sticky parent lets each daemon
create its own socket, and module logs go to own subdirs (eye-square/p2p) instead
of owning the shared /var/log/secubox. Parent lifecycle is owned solely by
tmpfiles.d + secubox-runtime.service. Bumped: hub 1.4.4, eye-remote 1.0.1,
eye-square 1.0.4, metablogizer 1.2.2, metrics 1.0.4, p2p 1.7.1 (core 1.1.7
in prior commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 68d98bf1eb)
2026-06-24 09:59:11 +02:00
039824dcfa fix(core): stop secubox-core.service clobbering /run/secubox perms (ref #494)
secubox-core.service ran After=network.target, so its ExecStart
'chown secubox:secubox /run/secubox + chmod 775' fired LAST — after
secubox-runtime.service had re-applied the canonical tmpfiles rule
'd /run/secubox 1777 root root'. Result: the socket dir ended up
secubox:secubox, non-secubox daemons lost parent +x, and their sockets 502'd.

- Remove the chown/chmod of /run/secubox from ExecStart; the dir is now owned
  solely by tmpfiles.d (1777 root root) + secubox-runtime.service. This unit
  keeps only the www-data group membership.
- postinst defensively removes a stale non-dpkg-owned /etc/tmpfiles.d/secubox.conf
  declaring /run/secubox as 0775 secubox secubox (it overrode /usr/lib canonical).
- Ensure /var/log/suricata exists (0755) so ReadWritePaths services don't
  NAMESPACE-fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 60f05992ee)
2026-06-24 09:58:17 +02:00
4674b6023a Merge feature/65-fix-nginx-add-missing-api-routes-to-webu 2026-06-24 09:55:41 +02:00
1f21c59c19 Merge fix/53-wazuh-uvicorn-process-causes-100-cpu-spi 2026-06-24 09:55:41 +02:00
b41032107a Merge fix/121-metablog-ingest-site-dirs-should-be-crea 2026-06-24 09:55:41 +02:00
6739e19fea docs: #494 systemic fix done (7 pkgs, live-verified); #471 resolved; /var/log shared-owner follow-up
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:52:32 +02:00
b2891ff4d2 docs: #65 resolved (prod uses secubox-routes.d; template synced)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:38:57 +02:00
dba06bc47a fix(nginx): sync webui.conf template to the secubox-routes.d include (ref #65)
#65's symptom (new modules' /api/v1/<m>/ return 404 because their nginx routes
aren't loaded) is already solved in the deployed webui.conf via
'include /etc/nginx/secubox-routes.d/*.conf' — every module package drops a
location-only snippet there at install. The repo template common/nginx/webui.conf
was stale (hardcoded core blocks only, no include), which is misleading. Add the
active include so the template matches production; keep the hardcoded
crowdsec/waf/system blocks (those core packages ship only the legacy secubox.d/
snippet, so no duplicate-location).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:38:28 +02:00
b24d5bbbe0 docs: #53 + #121 fixes pushed (branches ready), update T0 status
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:34:45 +02:00
bb499b7fd5 fix(wazuh): gate service on /var/ossec to stop 100% CPU spin (ref #53)
The Wazuh API uvicorn worker was started unconditionally, but SecuBox's
documented IDS/IPS stack is Suricata + CrowdSec and /var/ossec is absent on
normal boards — so it busy-looped at ~100% CPU for nothing (operators had to
mask it by hand). Add ConditionPathExists=/var/ossec/etc/ossec.conf so systemd
reports 'inactive (condition failed)' and never starts it unless a local Wazuh
agent/manager is present, plus RestartSec=5 as a hot-respawn guard. Module
kept for opt-in SIEM integration, not removed. Verified on gk2: /var/ossec
absent, so the gated unit stays inert without needing the manual mask.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:34:11 +02:00
845683c9c9 fix(metablog): chown site dirs to secubox:secubox after ingest (ref #121)
ingest_site() runs git init/commit inside /srv/metablogizer/sites/<name> over
ssh as root, so a freshly created .git is owned root:root — but the
metablogizer service runs as 'secubox' and cannot write the repo (blocked
sub-E #113 webhook deploys). Add a fix_perms helper that chown -R
secubox:secubox the site dir after every .git-touching ingest path
(ingested-fresh x2 + ingested-with-history). Matches module postinst pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:30:13 +02:00
04064a9fb7 docs: close #468 (traversal source+live OK) + T0 live recon notes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:26:32 +02:00
a273cb570a docs: close #515/#516/#519 (verified; #519 enforcement plane repaired) + perf follow-up note
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:20:15 +02:00
73414e7550 fix(toolbox/blacklist-sync): make #519/#522 enforcement plane functional
The DNS-guard sync (Phase 13.B) aborted on the first unresolvable blocklisted
domain (getent exits 2 on NXDOMAIN; set -euo pipefail propagated it from the
ips=$(...) assignment) and, even guarded, a full sweep (~3min) overran the
unit's 120s TimeoutStartSec. Net effect: the oneshot failed every run, the nft
blacklist_v4/v6 sets stayed empty, and the protection enforcement plane was
inert. Guard the substitution with || true, raise TimeoutStartSec 120->600,
drop per-lookup timeout 2s->1s. Verified live on gk2: systemd run green, sets
populate (v4=1712, v6=273). Enforcement stays default-off (service disabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:18:54 +02:00
6c55a21df5 docs: prioritized backlog index (T0..T6) — review of 64 open issues
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:03:04 +02:00
6e4ef4d557 docs: close #486 (geoip/ASN/flags in reports — delivered on master), clean stale worktree
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:58:20 +02:00
87a31fba45 docs: close validated lot #475/#495/#502/#507/#508/#531 (triage)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:54:37 +02:00
c8d0e7d352 docs: issue triage 2026-06-22 — sync open issues to WIP (status review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 08:31:22 +02:00
CyberMind
3fcdb8bd9a
Merge pull request #725 from CyberMind-FR/feature/724-toolbox-banner-in-banner-r0-r3-level-swi
toolbox banner: in-banner R0..R3 level switch (closes #724)
2026-06-24 07:51:05 +02:00
cc478872ae feat(toolbox): in-banner R0..R3 level switch — show real state + change it (closes #724)
The injected transparency banner showed the client level as static text. It's now
an interactive R0/R1/R2/R3 switch: the current tier is highlighted and a tap
changes it.

- bundle.py: lvlSwitch() renders the 4 buttons (current highlighted); wireLevels()
  fires GET /__toolbox/set-level?mh=&level= then reloads so the new tier applies.
  Dismiss-button selector tightened to button[aria-label=dismiss] (the level
  buttons are now first). invalidate() drops the cached bundle on change.
- api.py: GET /__toolbox/set-level (unauth, like /bundle) → store.set_client_level
  by hash; validates level, gates r2 by config + r3 by wg server.pubkey; invalidates
  the bundle cache.
- store.py: set_client_level(mac_hash, level) — by-hash setter (R3 wg peers have no
  captive MAC/ip, so /change-level's nft path doesn't apply).
- sbxmitm/banner.go: intercept /__toolbox/set-level and reverse-proxy to the portal
  (same path as loader.js/bundle), so the banner's same-origin fetch works on any site.

Verified live on gk2: set-level r2→bundle r2, switch back r1, bad level→400, and the
worker reverse-proxies /__toolbox/set-level on a forged origin → {"ok":true}.
2026-06-24 07:50:58 +02:00
31 changed files with 475 additions and 26 deletions

View File

@ -6595,3 +6595,19 @@ CONFIG_USB_NET_RNDIS_HOST=y
- LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1 - LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1
- ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online - ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online
- STALE, FAILED = offline - STALE, FAILED = offline
## 2026-06-24 — build+deploy T0 fixes (#494/#519/#53/#421) + dirs-guard /run self-heal
- Merged #121/#53/#65; cherry-picked #494 onto master (versions re-bumped above
master's advanced core 1.1.8/hub 1.4.6 → core 1.1.9, hub 1.4.7).
- Discovered #494 was systemic (7 pkgs chowning /run/secubox parent) AND that
91 services declare `RuntimeDirectory=secubox` → systemd re-chowns the parent
to secubox:secubox 0755 on each start (#421). Central fix: extended
secubox-dirs-guard to re-assert /run/secubox 1777 root:root every minute
(core 1.1.10) instead of editing 91 units.
- Built + deployed to gk2 (8 pkgs): core 1.1.10, hub 1.4.7, eye-remote 1.0.1,
metablogizer 1.2.2, metrics 1.0.4, p2p 1.7.1, wazuh 1.0.1, toolbox 2.7.18.
First deploy ssh was timeout-killed mid-toolbox-postinst → recovered with
dpkg --configure -a (cleared stale lock). Verified: /run/secubox=1777 root:root
holds, 0 half-configured, all services + R3 workers active, webui/portal 200,
toolbox blacklist-sync (#519) carried.

View File

@ -42,6 +42,78 @@
--- ---
## 🎯 Backlog priorisé — revue 2026-06-24 (64 issues ouvertes)
> Index d'autorité du triage. Les sections « Phase X » plus bas sont historiques :
> plusieurs portent « ✅ COMPLETE » alors que l'issue est restée **ouverte** (livré
> mais jamais fermé) → marquées **[vérifier→fermer]** ci-dessous.
### 🔴 T0 — Régressions & bugs sécurité (petits, débloquants, CSPN priv-sep)
- #494 secubox-core ExecStart écrase tmpfiles.d `/run/secubox` *(worktree actif)*
- #468 `/etc/secubox` parent 0750 casse la traversée non-secubox *(régression récurrente)*
- #471 secubox-mesh postinst écrase perms `/run/secubox` *(régression)*
- #421 sockets `/run/secubox` cachés en mount-ns privé (RuntimeDirectory)
- #447 kiosk : mot de passe admin semé par le CI (users.json shippe un hash) **← fuite**
- #91 haproxyctl régénère haproxy.cfg avec `waf_inspector` inexistant *(intégrité WAF)*
- #65 nginx : routes API manquantes dans webui.conf
- #53 Wazuh uvicorn 100% CPU spin
- #121 metablog ingest : dirs en `secubox:secubox`
### 🟠 T1 — Plan d'enforcement sécurité (mission CSPN ; détection→action)
- #498 Phase 7 — WAF active enforcement (mitm→CrowdSec→nft drop) *(worktree actif)*
- ✅ #519 Phase 13 — enforcement plane **FERMÉ 2026-06-22** (livré + réparé :
blacklist-sync avortait sur NXDOMAIN + timeout unit → fix `|| true` +
TimeoutStartSec 600 ; vérifié live, default-off). Inclut 13.B #522.
- #455 secubox-egress — détection egress + corrélation RDS multi-signaux
- #500 Phase 8 — Utiq operator-grade tracking (detect/alert/bypass)
- #514 Phase 12 — plateforme anti-human-detection (parent ; sous-tracks fermés)
- ✅ #515 Phase 12.A CDN cache detection — **FERMÉ** (live, `social_host_meta.cdn_vendor`)
- ✅ #516 Phase 12.B anti-bot detection — **FERMÉ** (live via #564/#565, `social_antibot`)
- #525 Phase 14 — plan de déception (idée future, parké)
- ⬜ Suivi #519 perf (non bloquant) : DNS-guard ne résout que les 2000 premiers
domaines/cycle (5523 en base) → couverture partielle ; résolution séquentielle
lourde sur board saturé. Option : résolution parallèle bornée + rotation du cap.
### 🟡 T2 — UX / Hub / conscommateurs report (worktrees actifs + polish)
- #615 security-posture dans la sidebar Hub *(worktree actif)*
- #655 webext content-script banner CSP-immune *(worktree actif)*
- #485 toolbox SOC scoring *(worktree actif)*
- #513 ToolBox WebUI : sous-onglets + retrait UI /admin redondante
- #69 diagramme flux trafic responsive
- #67 cache history-aware glances/netdata
- #68 health checks + dépendances services au démarrage
### 🟢 T3 — Backlog feature (valeur, non bloquant)
- #685 APK 'corrupt' — CI signe avec clé éphémère *(plan APK verrouillé)*
- #686 android-toolbox flux non-root cassé *(plan APK verrouillé)*
- #429 nextcloud dashboard : API stubs au lieu de la vraie instance *(bug)*
- #430 nextcloud — fédération OCM (doc/outillage)
- #472 nextcloud — Gondwana Desktop (canvas + widgets)
- #592 secubox-webmail-hub (Gmail OAuth2 + Gandi + OVH)
- #66 auth Google OAuth
- #70 Health Banner System *(preplanned)*
- #71 CDN proxy injection *(preplanned)*
- #393 source-home des scripts health prober
### 🔵 T4 — Hardware-gated (dépend de pièces ; piste parallèle ; pas de spare EP06)
- Modem/PCIe : #254 modules kernel LTE · #255 pins mPCIe modem · #460 DTS cp0_pcie2 ·
#467 U-Boot comphy5 SerDes · #462 pivot HW AR9271/MT
- Mesh/BLE : #449 WiFi 802.11s · #452 BT mesh · #453 QR multi-canaux · #454 sourcing BLE 5.x
- GSM : #347 sentinelle-gsm
- Smart-Strip : #33 module HMI · #42 sous-repo · #379 packaging
- Eye-remote : #41 sous-repo · #79 buildroot · #127 variante square · #138 radar_concentric ·
#155 collision link-rename *(bug)* · #158 multi-gadget L3 · #478 métriques live Round Eye
- VILLAGE3B : #480 dossier presse · #497 poster grand public
### ⚪ T5 — Images / OS variants (basse urgence)
- #446 Full Traveller OS multi-mode/arch · #125 build-live-usb +virtualbox · #422 vm-x64 cascade
### ⚫ T6 — Docs / housekeeping
- #81 headers SPDX CMSD-1.0 partout · #243 clarifier scope secubox-zkp-auth *(question)*
- #474 ToolBoX (epic parent — garder comme tracker)
---
## 🔥 P0 — Immediate (in flight) ## 🔥 P0 — Immediate (in flight)
### kbin Tor endpoint — anonymized quick-switch surfing (#683) ### kbin Tor endpoint — anonymized quick-switch surfing (#683)

View File

@ -3,6 +3,59 @@
--- ---
## 🗂️ 2026-06-22 : triage issues (30 ouvertes → revue obsolètes)
- **Fermées (user-validé 2026-06-22)** : #722 (nDPId — décidé contre, reverté) ·
#475 ToolBoX Phase 1 (live 2.7.x) · #502/#507/#508 Social mapping (carto +
/social/me + report PDF live) · #495 Phase 5 mitm-LXC (superseded par #662 Go
sbxmitm host) · #531 APK one-tap (superseded par #685/#686 non-root) ·
#486 geoip/ASN+flags+catégories dans rapports (livré master : geo.py + dpi_class.py +
report wiring ; complémentaire de #718 ASN collector ; worktree stale nettoyé) ·
#515 CDN detection (live `social_host_meta.cdn_vendor`) · #516 anti-bot detection
(live via #564/#565) · #519 enforcement plane (livré + **réparé** : blacklist-sync
avortait NXDOMAIN + timeout unit → fix `|| true` + TimeoutStartSec 600, vérifié live,
default-off ; inclut #522). Toolbox source bumpé 2.7.18 (fix live-patché sur gk2) ·
#468 /etc/secubox traversal (source+live = 0755, secrets/CA enfants restent 0750).
- **Actives (worktrees en cours)** : #655 webext banner · #615 security-posture ·
#494 secubox-core ExecStart · #498 Phase 7 WAF enforcement · #485 SOC scoring.
### 🔎 Reco T0 — recon live gk2 2026-06-24 (avant fix)
- ✅ **#494** : **FIX SYSTÉMIQUE poussé** (`fix/494-…`). Pas que core : 7 units re-chownaient
le parent partagé `/run/secubox` (core+hub services, eye-remote/eye-square/metablogizer/
metrics/p2p postinsts ; eye-square chownait aussi /var/log/secubox = pire). Tous nettoyés
(mkdir fallback only ; logs modules en sous-dossier propre ; orphan /etc/tmpfiles.d nettoyé).
**Vérifié live** : /run/secubox 1777 **root:root** stable après restart core ET hub ; webui 200.
Bumps core 1.1.7/hub 1.4.4/eye-remote 1.0.1/eye-square 1.0.4/metablog 1.2.2/metrics 1.0.4/p2p 1.7.1.
- ✅ **#471** (mesh /run/secubox) : déjà résolu (changelog mesh "drop install -d /run/secubox") → verify-close.
- ⬜ **#421** : sockets cachés en mount-ns privé (RuntimeDirectory) — mécanisme distinct, non traité.
- 🆕 Suivi (classe #511) : mesh/toolbox/admin font `install -d -o <module> /var/log/secubox`
(propriétaire du parent partagé = user module) → autres daemons ne peuvent créer leurs logs.
Séparé de #494, à traiter (sous-dossiers propres comme fait pour eye-square/p2p).
- **#447** : pas une fuite — `password_hash=null` → lockout kiosk + user CI parasite ;
**CI-image-gated** (rpi400, pas gk2).
- **#91** : `haproxy.cfg` active valide ; backup `*.broken-by-haproxyctl-*` prouve le bug
passé ; drift-guard #627 rattrape. Root cause = generate `haproxyctl` (api/main.py l.846/896).
- ✅ **#53** : **FIX poussé** (`fix/53-…`) — gate `ConditionPathExists=/var/ossec/etc/ossec.conf`
+ `RestartSec=5` ; module conservé (SIEM opt-in). Vérifié gk2 (/var/ossec absent). Bump 1.0.1.
- ✅ **#65** : déjà résolu en prod (webui.conf déployé inclut `secubox-routes.d/*.conf`,
163 snippets). Template `common/nginx/webui.conf` (stale) synchronisé sur `feature/65-…`.
Reco fermer. Convention : `secubox-routes.d/`=actif, `secubox.d/`=legacy.
- ✅ **#121** : **FIX poussé** (`fix/121-…`) — helper `fix_perms` chown -R secubox:secubox
le site dir après chaque ingest .git (metablog-ingest-site.sh). Script dev, pas de deploy.
- ⬜ Restent : **#91** (deploy WAF risqué) · **#65** (refactor include, risque 502) ·
**#447** (CI kiosk) · **#494/#471/#421** (worktree fix/494). Build+deploy toolbox 2.7.18 (#519) en attente.
- **Backlog/future** : #685/#686 APK non-root (plan verrouillé) · #592 webmail-hub ·
#514/#515/#516/#519/#522/#525 Phase 12-14 (#515 CDN / #516 anti-bot partiellement
couverts par antibot_sites/opgrade_sites du social graph) · #500 Utiq · #497/#480/
#478 VILLAGE3B Eye/poster · #472/#430/#429 Nextcloud · #471/#468/#421 perms (à
vérifier si déjà corrigées) · #467/#462/#460/#255/#254 hardware/kernel · #455 egress ·
#454/#453/#452/#449 mesh/BLE · #448/#447/#446/#434 kiosk · #422 vm cascade ·
#393/#379/#347 packaging · #513 WebUI sub-tabs.
- ⚠️ Fermeture finale = **user only** (sauf issues créées en session) ; les
recommandations ci-dessus sont commentées sur chaque issue.
---
## ✅ 2026-06-22 : DPI exfil + Netrunner report + sbxmitm fixes (tous mergés, live gk2) ## ✅ 2026-06-22 : DPI exfil + Netrunner report + sbxmitm fixes (tous mergés, live gk2)
Session livrée intégralement sur master + déployée. Détail dans HISTORY 2026-06-22. Session livrée intégralement sur master + déployée. Détail dans HISTORY 2026-06-22.

View File

@ -55,4 +55,13 @@ server {
proxy_pass http://unix:/run/secubox/system.sock:/; proxy_pass http://unix:/run/secubox/system.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf; include /etc/nginx/snippets/secubox-proxy.conf;
} }
# #65: per-module routes self-register here. Every module package drops a
# /etc/nginx/secubox-routes.d/<module>.conf (location-only snippet) at
# install time, so a newly added module's /<module>/ + /api/v1/<module>/
# routes are picked up automatically — no more hand-editing this file per
# module. This is the ACTIVE include (matches the deployed webui.conf).
# The crowdsec/waf/system blocks above stay hardcoded: those core packages
# only ship the legacy secubox.d/ snippet, so they would NOT duplicate here.
include /etc/nginx/secubox-routes.d/*.conf;
} }

View File

@ -1,3 +1,38 @@
secubox-core (1.1.10-1~bookworm1) bookworm; urgency=medium
* #421/#494: secubox-dirs-guard now also re-asserts /run/secubox to
1777 root:root every minute. 90+ services declare RuntimeDirectory=secubox,
so systemd re-chowns the shared socket parent to secubox:secubox 0755 on
each (re)start — which locks out services that create their socket as a
non-secubox user (e.g. secubox-blacklist-attrib "Permission denied").
Removing the explicit chowns (#494) was necessary but not sufficient against
the RuntimeDirectory churn; the guard now self-heals it centrally instead of
patching 90+ unit files.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 11:15:00 +0000
secubox-core (1.1.9-1~bookworm1) bookworm; urgency=medium
* #494 fix: secubox-core.service no longer chowns /run/secubox to
secubox:secubox + chmod 775 in ExecStart. Running After=network.target,
it fired LAST and clobbered the canonical tmpfiles rule
(`d /run/secubox 1777 root root`) that secubox-runtime.service had just
re-applied — leaving the dir secubox:secubox so non-secubox daemons lost
parent +x and their sockets 502'd. /run/secubox lifecycle is now owned
solely by tmpfiles.d (1777 root root) + secubox-runtime.service; this unit
only manages the www-data group membership. Complements the #623 sweep
(which covered /var/lib,/var/log,/var/cache,/etc,/usr/share — not /run).
* Defensive: postinst removes a stale, non-dpkg-owned
/etc/tmpfiles.d/secubox.conf if it declares /run/secubox as
0775 secubox secubox (leftover from an old manual fix; /etc overrides
/usr/lib so it contradicted the canonical rule). The shipped canonical
file is /usr/lib/tmpfiles.d/secubox.conf.
* Also ensure /var/log/suricata exists (0755) so services with
ReadWritePaths=/var/log/suricata (secubox-threats/suricata) don't
NAMESPACE-fail at start.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:30:00 +0000
secubox-core (1.1.8-1~bookworm1) bookworm; urgency=medium secubox-core (1.1.8-1~bookworm1) bookworm; urgency=medium
* fix(#623): tmpfiles.d now declares all shared secubox parents * fix(#623): tmpfiles.d now declares all shared secubox parents

View File

@ -26,6 +26,16 @@ case "$1" in
install -d -o secubox -g secubox -m 755 /usr/share/secubox/www install -d -o secubox -g secubox -m 755 /usr/share/secubox/www
# ── tmpfiles.d for persistent /run/secubox ── # ── tmpfiles.d for persistent /run/secubox ──
# #494: drop a stale, non-dpkg-owned /etc/tmpfiles.d/secubox.conf left over
# from an old manual fix — it declares /run/secubox as 0775 secubox secubox
# and, because /etc overrides /usr/lib, it contradicts the canonical rule
# (/usr/lib/tmpfiles.d/secubox.conf: 1777 root root). Only remove it if it
# is NOT owned by any package and carries that bad declaration.
if [ -f /etc/tmpfiles.d/secubox.conf ] \
&& ! dpkg -S /etc/tmpfiles.d/secubox.conf >/dev/null 2>&1 \
&& grep -qE '/run/secubox[[:space:]]+0?775[[:space:]]+secubox' /etc/tmpfiles.d/secubox.conf; then
rm -f /etc/tmpfiles.d/secubox.conf
fi
systemd-tmpfiles --create 2>/dev/null || true systemd-tmpfiles --create 2>/dev/null || true
# ── Config initiale si absente ── # ── Config initiale si absente ──

View File

@ -5,10 +5,26 @@ After=network.target
[Service] [Service]
Type=oneshot Type=oneshot
RemainAfterExit=yes RemainAfterExit=yes
ExecStart=/bin/mkdir -p /run/secubox # Phase 3 (#494) : /run/secubox is owned by tmpfiles.d at 1777 root:root.
ExecStart=/bin/chown secubox:secubox /run/secubox # The previous ExecStart sequence (mkdir + chown secubox:secubox + chmod 775)
ExecStart=/bin/chmod 775 /run/secubox # ran AFTER tmpfiles.d and silently regressed the dir to 775, breaking cross-
# Add www-data to secubox group if not already (for nginx access) # user traversal for daemons running as users other than secubox (e.g.
# secubox-threats which 226/NAMESPACE-failed because ReadWritePaths bind
# couldn't cross 775 secubox:secubox + missing /var/log/suricata). See
# secubox-core/debian/changelog 1.1.7 for the failure post-mortem.
#
# /run/secubox lifecycle is now :
# 1. tmpfiles.d/secubox.conf creates 1777 root:root
# 2. this service is a no-op for /run/secubox (NOT touched)
# 3. each module's postinst chmods its own socket via UMask if needed
# /var/log/suricata must exist for any service whose ReadWritePaths references
# it (secubox-threats, secubox-suricata). Create idempotently.
ExecStart=/bin/mkdir -p /var/log/suricata
ExecStart=/bin/sh -c "chown secubox:secubox /var/log/suricata 2>/dev/null || true"
ExecStart=/bin/chmod 0755 /var/log/suricata
# Add www-data to secubox group (for nginx static-file access).
ExecStart=/bin/sh -c "id -nG www-data | grep -q secubox || usermod -aG secubox www-data" ExecStart=/bin/sh -c "id -nG www-data | grep -q secubox || usermod -aG secubox www-data"
[Install] [Install]

View File

@ -10,4 +10,17 @@ for d in /var/lib/secubox /var/log/secubox /var/cache/secubox /etc/secubox /usr/
[ -d "$d" ] || continue [ -d "$d" ] || continue
[ "$(stat -c %a "$d" 2>/dev/null)" = "755" ] || chmod 0755 "$d" 2>/dev/null || true [ "$(stat -c %a "$d" 2>/dev/null)" = "755" ] || chmod 0755 "$d" 2>/dev/null || true
done done
# /run/secubox is the world-writable socket dir and MUST stay 1777 root:root so
# a service running as ANY user can create its own socket. 90+ services declare
# `RuntimeDirectory=secubox`, which makes systemd re-chown the parent to
# secubox:secubox 0755 on every (re)start — locking out non-secubox socket
# creators (e.g. blacklist-attrib: "Permission denied") (#421/#494). Re-assert
# the canonical perms here so that flapping self-heals each minute.
if [ -d /run/secubox ]; then
[ "$(stat -c '%a %U %G' /run/secubox 2>/dev/null)" = "1777 root root" ] || {
chown root:root /run/secubox 2>/dev/null || true
chmod 1777 /run/secubox 2>/dev/null || true
}
fi
exit 0 exit 0

View File

@ -1,3 +1,10 @@
secubox-eye-remote (1.0.1-1~bookworm1) bookworm; urgency=medium
* #494: postinst no longer chowns the shared /run/secubox parent to
secubox:secubox (tmpfiles.d owns it 1777 root root).
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-eye-remote (1.0.0-1) bookworm; urgency=medium secubox-eye-remote (1.0.0-1) bookworm; urgency=medium
* Initial release * Initial release

View File

@ -8,9 +8,10 @@ case "$1" in
# Reload udev rules # Reload udev rules
udevadm control --reload-rules || true udevadm control --reload-rules || true
# Create runtime directory # Create runtime directory (fallback). #494: never chown the shared
# parent — it is owned by tmpfiles.d (1777 root root); chowning it to
# secubox:secubox breaks cross-user socket traversal.
mkdir -p /run/secubox mkdir -p /run/secubox
chown secubox:secubox /run/secubox 2>/dev/null || true
# Ensure config dirs exist with correct ownership. # Ensure config dirs exist with correct ownership.
# 0755 on eye-remote/ so the dnsmasq daemon (running as dnsmasq:nogroup # 0755 on eye-remote/ so the dnsmasq daemon (running as dnsmasq:nogroup

View File

@ -1,3 +1,11 @@
secubox-eye-square (1.0.4-1~bookworm1) bookworm; urgency=medium
* #494/#511: postinst no longer chowns the shared parents /run/secubox and
/var/log/secubox to secubox-eye-square (it stripped every other daemon's
traversal/log access). Logs go to an own subdir /var/log/secubox/eye-square.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-eye-square (1.0.3-1~bookworm1) bookworm; urgency=medium secubox-eye-square (1.0.3-1~bookworm1) bookworm; urgency=medium
* Relocate firstboot.sh from /usr/local/sbin/ to /usr/lib/secubox/ * Relocate firstboot.sh from /usr/local/sbin/ to /usr/lib/secubox/

View File

@ -8,9 +8,15 @@ case "$1" in
useradd --system --no-create-home --shell /usr/sbin/nologin secubox-eye-square useradd --system --no-create-home --shell /usr/sbin/nologin secubox-eye-square
fi fi
# Ensure runtime + audit dirs # Ensure runtime + audit dirs.
# #494/#511: NEVER chown the shared parents (/run/secubox,
# /var/log/secubox) to a module user — that strips other daemons of
# traversal and 502s the whole board. The parents are owned by
# tmpfiles.d/secubox-core (1777 root root / 0755). This module creates
# its socket inside the 1777 sticky dir and logs into its OWN subdir.
mkdir -p /run/secubox /var/log/secubox mkdir -p /run/secubox /var/log/secubox
chown secubox-eye-square:secubox-eye-square /run/secubox /var/log/secubox install -d -o secubox-eye-square -g secubox-eye-square -m 0750 \
/var/log/secubox/eye-square
# Activate AppArmor profile # Activate AppArmor profile
if command -v apparmor_parser >/dev/null 2>&1; then if command -v apparmor_parser >/dev/null 2>&1; then

View File

@ -1,3 +1,12 @@
secubox-hub (1.4.7-1~bookworm1) bookworm; urgency=medium
* #494: drop the ExecStartPre chown/chmod of the shared /run/secubox parent.
It ran on every hub start and clobbered the canonical tmpfiles rule
(1777 root root) back to 775 secubox:secubox, 502'ing cross-user socket
traversal. The 1777 sticky parent already lets the hub create its socket.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-hub (1.4.6-1~bookworm1) bookworm; urgency=medium secubox-hub (1.4.6-1~bookworm1) bookworm; urgency=medium
* perf(#644): dashboard/status/modules/alerts + public/health-batch now served * perf(#644): dashboard/status/modules/alerts + public/health-batch now served

View File

@ -10,10 +10,13 @@ User=secubox
Group=secubox Group=secubox
WorkingDirectory=/usr/lib/secubox/hub WorkingDirectory=/usr/lib/secubox/hub
# Ensure /run/secubox exists before starting (fallback if secubox-core didn't run) # Ensure /run/secubox exists before starting (fallback if secubox-core didn't run)
# + prefix runs as root regardless of User= setting # + prefix runs as root regardless of User= setting.
# #494: do NOT chown/chmod the parent here — it is owned by tmpfiles.d
# (1777 root root) + secubox-runtime.service. This ExecStartPre ran on every
# hub (re)start and clobbered it back to 775 secubox:secubox, breaking
# cross-user socket traversal (nginx/www-data 502). The sticky 1777 parent
# already lets the hub create its own socket; only mkdir as a fallback.
ExecStartPre=+/bin/mkdir -p /run/secubox ExecStartPre=+/bin/mkdir -p /run/secubox
ExecStartPre=+/bin/chown secubox:secubox /run/secubox
ExecStartPre=+/bin/chmod 775 /run/secubox
# TCP binding for VM compatibility (Unix socket has issues in some VMs) # TCP binding for VM compatibility (Unix socket has issues in some VMs)
ExecStart=/usr/bin/python3 -m uvicorn api.main:app \ ExecStart=/usr/bin/python3 -m uvicorn api.main:app \
--host 127.0.0.1 --port 8001 \ --host 127.0.0.1 --port 8001 \

View File

@ -1,3 +1,10 @@
secubox-metablogizer (1.2.2-1~bookworm1) bookworm; urgency=medium
* #494: postinst no longer chowns the shared /run/secubox parent; only the
module's own /srv/metablogizer/sites is owned (see also #121).
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-metablogizer (1.2.1-1~bookworm1) bookworm; urgency=medium secubox-metablogizer (1.2.1-1~bookworm1) bookworm; urgency=medium
* debian/secubox-metablogizer.service: lower uvicorn --log-level from * debian/secubox-metablogizer.service: lower uvicorn --log-level from

View File

@ -1,7 +1,9 @@
#!/bin/bash #!/bin/bash
set -e set -e
mkdir -p /run/secubox /srv/metablogizer/sites mkdir -p /run/secubox /srv/metablogizer/sites
chown secubox:secubox /run/secubox /srv/metablogizer/sites 2>/dev/null || true # #494: do NOT chown the shared parent /run/secubox (owned by tmpfiles.d at
# 1777 root root). Only own this module's own data dir.
chown secubox:secubox /srv/metablogizer/sites 2>/dev/null || true
systemctl daemon-reload systemctl daemon-reload
systemctl enable secubox-metablogizer 2>/dev/null || true systemctl enable secubox-metablogizer 2>/dev/null || true
systemctl start secubox-metablogizer 2>/dev/null || true systemctl start secubox-metablogizer 2>/dev/null || true

View File

@ -1,3 +1,10 @@
secubox-metrics (1.0.4-1~bookworm1) bookworm; urgency=medium
* #494: postinst no longer chowns the shared /run/secubox parent
(tmpfiles.d owns it 1777 root root).
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-metrics (1.0.3-1~bookworm1) bookworm; urgency=medium secubox-metrics (1.0.3-1~bookworm1) bookworm; urgency=medium
* VisitorOrigin: add AmbientCapabilities=CAP_NET_ADMIN to the systemd * VisitorOrigin: add AmbientCapabilities=CAP_NET_ADMIN to the systemd

View File

@ -8,9 +8,9 @@ case "$1" in
useradd -r -s /usr/sbin/nologin -d /var/lib/secubox secubox useradd -r -s /usr/sbin/nologin -d /var/lib/secubox secubox
fi fi
# Create runtime directory # Create runtime directory (fallback). #494: never chown the shared
# parent /run/secubox — it is owned by tmpfiles.d (1777 root root).
mkdir -p /run/secubox mkdir -p /run/secubox
chown secubox:secubox /run/secubox
# Cache directory — was /tmp/secubox until #149. Now /var/cache/secubox, # Cache directory — was /tmp/secubox until #149. Now /var/cache/secubox,
# auto-managed by systemd's CacheDirectory=secubox at unit start. # auto-managed by systemd's CacheDirectory=secubox at unit start.

View File

@ -1,3 +1,10 @@
secubox-p2p (1.7.1-1~bookworm1) bookworm; urgency=medium
* #494/#511: postinst no longer chowns the shared parents /run/secubox and
/var/log/secubox; logs go to an own subdir /var/log/secubox/p2p.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:45:00 +0000
secubox-p2p (1.7.0-1~bookworm1) bookworm; urgency=medium secubox-p2p (1.7.0-1~bookworm1) bookworm; urgency=medium
* Absorb the master-link role from secubox-master-link (closes #384) * Absorb the master-link role from secubox-master-link (closes #384)

View File

@ -8,15 +8,15 @@ case "$1" in
adduser --system --group --home /var/lib/secubox --no-create-home secubox adduser --system --group --home /var/lib/secubox --no-create-home secubox
fi fi
# Create runtime directory # Create runtime directory (fallback). #494: never chown/chmod the
# shared parent /run/secubox — tmpfiles.d owns it (1777 root root);
# overriding it breaks cross-user socket traversal.
mkdir -p /run/secubox mkdir -p /run/secubox
chown secubox:secubox /run/secubox
chmod 755 /run/secubox
# Create log directory # Log dir: own a subdir, do NOT chown the shared /var/log/secubox
# parent (#494/#511 — that strips other modules' log access).
mkdir -p /var/log/secubox mkdir -p /var/log/secubox
chown secubox:secubox /var/log/secubox install -d -o secubox -g secubox -m 0750 /var/log/secubox/p2p
chmod 755 /var/log/secubox
# Enable and start the service # Enable and start the service
systemctl daemon-reload systemctl daemon-reload

View File

@ -209,7 +209,10 @@ func injectInlineBanner(body []byte, scriptBody string) []byte {
// match would never fire. Mirrors the Python request() p.startswith(...) checks. // match would never fire. Mirrors the Python request() p.startswith(...) checks.
func isToolboxAssetPath(path string) bool { func isToolboxAssetPath(path string) bool {
return strings.HasPrefix(path, "/__toolbox/loader.js") || return strings.HasPrefix(path, "/__toolbox/loader.js") ||
strings.HasPrefix(path, "/__toolbox/bundle") strings.HasPrefix(path, "/__toolbox/bundle") ||
// #724 — banner R0..R3 level switch: same-origin GET from the page,
// reverse-proxied to the portal /__toolbox/set-level.
strings.HasPrefix(path, "/__toolbox/set-level")
} }
// portalTargetURL builds the absolute portal URL for an intercepted asset // portalTargetURL builds the absolute portal URL for an intercepted asset

View File

@ -1,3 +1,10 @@
secubox-toolbox-ng (0.1.15-1~bookworm1) bookworm; urgency=medium
* #724 — intercept /__toolbox/set-level (banner level switch) and reverse-proxy
it to the portal, same as /__toolbox/loader.js + /__toolbox/bundle.
-- Gerald KERMA <devel@cybermind.fr> Mon, 22 Jun 2026 15:10:00 +0000
secubox-toolbox-ng (0.1.14-1~bookworm1) bookworm; urgency=medium secubox-toolbox-ng (0.1.14-1~bookworm1) bookworm; urgency=medium
* quic/banner: strip Alt-Svc response header so browsers stop learning/preferring * quic/banner: strip Alt-Svc response header so browsers stop learning/preferring

View File

@ -1,3 +1,31 @@
secubox-toolbox (2.7.18-1~bookworm1) bookworm; urgency=medium
* #519/#522 fix(blacklist-sync): the DNS-guard domain loop aborted the whole
enforcement sync on the first unresolvable blocklisted domain — getent
returns exit 2 on NXDOMAIN and, under set -euo pipefail, the
`ips=$(getent ... | awk | sort)` assignment propagated that 2 (status=2,
INVALIDARGUMENT under systemd). Blocklisted domains are overwhelmingly
dead/sinkholed, so the oneshot failed every run → the nft blacklist_v4/v6
sets were never populated and the protection enforcement plane was inert.
Guard the substitution with `|| true` so a dead domain is skipped, not fatal.
* #519/#522 fix(blacklist-sync): a full DNS-guard sweep (~700 live resolutions)
runs ~3min on a loaded board but the unit's TimeoutStartSec was 120s →
systemd SIGTERM'd the oneshot before it loaded the sets. Raise to 600s and
drop the per-lookup timeout default 2s→1s so a sweep finishes well within it.
Verified live on gk2: sets populate (blacklist_v4=1675, blacklist_v6=207).
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 09:30:00 +0000
secubox-toolbox (2.7.17-1~bookworm1) bookworm; urgency=medium
* #724 banner: in-banner R0..R3 level switch — the injected transparency
banner now shows the real client level AND lets the client change it inline
(GET /__toolbox/set-level, reverse-proxied by sbxmitm like /__toolbox/bundle;
store.set_client_level by hash; bundle cache invalidated so it reflects at
once). r2 gated by config, r3 by wg server.pubkey.
-- Gerald KERMA <devel@cybermind.fr> Mon, 22 Jun 2026 15:10:00 +0000
secubox-toolbox (2.7.16-1~bookworm1) bookworm; urgency=medium secubox-toolbox (2.7.16-1~bookworm1) bookworm; urgency=medium
* fix: restore banner on heavy sites (leparisien.fr). The #685 stream_large_bodies=1m * fix: restore banner on heavy sites (leparisien.fr). The #685 stream_large_bodies=1m

View File

@ -61,14 +61,18 @@ fi
# Bounded : cap on domains/cycle + per-lookup timeout so the sync never # Bounded : cap on domains/cycle + per-lookup timeout so the sync never
# hangs on a dead resolver. # hangs on a dead resolver.
DOMAIN_CAP="${SECUBOX_BL_DOMAIN_CAP:-2000}" DOMAIN_CAP="${SECUBOX_BL_DOMAIN_CAP:-2000}"
RESOLVE_TIMEOUT="${SECUBOX_BL_RESOLVE_TIMEOUT:-2}" RESOLVE_TIMEOUT="${SECUBOX_BL_RESOLVE_TIMEOUT:-1}"
resolved_domains=0 resolved_domains=0
if [ -r "$TOOLBOX_DB" ] && command -v sqlite3 >/dev/null 2>&1; then if [ -r "$TOOLBOX_DB" ] && command -v sqlite3 >/dev/null 2>&1; then
while IFS= read -r dom; do while IFS= read -r dom; do
[ -n "$dom" ] || continue [ -n "$dom" ] || continue
# getent ahosts returns both A + AAAA ; timeout guards a dead lookup. # getent ahosts returns both A + AAAA ; timeout guards a dead lookup.
# NXDOMAIN makes getent exit 2 → with pipefail+set -e the assignment
# would abort the whole sync on the first dead blocklisted domain
# (and blocklisted domains are overwhelmingly dead/sinkholed). Guard
# the substitution so an unresolvable domain is simply skipped.
ips=$(timeout "$RESOLVE_TIMEOUT" getent ahosts "$dom" 2>/dev/null \ ips=$(timeout "$RESOLVE_TIMEOUT" getent ahosts "$dom" 2>/dev/null \
| awk '{print $1}' | sort -u) | awk '{print $1}' | sort -u || true)
if [ -n "$ips" ]; then if [ -n "$ips" ]; then
printf '%s\n' "$ips" >> "$TMP4.raw" printf '%s\n' "$ips" >> "$TMP4.raw"
resolved_domains=$((resolved_domains + 1)) resolved_domains=$((resolved_domains + 1))

View File

@ -78,6 +78,39 @@ async def toolbox_bundle(mh: str = Query(default=""), wg: int = Query(default=0)
) )
@router.get("/__toolbox/set-level")
async def toolbox_set_level(mh: str = Query(default=""), level: str = Query(default="")) -> JSONResponse:
"""#724 — banner self-service level switch. Reverse-proxied to the portal by
sbxmitm (same-origin from the page) like /__toolbox/bundle, so it carries no
cookies/CSRF and is identified by the client's baked ``mh`` hash. Persists the
analysis tier for that hash (R3 wg peers have no captive MAC, so this is the
by-hash setter, not the nft-based /change-level)."""
mh = (mh or "").strip().lower()
level = (level or "").strip().lower()
if not (mh and all(c in "0123456789abcdef" for c in mh) and 8 <= len(mh) <= 64):
return JSONResponse({"ok": False, "error": "bad mh"}, status_code=400,
headers={"Cache-Control": "no-store"})
if level not in ("r0", "r1", "r2", "r3"):
return JSONResponse({"ok": False, "error": "bad level"}, status_code=400,
headers={"Cache-Control": "no-store"})
# honour the same gates as /change-level
try:
cfg = _get_cfg()
if level == "r2" and not cfg.r2.enabled:
level = "r1"
except Exception:
pass
if level == "r3" and not Path("/etc/secubox/toolbox/wg/server.pubkey").exists():
level = "r1"
try:
store.set_client_level(mh, level)
bundlemod.invalidate(mh) # drop cached bundle so the new level shows at once
except Exception as e: # pragma: no cover
return JSONResponse({"ok": False, "error": str(e)}, status_code=500,
headers={"Cache-Control": "no-store"})
return JSONResponse({"ok": True, "level": level}, headers={"Cache-Control": "no-store"})
@router.get("/__toolbox/inline") @router.get("/__toolbox/inline")
async def toolbox_inline( async def toolbox_inline(
mh: str = Query(default=""), mh: str = Query(default=""),

View File

@ -95,6 +95,14 @@ def build_bundle(client_id: str, is_wg: bool = False) -> dict:
} }
def invalidate(client_id: str) -> None:
"""#724 — drop a client's cached bundle (both wg variants) so a level switch
is reflected on the next banner render without waiting for the TTL."""
cid = client_id or ""
for k in ((cid, True), (cid, False)):
_cache.pop(k, None)
def get_bundle(client_id: str, is_wg: bool = False) -> dict: def get_bundle(client_id: str, is_wg: bool = False) -> dict:
"""Return the cached bundle for a client, rebuilding past the TTL. Fail-open.""" """Return the cached bundle for a client, rebuilding past the TTL. Fail-open."""
try: try:
@ -163,6 +171,35 @@ _BANNER_CORE = r"""
var c = document.getElementById("sbx-ck"); var c = document.getElementById("sbx-ck");
if (c) c.textContent = "🍪 " + countCookies() + " cookies"; if (c) c.textContent = "🍪 " + countCookies() + " cookies";
} }
// #724 — inline R0..R3 level switch. Shows the real current level (highlighted)
// and lets the client change it: GET /__toolbox/set-level (same-origin, the Go
// engine reverse-proxies it to the portal), then reload so the new tier applies.
function lvlSwitch(b){
var cur = String(b.level || "r1").toLowerCase();
var lv = ["r0","r1","r2","r3"], out = "<span id=\"sbx-lvl\" title=\"Niveau d'analyse — clique pour changer\">";
for (var i=0;i<lv.length;i++){ var on = lv[i]===cur;
out += "<button data-lvl=\"" + lv[i] + "\" class=\"sbx-lvl\" style=\"background:"
+ (on?"#148C66":"transparent") + ";color:" + (on?"#0A0E14":"#8A9AA8")
+ ";border:1px solid #148C66;border-radius:3px;padding:0 5px;margin:0 1px;"
+ "font:inherit;font-size:11px;cursor:pointer\">" + lv[i].toUpperCase() + "</button>";
}
return out + "</span>";
}
function wireLevels(bar, b){
var els = bar.querySelectorAll(".sbx-lvl");
for (var i=0;i<els.length;i++){ (function(el){
el.onclick = function(){
var lvl = el.getAttribute("data-lvl");
var who = (typeof mh !== "undefined" && mh) ? mh : (b.client_id || "");
if (!who) return;
el.textContent = "";
fetch("/__toolbox/set-level?mh=" + encodeURIComponent(who) + "&level=" + lvl,
{credentials:"omit", cache:"no-store"})
.then(function(r){ if (r && r.ok) { location.reload(); } else { el.textContent = lvl.toUpperCase(); } })
.catch(function(){ el.textContent = lvl.toUpperCase(); });
};
})(els[i]); }
}
function render(b){ function render(b){
if (dismissed) return; if (dismissed) return;
if (document.getElementById("sbx-banner")) return; if (document.getElementById("sbx-banner")) return;
@ -184,7 +221,7 @@ _BANNER_CORE = r"""
bar.innerHTML = "<b style=\"color:#148C66\">SecuBox</b>" bar.innerHTML = "<b style=\"color:#148C66\">SecuBox</b>"
+ cspProof + cspProof
+ tor + tor
+ "<span>" + esc((b.level || "r1").toUpperCase()) + "</span>" + lvlSwitch(b)
+ "<span id=\"sbx-trk\">🛰️ " + trk + " trackers</span>" + "<span id=\"sbx-trk\">🛰️ " + trk + " trackers</span>"
+ "<span id=\"sbx-ck\">🍪 " + ck + " cookies</span>" + "<span id=\"sbx-ck\">🍪 " + ck + " cookies</span>"
+ pin + pin
@ -192,7 +229,8 @@ _BANNER_CORE = r"""
+ "<button aria-label=\"dismiss\" style=\"background:none;border:0;color:#8A9AA8;cursor:pointer;font-size:14px\">✕</button>"; + "<button aria-label=\"dismiss\" style=\"background:none;border:0;color:#8A9AA8;cursor:pointer;font-size:14px\">✕</button>";
document.body.appendChild(bar); document.body.appendChild(bar);
try { document.body.style.paddingTop = (bar.offsetHeight || 34) + "px"; } catch (_) {} try { document.body.style.paddingTop = (bar.offsetHeight || 34) + "px"; } catch (_) {}
var btn = bar.querySelector("button"); wireLevels(bar, b);
var btn = bar.querySelector("button[aria-label=\"dismiss\"]");
if (btn) btn.onclick = function(){ dismissed = true; try { document.body.style.paddingTop = ""; } catch (_) {} bar.remove(); }; if (btn) btn.onclick = function(){ dismissed = true; try { document.body.style.paddingTop = ""; } catch (_) {} bar.remove(); };
} }
// ensure(): (re)render the banner if it's absent and the bundle is loaded and // ensure(): (re)render the banner if it's absent and the bundle is loaded and

View File

@ -251,6 +251,27 @@ def upsert_client(mac_hash: str, ip: str, level: str = "r1") -> None:
) )
def set_client_level(mac_hash: str, level: str) -> None:
"""#724 — set a client's level by hash only (no ip needed), for the banner
self-service switch on R3 wg peers (which have no captive MAC/ip). Updates the
existing row; inserts a minimal row if the client is unknown."""
now = int(time.time())
with _conn() as c:
try:
c.execute("ALTER TABLE clients ADD COLUMN level TEXT NOT NULL DEFAULT 'r1'")
except sqlite3.OperationalError:
pass
cur = c.execute("UPDATE clients SET level=?, last_seen=? WHERE mac_hash=?",
(level, now, mac_hash))
if cur.rowcount == 0:
c.execute(
"INSERT INTO clients(mac_hash, ip, level, first_seen, last_seen) "
"VALUES (?,?,?,?,?) ON CONFLICT(mac_hash) DO UPDATE SET "
"level=excluded.level, last_seen=excluded.last_seen",
(mac_hash, "?", level, now, now),
)
def get_client_level(mac_hash: str) -> str: def get_client_level(mac_hash: str) -> str:
"""Returns 'r0' | 'r1' | 'r2'. Default 'r1' if not found.""" """Returns 'r0' | 'r1' | 'r2'. Default 'r1' if not found."""
try: try:

View File

@ -14,7 +14,11 @@ ExecStart=/usr/sbin/secubox-blacklist-sync
User=root User=root
Nice=10 Nice=10
IOSchedulingClass=idle IOSchedulingClass=idle
TimeoutStartSec=120 # DNS-guard resolves up to DOMAIN_CAP blocklisted domains sequentially; on a
# loaded board that can run a few minutes. 120s was shorter than a full sweep
# (~3min for ~700 live resolutions) → systemd SIGTERM'd the oneshot before it
# loaded the sets. Give it headroom (#519/#522).
TimeoutStartSec=600
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -1,3 +1,15 @@
secubox-wazuh (1.0.1-1~bookworm1) bookworm; urgency=medium
* #53 fix: stop the 100% CPU spin on boards without Wazuh. The uvicorn worker
was started unconditionally even though SecuBox's IDS/IPS stack is
Suricata + CrowdSec and /var/ossec is absent, so it busy-looped for nothing.
Gate the unit with ConditionPathExists=/var/ossec/etc/ossec.conf — systemd
now reports "inactive (condition failed)" and never starts it unless a local
Wazuh agent/manager is installed. Add RestartSec=5 as a hot-respawn guard.
Module kept (opt-in SIEM integration), not removed.
-- Gerald KERMA <devel@cybermind.fr> Wed, 24 Jun 2026 10:00:00 +0000
secubox-wazuh (1.0.0-1) stable; urgency=low secubox-wazuh (1.0.0-1) stable; urgency=low
* Initial release * Initial release

View File

@ -1,6 +1,13 @@
[Unit] [Unit]
Description=SecuBox Wazuh API Description=SecuBox Wazuh API
After=network.target secubox-core.service After=network.target secubox-core.service
# #53: this API only does anything when a local Wazuh agent/manager is present
# (/var/ossec, port 55000). SecuBox's documented IDS/IPS stack is Suricata +
# CrowdSec, so on every normal board Wazuh is absent and the uvicorn worker just
# burned 100% CPU for nothing. Gate the unit on Wazuh actually being installed:
# systemd reports "inactive (condition failed)" and never starts it otherwise.
# Remote-API-only integrations (no local ossec) should drop this condition.
ConditionPathExists=/var/ossec/etc/ossec.conf
[Service] [Service]
Type=simple Type=simple
@ -9,6 +16,8 @@ WorkingDirectory=/usr/lib/secubox/wazuh
ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/wazuh.sock --log-level warning ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/wazuh.sock --log-level warning
UMask=0117 UMask=0117
Restart=on-failure Restart=on-failure
# Defensive: never hot-respawn (the default ~100ms churns CPU on its own).
RestartSec=5
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -50,6 +50,12 @@ fi
cd "\$SITE" cd "\$SITE"
# #121: every git op below runs as root (ssh root@host), so a freshly created
# .git ends up root:root — but metablogizer runs as 'secubox' and must be able
# to write the repo (webhook deploys, sub-E #113). Re-own the site dir after any
# ingest that touches .git. Matches the chown pattern in module postinsts.
fix_perms() { chown -R secubox:secubox "\$SITE" 2>/dev/null || true; }
# Determine remote HEAD (might fail if repo doesn't exist yet — that's OK) # Determine remote HEAD (might fail if repo doesn't exist yet — that's OK)
remote_head=\$(git ls-remote "\$REPO_URL" main 2>/dev/null | awk '{print \$1}' || true) remote_head=\$(git ls-remote "\$REPO_URL" main 2>/dev/null | awk '{print \$1}' || true)
@ -76,6 +82,7 @@ if [[ -d .git ]]; then
fi fi
git tag -f v1.0.0 git tag -f v1.0.0
git push --quiet --force origin v1.0.0 git push --quiet --force origin v1.0.0
fix_perms
echo "ingested-with-history" echo "ingested-with-history"
exit 0 exit 0
fi fi
@ -95,6 +102,7 @@ if [[ -d .git ]]; then
git push --quiet origin main git push --quiet origin main
git tag v1.0.0 git tag v1.0.0
git push --quiet origin v1.0.0 git push --quiet origin v1.0.0
fix_perms
echo "ingested-fresh" echo "ingested-fresh"
exit 0 exit 0
else else
@ -113,6 +121,7 @@ else
git push --quiet origin main git push --quiet origin main
git tag v1.0.0 git tag v1.0.0
git push --quiet origin v1.0.0 git push --quiet origin v1.0.0
fix_perms
echo "ingested-fresh" echo "ingested-fresh"
exit 0 exit 0
fi fi