secubox-deb/.claude/HISTORY.md
CyberMind-FR c2c1d0e008
Some checks are pending
License Headers / check (push) Waiting to run
docs: APK rebuilt + republished (SW v11, peertube, upload, 401 re-login)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 11:12:06 +02:00

378 KiB
Raw Blame History

HISTORY — SecuBox-DEB Migration Log

Tracking completed milestones with dates


2026-07-17 — Session auditing + Companion auth/system/metrics + billets emoji hashtags (branch feat/sessions-companion-emoji)

Sessions became auditable, the two stub Companion modules became real, and billets gained emoji-hashtag quick views. All live-verified on gk2.

  • Session logging (root-caused) 🔑/login/mfa and /totp/confirm hardcoded "ip": "" and dropped user_agent; only the password path captured them. admin is forced-TOTP, so every real admin login took the MFA path → every session row was unauditable and the users webui sessions tab (which already existed, fully built) rendered blanks. Fix: _client_meta(request) (X-Forwarded-For first — nginx/HAProxy front every login, so request.client.host is only ever the proxy) on all three paths. Proven by driving the real /login/mfa handler in-process with SECUBOX_AUTH_SESSIONS on a temp file: records ip=192.168.1.77 (first XFF hop) + full UA.
  • Companion auth + system ⚙️ — both were 34-line stubs guessing routes (/api/v1/auth/users404; identities are under /api/v1/users). Rebuilt against routes read from handler source. auth shows identities + sessions (who/IP/device/age/current); system shows status/metrics/resources/health_score/network/security, each section degrading independently.
  • Emoji metrics 📊core/metrics.js: per-kind severity thresholds (disk strictest, cpu tolerant) so gk2's disk 94.5% reads 🔴 while cpu 10% reads 🟢; plain language ("Memory 75% — 5.5 GB of 7.7 GB used") + byte/uptime formatters.
  • Billets emoji hashtags 🏷️ — migration 0004 (tag + billet_tag, indexed); tags extracted from the body (#secu) so authoring costs nothing and applies retroactively — the backfill tagged 37 of 46 existing billets. Accent-folded slugs (#Réseau==#reseau); emoji stored (no silent restyling of published billets); URL anchors don't become tags; unknown tags get a neutral badge. Quick views via ?tag= on / + /feed.json (EXISTS not JOIN, so keyset paging can't duplicate rows; pager carries the tag). feed.json gains standard summary+tags with emoji in the _secubox extension.
  • Feed resumes 📄 — long billets show a short excerpt + "Lire la suite"; short ones render whole (is_long measured on markdown-collapsed text). 16/20 resumed on the live feed.
  • GET /admin/api/billets — the authoring list. The public feed can't serve it: its item id is a permalink URL (why Edit/Delete 404'd) and it hides drafts.
  • Companion 401 re-login 🔓 — box tokens live 24h but the Companion seals one at pairing and reuses it forever → a day later every authed call 401'd with no escape but unpairing ("Failed: unauthorized" in billets + podcaster). api.js now triggers re-auth and replays the request once, single-flight (one prompt, not one per request), so an in-flight write survives. SW v10.

APK rebuilt + republished 📱 — the shipped APK (16/07) bundled pre-fix code (SW v5, the /feed 404, no upload, no peertube); Capacitor bundles www, so no service worker could ever refresh it. Rebuilt with the tracked android/ scaffold (JAVA_HOME=Android Studio JBR — the system JDK has no jlink; cap copy not cap sync; gradle 8.9-bin, not the default 8.7-all; TLS was not MITM-intercepted this run so no truststore needed). Debug signer digest is unchanged (5e6163f9…) → installs over the old app, no uninstall. Live at https://companion.gk2.secubox.in/secubox-companion.apk (3.9 MB, carries SW v11 + peertube). Previous APK kept as .apk.prev.

Follow-ups: rebuild the billets .deb (migration 0004 + tags.py); consider a token refresh so re-login isn't needed every 24h.


2026-07-17 — Billets JWT admin surface + Companion authoring & image upload (branch fix/companion-billets-feed)

Made the SecuBox Companion a working authoring client for the billets micro-blog — the last gap ("New billet missing upload", comment moderation 404).

  • billets JWT admin API — new packages/secubox-billets/api/routes/jwt_admin.py: a JWT-authed JSON surface (secubox_core.auth.require_jwt — Bearer OR secubox_session cookie) parallel to the session HTML admin, reusing the SAME repo/media layer. POST/PUT/DELETE /admin/api/billets, POST /admin/api/billets/{id}/media (multipart, EXIF-strip via services.media), GET /admin/api/comments, approve/delete moderation. No-op if secubox_core is absent (isolated tests). Wired into api/main.py via register_jwt_admin.
  • venv import fix (packaged) — the isolated billets venv couldn't import the .deb-managed secubox_core; postinst now drops a sorted-last zz-secubox-system.pth appending /usr/lib/python3/dist-packages (venv wheels keep precedence — fastapi/pydantic NOT shadowed). Backports the live board hotfix.
  • Companion billets module — EP map repointed at /feed.json + /admin/api/*; editor gained an image file input that uploads after billet creation (multipart to /admin/api/billets/{id}/media); SW bumped v6→v7.
  • Verified live on gk2 — minted a JWT as the secubox user (matching the service's _secret() config resolution) and ran the full round-trip: create → feed (30 items), valid PNG upload → {url, thumb} (a corrupt PNG was correctly 415'd, proving media.process runs), delete — all 200. Route probes: /admin/api/billets 405 (POST-only), /admin/api/comments 401 (JWT-gated) = both registered.

Follow-ups: merge fix/companion-billets-feed; rebuild the billets .deb to ship the postinst .pth on fresh installs.


2026-07-17 — WAF webui + autoban-to-firewall fix + efficiency analysis + Nextcloud rework (PR #866, #867)

Reworked two dashboards to the cyan hybrid-skin, fixed a major WAF control gap, and repaired the Nextcloud module — all live-verified on gk2.

  • WAF webui rework (merged earlier to master) — sbxwaf dashboard restyled to the /certs/ cyan hybrid-skin (emoji cards, live pulse, 30s/10s reactive), netifyd/crt-light cruft dropped; every endpoint preserved. Renders the live 198K-threat data.
  • WAF efficiency analysis — detection strong (198K threats, 17 categories) but CONTROL weak: only 2 IPs actually banned. Bans were app-level (403 page) not firewall-level, so offenders kept hammering (one IP 36,881×; top /24 = 26% of volume). ~26% of pattern-matching (voip/xmpp/cve_voip/cve_xmpp = 39/149) is dead weight on HTTP. Ranked fixes presented.
  • WAF autoban→firewall fix (PR #866) — the CrowdSec bridge was silently OFF: the unit passes --crowdsec-url but no --crowdsec-jwt-file, so main.go hit the 'disabled' branch and Report() was never called; the LAPI /v1/alerts path is doubly broken (hourly JWT + 500 on our schema). Added CscliReporter — the proven path the manual dashboard ban uses (cscli decisions add → real bouncer nft drop) — engaging when --crowdsec-url is set without a JWT file (new --crowdsec-cscli flag). Per-IP 5-min dedup collapses the storm (graduated ban fires Report on every banned request → dozens of concurrent cscli procs signal: killed). Rebuilt arm64 + deployed (mv over /usr/sbin/sbxwaf, systemctl restart secubox-waf-ng). Verified: 5 honeypot hits from a test IP → exactly 1 decision, no storm; a live honeypot offender got a real secubox-waf/honeypot decision.
  • Nextcloud rework (PR #867) — the /nextcloud/ webui showed Stopped/empty because the status probe was fully broken (same unprivileged-LXC pattern as mail/dpi): ran lxc-info/occ WITHOUT sudo + lxc_path /srv/lxc vs real /data/lxc (fixing the path alone then 500'd on a PermissionError); the only privileged surface is sudo nextcloudctl; and NoNewPrivileges=yes blocked sudo (the wireguard gotcha). Fix: privilege-free TCP port probe for liveness, PermissionError-tolerant installed, all container ops via sudo nextcloudctl (occ passthrough), a daemon-thread 60s cache for the slow occ fields (@app.on_event("startup") wouldn't fire under uvicorn --uds), real data_path + public web_url. Webui restyled to cyan hybrid-skin (subagent, 361 lines, endpoints preserved). Verified: running, v32.0.10, 2 users, 5.8G, nc.gk2.secubox.in. Board drift: NNP drop-in + [nextcloud] domain in secubox.conf.

Follow-ups: WAF efficiency #24 (subnet/ASN bans, skip voip/xmpp on HTTP, stop logging banned IPs); backport the Nextcloud NNP drop-in + secubox.conf domain; #862#864 still open.


2026-07-16 — Mail stack overhaul + systemic auth fix + DPI exfil/regen (PR #865, #862#864)

Session multi-fix, tout live-vérifié sur gk2, portable committé et mergé via PR #865.

  • Auth (systémique, fleet-wide)secubox_core.auth.require_jwt utilisait un Bearer présent-mais-périmé exclusivement, ignorant un cookie de session valide → 401 → boucle login sur chaque panel (le "comme à chaque fois"). Fix : tenter Bearer puis cookie ; 401 seulement si aucun ne valide (strictement plus permissif, zéro régression). Prouvé : Bearer périmé + cookie valide → 200. Un seul fichier partagé, tout le fleet au prochain restart.
  • Mail — reset à gk2-only (3 users cassés + maildirs {cur,new,tmp} littéraux supprimés) ; mailbox réparée = ownership idmap (maildir host-5000nobody dans le conteneur ; chown 105000, base idmap 100000) ; submission 587/465 activée (SASL dovecot + vrai cert /etc/ssl/mail) → webmail peut envoyer ; dashboard status via probes TCP (lxc-info lisait le mauvais lxc_path → "Stopped" faux ; storage lisait /mail au lieu de /vmail) ; domaine secubox.localgk2.secubox.in (config dans secubox.conf [mail], pas le mail.toml vestigial) ; webui reskin cyan hybrid-skin (stat-cards emoji, pulse live, onglet External, plus de scanlines) ; /user/password corrigé (écrivait une copie host non lue par dovecot → reset admin no-op) ; Roundcube : plugin password self-service (helper chpasswd → passwd-file + sudoers) + ident_switch vendored (Gecka-Apps) pour comptes externes Gmail/IMAP.
  • DPI/exfil sert le rollup cumulatif (7 devices) au lieu du state.json live vide → "no devices in R3" corrigé ; bloc engine-liveness ; contrôle service repointé de netifyd dormant vers le vrai collecteur Go secubox-dpi-flowcap (sudoers scellé, postinst) ; webui régénéré cyan, cruft netifyd/mirred/block-rules retiré, rendu media-buffer XSS-safe préservé.
  • openclaw — CT lookup : fallback certSpotter quand crt.sh est down (dumpait du HTML brut) ; whois résout le domaine registrable pour les sous-domaines.
  • ACME HTTP-01 — câblé live (nginx :8880 activé + route HAProxy /.well-known/acme-challenge/ → backend acme) ; l'émission LE ne marchait pour aucun vhost WAF-routé. Certs émis pour live.maegia.tv + mail.maegia.tv.

Follow-ups tracés : #862 (provisioning mail : submission SASL, chown idmap, users.sh maildir, self-service helper), #863 (ACME backport nginx/HAProxy/certs-webroot), #864 (vendorer ident_switch dans le paquet). WAF webui rework + analyse triggers/control en cours.


2026-07-12 — metalogue: Maltego-style OSINT suite as LXC modules (#845)

Wanted Maltego-style OSINT (collect → correlate → dossier). Maltego is commercial/cloud → built the FOSS stack as SecuBox LXC modules, wired collectors → hub. Analyzed github.com/topics/osint; picked Maigret (identity collector) + SpiderFoot (automation + correlation graph) for gk2, deferred OpenCTI (the true graph hub) to a beefier node (its Elasticsearch/Redis/RabbitMQ/MinIO stack would OOM the arm64 board, ~1.5 GB free). Built subagent-driven (implementer → adversarial review → fix per module), following the openclaw LXC pattern. Merged #846#850, deployed and installed on gk2.

  • secubox-maigret — username → dossier across 3000+ sites. LXC 10.100.0.42, CLI wrapped by a host FastAPI (async job+poll, concurrency cap 3 → 429, passive-only, JWT + append-only audit). maigretctl privileged control plane: username passed ONLY positionally to lxc_attach (no shell interpolation), flag-injection guards, __guard test verb. Installed + functional (a real torvalds lookup returned GitHub/Instagram/Telegram/Facebook).
  • secubox-spiderfoot — SpiderFoot (200+ passive modules + correlation graph = interim Maltego-style hub) in an LXC (10.100.0.43), in-container systemd unit binding sf.py to the container LAN IP only. Its UI emits absolute /static paths so it can't live under a portal subpath (admin.gk2/spiderfoot-ui/ → the SPA's "module not found"); served at root on its own vhost https://spiderfoot.gk2.secubox.in/ (zigbee/lyrion pattern: nginx :9080 vhost + sbxwaf route + HAProxy ACL → mitmproxy_inspector + LAN-gated exposure snippet, wildcard *.gk2 cert) + a :9043 LAN-direct fallback. Review HIGH fixed: SpiderFoot has no native auth → every nginx location auth-gated (auth_request /__sbx_auth_verify, peer pattern).
  • Stylized PDF report (maigret) — raw --json simple → a FORMAL SecuBox dossier (api/report.py, fpdf2): dark masthead, document metadata block (REFERENCE/SUBJECT/CLASSIFICATION), §1 Summary, §2 Findings as a coherent results table (#/platform/category/profile/details, zebra rows, category colour-coding, paginated). GET /lookup/{id}/report.pdf (JWT, plain def → threadpooled so the render never blocks the aggregator loop) + 📄 PDF panel button (authed fetch → blob). 12 tests.
  • Two root causes fixed during install: (1) net.ipv4.ip_forward had drifted to 0 on gk2 → every LXC (incl. openclaw) had zero internet egress; restored via sysctl --system (the 99-secubox-zz-lxc-forward.conf drop-in intends 1). (2) maigret/SpiderFoot deps (lxml, pandas, cryptography…) have no stock arm64 wheels → minbase LXC can't compile → piwheels (--extra-index-url) + dev-header fallback; 17-min-fail → ~2-3 min. Backported to both <mod>ctl.
  • All handlers plain def, workers detached — no board-wide loop-blocking SPOF. Aggregator registry (aggregator.toml) hand-edited (like openclaw); package self-registration remains a follow-up.

2026-07-11 — MetaBlogizer Publisher Wizard (#832): publish is fixed end-to-end

The publish ecosystem was broken: zem.gk2 answered a bare 421, droplet PermissionError'd on /etc/secubox/droplet.toml, and secubox-publish wrote /etc/haproxy+/etc/nginx directly as unprivileged secubox (silent failure). Root cause of the 421: metablogizer's route-sync wrote the retired mitmproxy-LXC file, not the live sbxwaf host file.

Built a guided Publisher Wizard in secubox-metablogizer (subagent-driven, 11 tasks, TDD, 63 tests): upload zip/html (zip-slip guarded) → gitea version → sbxwaf host-file route + advisory HAProxy vhost via a new audited root helper secubox-publishctl (tight sudoers) → cert (wildcard *.gk2 / certbot custom) → portable .sbxsite backup (git bundle + manifest, traversal-safe import). secubox-publish now delegates to a new metablogizer POST /publish/route (no more illegal root writes); droplet.toml owned secubox:secubox 0640.

Live-verified on gk2: publish → ok:True, https://wiztest3.gk2.secubox.in/200 (was 421), route landed ["192.168.1.200",8900] in /etc/secubox/waf/haproxy-routes.json, backup export→import round-trip recreated the site. Live-driven fixes: real haproxyctl vhost add <domain> <backend> signature; route_ok gates on the WAF route (vhost advisory — blocked by haproxyctl drift-guard + redundant with default_backend mitmproxy_inspector); portable safe tar-extract (board is Python 3.11.2, no filter=). Whole-branch review: READY TO MERGE, all 5 security invariants held. metablogizer 1.3.0.


2026-07-11 — Admin-TOTP toggle from webui + per-user 2FA reset (default OFF)

User: "totp disableable by options from webui and default absent ok" / "or permit totp reset from webui". Two iPhone login failures traced to admin accounts being unconditionally forced into TOTP while login.html has no TOTP UI. Immediate unblock was a non-admin operator account; the durable fix makes the requirement opt-in.

secubox-auth 1.0.4require_admin_totp now defaults OFF so a node stays reachable. New admin-gated GET/POST /api/v1/auth/settings persist the flag to /etc/secubox/auth-runtime.json. Login enforcement reads _get_require_admin_totp() (runtime file > [auth] config > default False) and is fail-open — a config read error no longer locks admins out. Verified live on gk2: default-off admin login returns access_token; toggle on→enrollment_required, off→session; non-admin POST 403. Tests realigned from the old default-ON/fail-secure contract to the reachability-first one, fixture isolates the config + runtime paths (10 passing).

secubox-users 1.4.3/users/ gained an "Admin 2FA: ON/OFF" toolbar toggle (wired to the auth /settings endpoint) and a per-user "🔑 Reset 2FA" button (POST /user/<u>/totp/disable), shown only for TOTP-enrolled users. Deployed live.


secubox-portal 2.2.3login.html now completes the TOTP flow in-browser: the login response branches to a code step (mfa_required), a QR + manual-key enrollment step (enrollment_required/totp/enroll + /totp/confirm) with one-time backup-code display, or straight through on access_token. So turning the toggle ON is now fully usable from a phone. Verified live end-to-end on gk2 (enroll→confirm→re-login→mfa, 10 backup codes).


2026-07-10 — wg-toolbox VPN surf blackhole after reboot (nft drop-in aborted every boot)

User: "surf stopped when wg-toolbox VPN activated". Root cause was NOT the peer prune (restored the 540 from backup as a precaution) but the wg-toolbox nft DNAT missing entirely. nftables.service was failed: the persistent drop-in /etc/nftables.d/secubox-toolbox-wg.nft (+ zz-fanout) aborts the whole atomic include load at boot for two reasons — (1) it added the UDP-51820 accept to inet filter input, a table that does not exist on gk2 at that point; (2) it used iif "wg-toolbox", which resolves an interface index at LOAD time and fails because nftables.service runs before wg-quick@wg-toolbox creates the iface. Either abort → table inet wg-toolbox never applied → all tunnel traffic blackholed → no surf. Fix (source + board): fold the handshake accept into the wg-toolbox table's own input base chain (self-contained, no inet filter dependency) and switch to iifname/oifname (per-packet name match, load-order independent). nft -c -f /etc/nftables.conf now passes. Restored runtime live via secubox-toolbox-wg-provision + fanout (DNAT 443/80 → round-robin 10.99.1.1:8091-8094, the 4 live Go sbxmitm ng-workers). Did NOT restart nftables.service (its flush ruleset would wipe live crowdsec/waf/lxc tables); reset-failed instead, next boot loads clean.

2026-07-10 — WireGuard webui full rewrite + status/peers perf avalanche fix (secubox-wireguard 1.0.2)

/wireguard/ was inoperative ({"interfaces":[" truncated) and noisy. Root causes, fixed end-to-end on gk2 + source:

  • Privilege: the API (user secubox) reads WG state via wgctl, which needs root (wg show dump exposes private keys). Added a sudo -n /usr/sbin/wgctl route + /etc/sudoers.d/secubox-wireguard (wgctl + wg show * only) with visudo -cf guard in postinst. The service had NoNewPrivileges=true which blocks sudo → flipped to false in the packaged unit (same tradeoff as the aggregator).
  • Perf avalanche (the real incident): wgctl status/peers ran ~8 wg show | grep subprocesses per peer. On the 542-peer wg-toolbox transparent-proxy tunnel that took 30s+ and — because asyncio.wait_for in _run_ctl never killed the timed-out child — every slow /peers left a churning wgctl. They piled up to load avg 56 and starved the whole board (cached /status still slow). Fixes: rewrote both as single awk / wg show <iface> dump passes (O(interfaces); 542 peers now 0.07s, was >30s), added proc.kill() on timeout, and a single-flight + short-TTL cache on the read endpoints (invalidated after up/down/peer add/remove).
  • Security (review-caught): the sudo route activated a dormant bug in _parse_wg_show — it parsed wg show all dump with the wrong layout, emitting a truncated private key via /stats,/summary, /peers/status and dropping all peers. Rewrote to parse by field count (5=iface/9=peer) and never read the private key.
  • webui: full rewrite — interface-card dashboard (role-labelled tunnels 🧰🕸️🛡️, live status, lazy per-interface peer drawers with the 542-peer tunnel gated behind "Load anyway", up/down + add-peer/QR), certs hybrid-skin + shared sidebar navbar (already registered in menu.d, category mesh). Actions use data-* + event delegation (no name interpolated into inline handlers).
  • Also backported the post-reboot netplan fix to source board/mochabin/netplan/00-secubox.yaml (WAN=eth2 DHCP metric100; lan0-3/eth1 optional; drop the stray lan3 static that wedged boot).

Two-stage subagent review ran on the diff; both findings (private-key disclosure, onclick breakout) fixed before the durable .deb install. Board recovered: load 56→4, all vhosts 200. Commits local on master (unpushed).

Follow-ups same day: (1) 1.0.3 — large tunnels render connected-only peers by default (active/recent handshake) with a "Show all" toggle instead of suppressing. (2) Pruned wg-toolbox peers: it had 543 runtime peers but 540 had never handshaked (phantom enrollments — the R3 transparent-proxy enrolls one wg peer per client browser-UA into /var/lib/secubox/toolbox/wg-peers.json, re-applied on boot by secubox-toolbox-wg-restore; never-connected ones accumulate). Backed up the store, pruned runtime + store to the 3 ever-handshaked live clients (wg set … peer … remove in one call + JSON filter). Open item: no GC prunes these automatically → they will re-accumulate; a secubox-toolbox timer that drops peers with no handshake older than N days is the durable fix (not yet built).


2026-07-09 — OpenClaw OSINT scanner: LXC module live end-to-end on gk2 (branch feature/openclaw-lxc-scanner)

8-task subagent-driven build (async-job scan endpoints dropping the old sync-shell-out machinery, target-policy/audit gate, packaging with sudoers+visudo, aggregator-in-process wiring, XSS-hardened dashboard) — Task 8 deployed + verified. secubox-openclaw 1.0.1-1~bookworm1 built and installed on gk2 over the existing 1.0.0 (LXC container openclaw/10.100.0.41 with nmap/dig/whois/curl already provisioned in earlier tasks reused as-is). sudo -u secubox sudo -n openclawctl status --json proves the sudoers grant; secubox-aggregator restarted once to load the new in-process api/main.py. SPOF proof: kicked a real openclawctl scan ip 127.0.0.1 in the background — a concurrent /api/v1/cookies/status call through the aggregator socket returned in 6ms, and the scan itself reached status: completed, confirming the async-job design does not block the shared aggregator loop the way the old sync-shell-out path would have. Dashboard (/openclaw/) verified 200 with wired markup via the generic static-root fallback (no per-module nginx alias needed — same pattern as /cookies/). Board confirmed healthy after the single restart (all sampled vhosts/APIs 200/401, no 502).


2026-07-06/07 — Sentinel threat engine + activation + 3 surfaces + C2 auto-learning (#821 #823#828)

Brainstorm → spec → plan → subagent-driven (per-task two-stage review + adversarial whole-branch review) throughout. All merged; all deployed + verified live on gk2.

  • #821 / #822 — Frigate NVR foundation MERGED. podman-in-LXC (amd64/OpenVINO), go2rtc, /api/v1/frigate/* shim, WAF-fronted no-bypass.
  • #823 / #824 — sbxmitm Sentinel engine MERGED (dark). Inline IOC gate + async sbx-sentinel daemon (bbolt store, YARA cgo build-tag + no-cgo stub), commercial-spyware base packs (Pegasus/Predator/Intellexa) + live-feed overlay, FinalizeAction report-only guard.
  • #825 — Sentinel activation + 3 surfaces MERGED (PR #825). Activated on gk2 (daemon enabled, SENTINEL_HTTP_ADDR=127.0.0.1:8790, worker mirror wired). Surfaced on the WebUI ToolBoX 🛡️ Sentinelle fleet tab, the kbin per-device Compromission report tab, and the PDF report section — all report-only, mac_hash-only, fail-safe. Fixed a RuntimeDirectory=secubox clobber of the shared /run/secubox (drop-in). Proven e2e (Pegasus/Predator).
  • #826 / #827 — C2/botnet auto-learning MERGED (PR #827). Sustained + gated + strong-corroborated beaconing → report-only learned indicators. High-precision FP gate (box-vhost/first-party-LAN/seeded allowlist) + multi-signal (rare/non-browser-JA4/DGA) + candidate→confirm (≥3 windows/≥30 min). New /c2/* endpoints + portal proxy + WebUI C2 appris view with one-click Ignorer.
  • #828 — C2 false-positive fix OPEN (PR #828, deployed to board as toolbox-ng 0.1.31). Live testing learned an admin dashboard on rarity alone → promotion now requires a STRONG signal (dga/non_browser_ja), rare is supporting-only; non_browser_ja disabled when the browser-JA4 set is unconfigured. Re-verified live: real DGA-C2 learned, admin dashboard + mail suppressed. Follow-up: populate browser-ja4.txt (empty seed → dga-only out-of-box).

Deployed versions on gk2: toolbox-ng 0.1.31, toolbox 2.8.2.


2026-07-04 — ToolBox privacy report (#785 #790 #792), Zigbee WS fix (#796), PeerTube admin ops (#798)

  • #785 / #790 / #792 — ToolBox kbin report MERGED (PR #787, #794). Faithful-to-page PDF: DPI-Exfil (me) + Overall donut-grids, 🎬 media types block (real sbxmitm MIME + DPI media category) in web + PDF, rich Netrunner character sheet (_enrich_report_data → parity across all 3 PDF routes), real Quêtes from _dpi_stats.alerts_raw. Deploy incident fixed: matplotlib PDF render was sync on the single uvicorn loop + WAF 504 auto-retry storm → board-wide 504; hardened with threadpool + asyncio.Lock + per-device PDF cache + persistent MPLCONFIGDIR.

  • #796 — sbxwaf dropped WebSocket upgrades FIXED, live, PR #805 open. wss://zigbee broke (close 1006 + reconnect loop) via 3 layers: main.go forced Connection: close clobbering Connection: Upgrade; statusRecorder + cachingResponseWriter didn't implement http.Hijacker. Added isWebSocketUpgrade() guard + Hijack() forwarding on both wrappers + upstream-error logging (which pinned the Hijacker layer). New websocket_test.go (token matrix, Hijack forwarding, real 101 handshake through the handler, non-WS regression). Live on gk2: wss://zigbee.gk2.secubox.in/api → 101 Switching Protocols, dashboard stable.

  • #798 — PeerTube WebUI admin ops (reset-password + version check + upgrade) MERGED (PR #800), spool→root privilege-separation (#407 pattern): unprivileged API drops an intent file, root peertube-ops.path drains it via peertubectl (no sudo, NoNewPrivileges intact). Follow-up loop bug fixed, PR #804 open: peertube-ops.path watches OPS_DIR with DirectoryNotEmpty=, but process-ops wrote results back into OPS_DIR → the dir was never empty → .path re-triggered .service in a loop until start-limit-hit killed the mechanism after the first op. Fix: results go to a dedicated /run/secubox/peertube/results/; ops/ empties after draining. Live-verified on gk2 (two consecutive pings drain, NRestarts=0).

  • Wiki — new ToolBox page (numeric-cabin use cases + report features), poster image, Authelia stale entries removed from MODULES, sidebar/navbar link fixes (Gollum [[Text|Page]]).


Rebuilt the P2P evolutions cleanly (the non-integrating Mistral draft was removed) via subagent-driven TDD — 17 tasks, 132 tests, per-task review + final opus review. Branch feature/p2p-dht-federation, PR #775 open.

  • secubox-p2p DHT — custom Kademlia over asyncio/UDP :51823, JSON wire, Ed25519-signed reachability records {did,id_pubkey,wg_pubkey,endpoint,ts,sig}, iterative α-parallel lookup, routing persistence, advisory put_health/get_health store.
  • Federation health-checksHealthStore (debounce up/down) + HealthChecker (semaphore-capped async sweep) + default_probe (aiohttp GET /health, TCP fallback), published over the DHT.
  • Master-linkRole enum, deterministic elect() (min priority, node_id_hex), monotonic TermStore, term-based failover with equal-term tie-break (no zero-master window), Ed25519-signed heartbeats over UDP :51824, request_promotion().
  • Feature-flagged OFF by default (OPAD); [dht]/[federation]/[masterlink] in /etc/secubox/p2p.toml via mesh.load_p2p_config.

Live-activated on all 3 mesh nodes: gk2 10.10.0.1 (MASTER, term 1, prio 10), c3box 10.10.0.2 + amd64 10.10.0.3 (satellites). Each DHT discovered the other two (peers=2), no split-brain. Also shipped: Mesh visualization tab (p2p dashboard canvas: role/term/ DHT peers), login-bounce auth fix (3 boxes), gk2 nginx /api/v1/p2p/ re-routed to p2p.sock so the endpoint mirrors the running daemon (was showing the aggregator's enabled:false), and reboot-persistent nft wg-mesh udp {51823,51824} on c3box+amd64.

The SDD review loop caught and fixed: malformed-contact crash (Task 7), id/wg key schema conflation (Task 8a), equal-term split-brain (Task 15), audit-log PermissionError + unwired peers_fn (final review).

Follow-ups (roadmap): mesh-bans → sbxwaf engine bridge (currently nft-only); macroctl on satellites (standalone NoNewPrivileges=yes blocks the sudo path — works on gk2 via aggregator); smooth the p2p-socket restart window (satellite 502/504). See docs/P2P-EVOLUTIONS-POSTER-PROMPT.md for the poster prompt + full roadmap.


2026-07-01 — Macro subsystem (M2) + tor-exit reference kind (#771)

Services can now propose a vetted, AppArmor-confined access macro so an approved peer consumes them. First increment: framework + tor-exit (SOCKS-over-mesh). Three packages:

  • secubox-annuaire 0.3.3 — optional signed ServiceOffer.macro {kind, params} that federates in the signed payload (byte-stability guard keeps macro-less offers compatible with pre-0.3.0 signatures); annuairectl offer --macro-kind/--macro-param.
  • secubox-macro 0.1.0 (NEW)secubox-macroctl root dispatcher (kind allowlist, plugin root-owned+non-writable tamper guard, src-ip mesh-CIDR check, euid-gated env-ignore, append-only audit) + macros.d/tor-exit (nft grant/revoke, service_id path-traversal sanitize, socks_port bound) + tight sudoers (env_reset) + AppArmor enforce (net_admin) + postinst (Tor SocksPort on mesh IP, nft base set, audit pre-create).
  • secubox-p2p 1.9.0 — provider grant endpoint (self-signed Subscription auth: self-cert DID + ed25519 over annuaire's canonical bytes; auto-mode only), consumer activate pulls the credential over the mesh + runs macroctl activate, mesh listener :8798 (mesh-IP-only, X-Real-IP), revoke-access, NNP=no for the sudo path, UI SOCKS endpoint + Revoke.

Built via SDD (8 tasks, per-task + final opus review = READY TO MERGE). The review loop caught and fixed 6 Criticals: offer-signature wire-break (macro:null changed signed bytes), tor-exit root path-traversal (service_id as root filename), prerm mawk-bricks-dpkg, macroctl NNP-blocks-sudo, macroctl env-injection root-RCE + missing root-ownership guard. Deployed the 3 debs to gk2 + c3box; proven live on c3box under AppArmor enforce: macroctl→tor-exit grant adds a mesh IP to the secubox_macro_torexit nft set + returns the endpoint + writes an append-only audit line; revoke empties it; bad-kind/non-mesh-IP/missing-src-ip all rejected. Full cross-node federation + real Tor routing is env-blocked (Tor not installed on c3box; gk2 uses inet filter not secubox_filter) — not a code issue. Suites: annuaire 189, p2p 49, macro 14.


2026-06-30 — secubox-p2p 1.8.0 : Service Registry = live view of annuaire catalog (#769)

Le registre de services p2p (/p2p/) affichait « No services registered » (JSON local isolé), déconnecté du catalogue fédéré secubox-annuaire 0.2.0. Désormais c'est une vue live : GET /services fusionne le catalogue annuaire + mes abonnements + un mince activation overlay + les services p2p-locaux hérités (sans duplication, sans dérive). api/registry.py (fusion pure, testable) + api/annuaire_client.py (lit /run/secubox/annuaire.sock, jamais l'aggregator ; s'abonne EN TANT QUE nœud via la clé 0600 ; frappe un JWT de service car le subscribe annuaire est JWT-gated). 4 endpoints : GET /services (dégrade en catalog_unavailable, ne 500 jamais), POST /services/auto-register (active les offres locales + s'abonne aux distantes selon auto/pending), /{id}/request, /{id}/activate. UI : bouton « Auto register all » + Request access / Activate + badges d'état (service_id en encodeURIComponent — XSS-safe). annuaire inchangé ; exécution des macros déférée au Milestone 2.

Construit via SDD (5 tâches TDD + revues par tâche + revue finale opus = READY TO MERGE). Bug trouvé au déploiement : le subscribe annuaire est JWT-gated ET vérifie que le sujet est un user activé → token de service frappé pour admin (override SBX_SERVICE_USER). 34 tests. Déployé gk2 + c3box : GET /services = WAF mirror (local) + Suricata (fédéré de c3box) sur gk2, image miroir sur c3box ; auto-register gk2 = {activated:1, requested:1} → Suricata fédéré approved.


2026-06-30 — secubox-annuaire 0.2.0 : trustless cross-node service federation (#766)

Le /services/pull de 0.1.3 n'était pas réellement sans-confiance ni opérable : ingest_offer vérifiait la signature contre une pubkey fournie par l'appelant sans jamais contrôler did_from_pubkey(pubkey) == provider (forge « apporte ta clé, réclame n'importe quel DID ») ; et GET /services renvoyait des offres sans signature ni pubkey (le payload stocké les omet), donc un pair inconnu ne pouvait rien vérifier.

Corrigé :

  • Ingest sans-confiance : ingest_offer impose did_from_pubkey(pubkey)==provider AVANT la vérif de signature. did:plc = sha256(pubkey)[:32] → liaison auto-certifiante, aucun annuaire, aucune confiance préalable.
  • Offres auto-portées : _get_offers/GET /services ré-attachent sig + signer_did
    • provider_pubkey (métadonnée de transport, retirée avant reconstruction du modèle extra=forbid). pull_services les consomme.
  • Bootstrap de nœud : verbe genesis() (un nœud s'auto-atteste MEMBER fondateur, brise le paradoxe invite/join ; DID auto-certifiant, invited_by vide → n'inflige jamais la pluralité d'émancipation ; idempotent). Op.GENESIS (additif). CLI /usr/sbin/annuairectl (init|whoami|status|offer|services|pull) opérant le journal directement en tant que secubox (pas de JWT pour le bootstrap privilégié) ; clé 0600 dans /etc/secubox/secrets/annuaire/node.key.
  • Écouteur mesh : annuaire-mesh.conf.tpl rendu par postinst sur l'IP wg-mesh du nœud uniquement (10.10.0.1:8799 sur gk2), allow 10.10.0.0/24 + deny all, GET /services seul.
  • Tests : +7 (forge, payload altéré, hex invalide, round-trip), 134 passants.
  • Revue sécurité : aucune forge exploitable. Deux durcissements board-wide : postinst valide l'écouteur rendu via nginx -t et le retire si échec (un rendu cassé ne persiste jamais) ; livraison de ip_nonlocal_bind=1 (nginx lie l'IP mesh même si wg-quick@wg-mesh démarre après nginx).

Déployé sur gk2 (0.1.3 → 0.2.0) : service actif, nginx -t OK, écouteur live, genesis gk2 (DID 0463…) + offre « WAF mirror ». Démo live : un second nœud (fondateur distinct, gk2 inconnu) annuairectl pull http://10.10.0.1:8799 → ingested 1, chain_ok. Reste : pull live gk2→c3box bloqué (clé SSH non autorisée sur .94) ; NIZK/PSI GK·HAM à venir.


2026-06-30 — secubox-yacy 1.0.12 : repair webui embed + navbar integration

Page admin https://admin.gk2.secubox.in/yacy/ cassée sur deux points, corrigés dans www/yacy/index.html :

  • webui (iframe récursif) : l'<iframe src="/yacy/"> pointait sur cette même page (nginx sert /yacy/ en alias statique, pas en proxy) → le panneau s'affichait récursivement au lieu de l'UI YaCy. Le src est désormais construit au runtime depuis /api/v1/yacy/access, en préférant l'URL publique https (yacy.gk2.secubox.in, vérifiée sans X-Frame-Options/CSP → framable) pour éviter le blocage mixed-content ; repli sur un lien « ouvrir dans un nouvel onglet » si seule une URL http LAN est joignable.
  • navbar : la page utilisait une grille CSS .layout custom + sidebar-light.css legacy, en conflit avec sidebar.js v2.33 (injecteur hybrid-skin). Migration vers le pattern canonique (annuaire/cookies) : design-tokens.css + crt-light.css, <nav class="sidebar"> + sidebar.js, contenu dans <main class="main">. Strings issues de l'API échappées avant injection.

Déployé live sur gk2 (/usr/share/secubox/www/yacy/index.html, backup .bak-pre-fix), copie debian stagée synchronisée, bump 1.0.11 → 1.0.12. Assets /shared/* 200, JS node --check OK.

Cause racine réelle des cartes « unavailable » + « no search results » : drift nginx live. /etc/nginx/secubox-routes.d/yacy.conf (inclus par le vhost admin) avait dérivé vers proxy_pass …/aggregator.sock en gardant le rewrite qui dénude le préfixe → l'aggregator (modules montés sur le chemin complet /api/v1/yacy/*) recevait /access nu → 404 → .catch() du JS → iframe jamais construit → pas d'UI YaCy. Réaligné sur la config livrée (yacy.sock, ~0,2 s vs 11-20 s via l'aggregator qui bloquait sa boucle). YaCy jamais cassé (freeworld, 352 global / 466 local pour « debian »). Même pattern que Lyrion #763 ci-dessous.

Phase 2 — yacyctl detection + sudoers + postinst + .deb :

  • yacyctl reportait lxc « absent » / daemon « stopped » / overall red alors que tout tournait : secubox-yacy.service tourne en User=secubox + NoNewPrivileges=true, et lxc-info/lxc-attach exigent root (NNP bloque sudo). Remplacé par une sonde réseau privilège-free (curl http://$LXC_IP:$HTTP_PORT/) — préserve le durcissement, signal plus vrai. lxc-info gardé en enrichissement best-effort root-only. → vert via l'API.
  • Ship /etc/sudoers.d/secubox-yacy (lxc-*) pour yacyctl reload (restart daemon in-LXC).
  • postinst : systemctl restart inconditionnel — deb-systemd-invoke restart de dh_installsystemd refuse de démarrer une unité disabled → upgrade laissait yacy.sock absent → 502. (Piège de test : dpkg s'arrête sur un prompt conffile quand on a édité les /etc à la main → --force-confnew pour aligner.)
  • Construit + installé secubox-yacy_1.0.12-1~bookworm1_all.deb (output/debs/). Upgrade propre validé : service active+enabled, dashboard vert, recherche 466, route yacy.sock.

2026-06-30 — Lyrion admin 404 → dedicated-socket extraction (#763)

Page https://admin.gk2.secubox.in/lyrion/ : tous les widgets en HTTP 404.

Cause racine

La route nginx /api/v1/lyrion/ avait dérivé sur la board vers rewrite … /$1 break; + proxy_pass …/aggregator.sock; (sans suffixe /api/v1/lyrion/). L'aggregator monte les modules au chemin complet /api/v1/lyrion/…, donc le /status dénudé → 404. (curl aggregator.sock /api/v1/lyrion/status → 200 ; /status → 404.)

Décision — extraction socket dédié (comme auth/metrics)

Les handlers now-playing / players font du JSON-RPC LMS bloquant à chaque requête. Sur la boucle unique de l'aggregator (~110 modules) un appel LMS lent fige toute la passerelle → 502 board-wide (SPOF observé). lyrion repasse sur son propre secubox-lyrion.service + /run/secubox/lyrion.sock + route nginx dédiée.

Changements

  • source packages/secubox-lyrion/nginx/lyrion.conf : invariant documenté (dedicated socket, ne jamais folder dans l'aggregator).
  • source packages/secubox-lyrion/debian/postinst : préserve l'état runtime sur upgrade (try-restart), démarre au fresh install si le LXC LMS répond.
  • source packages/secubox-aggregator/sbin/secubox-aggregator-migrate : AGG_EXCLUDE (lyrion) → discovery + switch route + disable service le sautent (durabilité).
  • board : secubox-lyrion.service enable --now ; route nginx (secubox.d + secubox-routes.d) → lyrion.sock ; reload. 5 endpoints à 200, stream live affiché.
  • gotcha : le 1er enable --now a re-chown /run/secubox (1777 root:root → 755 secubox:secubox) car le drop-in no-runtime-dir.conf (RuntimeDirectory=) n'était pas rechargé en mémoire systemd. daemon-reload + restaure le parent à 1777 root:root → restart ne le re-casse plus (boot-safe).

2026-06-27 — LAN standardisé 192.168.10.0/24 + c3box/gk2 live Freebox + bump 1.10.0 (#760)

Session terrain "c3box derrière Freebox" : la LAN SecuBox par défaut (br-lan 192.168.1.1/24) entrait en collision avec la LAN d'un routeur opérateur courant (Freebox/Livebox en 192.168.1.0/24). En aval d'une Freebox, le WAN DHCP et la LAN se retrouvaient sur le même sous-réseau → route dupliquée, ARP ambigu, IP de management injoignable.

A. Constat live + remédiation immédiate

  • c3box (second MOCHAbin) derrière Freebox : WAN eth2=192.168.1.94 (bail Freebox) + br-lan=192.168.1.1/24.94 injoignable depuis le LAN. Corrigé live : br-lan → 192.168.10.1/24. SSH root activé, webadmin https://192.168.1.94/ OK, /dev/sda1 (931 G) monté sur /data (style gk2 : UUID + nofail), partition eMMC retirée (emmc-data).
  • gk2 (live PoC) : uplink déplacé de lan0 (DSA) vers le port cuivre WAN eth2 ; netplan réparé via série (gk2 hors-réseau le temps du switch) → eth2 dhcp4: true, lan0 dépouillé. Bail Freebox réservé sur le MAC eth2 f0:ad:4e:27:88:9b → gk2 reprend 192.168.1.200. Persisté.

B. Standardisation source (LAN = 192.168.10.0/24, gw .10.1) — 17 fichiers

  • Netplans board : mochabin, espressobin-v7, espressobin-ultra, x64-vm, x64-live (br-lan),
    • unification VM vm-x64/vm-arm64 (192.168.100.1 → 192.168.10.1).
  • Générateurs de netplan : secubox-netmodes, secubox-hub (preview), secubox-net-detect.
  • dnsmasq (espressobin-v7.conf) : dhcp-range + option:router + option:dns-server.
  • Scripts live-usb (mochabin/ebin) + SAN des certs auto-signés (firstboot, build-image, build-rpi-usb, build-live-usb) → IP:192.168.10.1.
  • Hors scope (intacts) : 192.168.255.1 (whitelist mgmt/trusted-proxy WAF/mail/wg/mitm), listes GATEWAYS de sonde WAN, exemples remote-ui/round + tests.

C. Release

  • Bump mineur (« medium ») 1.9.0 → 1.10.0 : build-image.sh, build-live-usb.sh, build-ebin-live-usb.sh, build-rpi-usb.sh (mochabin-live reste sur sa piste 2.0.0).
  • Artefacts amd64 (x64) reconstruits depuis cette base.

2026-06-27 — Netboot live PROUVÉ + première install SecuBox Debian sur c3box (second MOCHAbin) (#748 #737)

Grande session hardware : netboot gk2→c3box validé de bout en bout, premier SecuBox Debian installé sur un vrai MOCHAbin, et le blocage U-Boot qui empêche #748 de fermer est formellement documenté.

A. Netboot gk2 → c3box : validé en prod

  • c3box (second MOCHAbin, Armada 7040) a booté l'installeur SecuBox Debian servi par gk2 via TFTP : factory U-Boot 2020.10 → tftpboot Image/dtb/initrdbooti → rescue shell installeur, kernel custom 6.12.85 #5secubox. Le FIT signé (49 Mo) était servi en HTTP sur :8099.
  • Le long détour cabling était une impasse LAB (prouvé via gk2 bridge-FDB + test DHCP) — aucun bug logiciel.
  • Learnings opérationnels réutilisables (documentés dans wiki/Netboot-Install.md) :
    • Factory U-Boot 2020.10 s'interrompt sur Enter (pas Ctrl-C), bootdelay=2.
    • Son env n'est PAS dans SPI mtd2 (env étranger fossile) → fw_setenv depuis Linux n'a aucun effet ; seule la config U-Boot interne compte.
    • Seul le port cuivre RJ45 unique = mvpp2-2 est bootable par le factory U-Boot (les 4 ports switch nécessitent le driver MV88E6XXX DSA, absent au boot).
    • Kernel load à 0x02080000 = adresse mémoire réservée → crash immédiat ; utiliser 0x0a000000.
    • setenv tftpblocksize 1468 pour TFTP rapide.

B. #748 enhanced Tow-Boot (HTTP/wget bootloader) — DIFFÉRÉ, bloquant documenté

Branche feature/748-enhanced-tow-boot-http-netboot-serial-fl (stackée sur #737) : spec+plan (docs/superpowers/), Kconfig Tow-Boot, build-uboot-overlay.sh --tow-boot, plan serial-flasher, CI .github/workflows/build-tow-boot.yml (push-triggered).

Bloquant dur (ciseau) : le board MOCHAbin n'existe que dans le fork U-Boot 2022.07 de Tow-Boot (pas de wget) ; wget n'existe que dans U-Boot stock ≥2023.07 (pas de board mochabin/DTS). Bump à stock 2023.07 = wget compile mais build sans DTS. Pour débloquer : backporter wget/TCP dans le fork Tow-Boot 2022.07, OU porter le board mochabin vers mainline ≥2023.07. Pas un tweak de config.

C. PREMIÈRE INSTALL — c3box → SecuBox Debian (la headline)

  • Image : artefact CI secubox-mochabin-bookworm (run 27426515472, 1,8 Go gzip / 8,0 Gio décompressé), téléchargée sur gk2 /data, SHA256SUMS vérifié.
  • Signature : clé secubox-netboot.key de gk2. Vérifié : cette clé FIT == netboot-image.pub embarquée dans l'installeur (modulus match + roundtrip sign/verify). sbx.img.gz + .sig publiés dans le root HTTP netboot, servis sur :8099 (symlink depuis /data).
  • Install automatisé depuis le rescue shell : wget sbx.img.gz (en RAM, c3box a 8 Go) → openssl dgst -verify contre netboot-image.pub (résultat : Verified OK) → gunzip | dd of=/dev/mmcblk0 bs=4M conv=fsync (8 Gio, progression 32→62→94→100%) → sync.
  • c3box démarre SecuBox Debian v1.9.0 — hostname secubox-mochabin, kernel Debian 6.1.0-47-arm64, stack complète : secuboxd, hub, grafana, zigbee, mqtt, authelia, sentinel/rogue-BTS (layers WALL+MIND). Creds root/secubox, Web UI :9443.
  • Fix auto-boot persistant : l'image utilise extlinux.conf à 0x02080000 (adresse réservée factory U-Boot → reset immédiat) et ne livre pas de boot.scr compilé. Construit /boot/boot.scr (kernel@0x0a000000, initrd@0x10000000, console=ttyS0 + earlycon, root=LABEL=rootfs) : le factory U-Boot charge boot.scr depuis mmc et démarre Debian sans intervention. VÉRIFIÉ : reboot sans intervention → login Debian.
  • Layout eMMC installé : GPT p1=boot (FAT, /boot) p2=ROOT (/) p3=DATA. c3box était OpenWrt ; eMMC écrasé (install RAM-only, pas de risque sur l'OS tournant avant le dd).
  • Rig netboot temporaire gk2 encore actif : lan1=192.168.77.1/24, dnsmasq test (DHCP) sur lan1, nft iif lan1 accept, nginx boot-vhost extra listen 192.168.77.1:8099.

2026-06-24 (cont.) — R4 analyst mode: MITM-everything + media reverse-catcher + clone (#736)

New "R4" doctrine — visibility over performance. Delivered + live on gk2:

  • Splice fliptls-splice-seed.conf reduced from a media-CDN perf list to breakers-only (api.anthropic.com); splice now applied ONLY where MITM provably breaks (cert pinning). Banner reaches every page; catcher sees media URLs. Live: learned splices cleared, autolearn gated (tls_splice=off).
  • sbxmitm media reverse-catcher (cmd/sbxmitm/mediacatch.go, toolbox-ng 0.1.20) — 2xx MITM'd flows → cloneable media URLs (HLS/DASH manifests, direct A/V, googlevideo videoplayback) appended to /run/secubox/media-catch.jsonl (URLs only, deduped, atomic, fail-open). --media-catch default on; worker unit ReadWritePaths=/run/secubox.
  • mediaflow Discovered Media + Clone (2.1.0) — /discovered, /clone (yt-dlp→ffmpeg queue, lazy worker for the aggregator), /library, /download/{id}, DELETE; dashboard cards. Verified: HLS caught → ffmpeg → 464 MiB mp4 in library. yt-dlp installed.
  • Also fixed the empty mediaflow dashboard (2.0.2 contract + 2.0.3 cumulative services): cards/streams live, Top Media Services from DPI cumulative store. KEY: dashboard routes via the aggregator (in-process import) — restart secubox-aggregator to pick up mediaflow code changes.
  • Phase 4 done — R4 button added to the banner topbar (R0..R4) + set-level + by-MAC validation + analytics buckets; gated to the wg path like R3 (secubox-toolbox 2.7.20).
  • yt-dlp upgraded 2023.03.04 → 2026.06.09 (standalone binary; YouTube works).
  • Recos: catcher now captures YouTube watch pages (kind=page, toolbox-ng 0.1.22); Discovered Media persisted off tmpfs into a durable capped store (mediaflow 2.1.1); yt-dlp packaged (Recommends + weekly refresh timer + postinst).
  • Catch-log ownership bug/run/secubox/media-catch.jsonl was created secubox-owned while the worker runs as secubox-toolbox, so O_APPEND failed silently → nothing captured. Fixed with a tmpfiles.d entry pre-creating it owned by the writer every boot (zz-secubox-toolbox-ng.conf). Live: rm + worker recreate.

2026-06-24 (cont.) — Banner on nonce-CSP sites + Claude API splice + YouTube unblock (#728)

Three distinct root causes behind "no banner on youtube / news", fixed in order:

  1. Trusted Types (0.1.17) — require-trusted-types-for blocked DOM injection. Stripped.
  2. Nonce-based CSP (0.1.18) — the banner is inlined (service-worker-proof), but a CSP nonce/hash makes 'unsafe-inline' IGNORED → the bare inline <script> was silently blocked. relaxCSPForLoader now borrows the page's own nonce and stamps it on the injected <script nonce=…> (surgical: page CSP/nonces/hashes untouched), falling back to forcing unsafe-inline (drop nonce/hash/strict-dynamic) only when there's no nonce. Nonce validated to base64 charset (attribute-breakout guard). Threaded nonce through injectIntoBody → injectHTML → injectInlineBanner. Tests rewritten for inline semantics.
  3. YouTube wholly blocked (runtime) — autolearn false-positive put youtube.com in /var/lib/secubox/toolbox/learned-trackers.txtDecide() returned block (204) → page never loaded. Removed from learned + added to ad-allowlist.txt (hot-reloaded). Latent-bug tracker: #735 (autolearn must not block apex/first-party nav targets).

Claude API splice (user request) — api.anthropic.com added to tls-splice-seed.conf (+ live seed): cert-pinned Claude API/SDK clients reject the MITM CA, so pass them through; claude.ai web stays MITM'd (browser trusts the CA → still gets the banner).

Verified end-to-end on gk2: YouTube 200 + banner nonce == page nonce; lemonde/lefigaro banner via unsafe-inline fallback. DPI confirmed healthy — collector writes to /var/lib/secubox/dpi/ (state.json/cumulative.json fresh), /exfil returns categorized flows; the earlier "empty" was me checking the wrong paths (/run/secubox/dpi).

2026-06-24 — DPI YouTube bannering: strip Trusted Types CSP (#728)

  • Root cause — YouTube serves a standalone Content-Security-Policy: require-trusted-types-for 'script' header. sbxmitm's relaxCSPForLoader already relaxed script-src (drop strict-dynamic, add 'self'/'unsafe-inline') so the banner loader runs, but Trusted Types still blocked the banner's DOM injection → banner silently never mounted on YouTube.
  • Fix (cmd/sbxmitm/csp.go, toolbox-ng 0.1.17) — drop require-trusted-types-for and trusted-types directives during the relax; omit the resulting empty CSP header line. Local Go unit tests cover both the relax and the empty-header drop.
  • DPI capture half — collector state.json was stale (frozen 09:44); restarted secubox-dpi-flowcap → fresh windows, YouTube/media flows now visible in mediaflow.
  • Deployed to gk2; R3 workers secubox-toolbox-ng-worker@1..4 restarted on 0.1.17.
  • Filed for later: #729 wireguard peers/tabs, #730 yacy, #731 lyrion, #732 magicmirror, #733 firewall dashboard misreport, #734 webui.conf hardcoded-route cleanup.

2026-06-22 — DPI exfil engine + Netrunner report (HTML+PDF) + sbxmitm fixes

Big session: full per-device DPI exfiltration pipeline, the kbin report reborn as a cyberpunk-netrunner character sheet, and two live-ops fixes on the Go MITM engine. All PRs merged to master and deployed live on gk2.

DPI — per-device cloud-exfiltration (#687, secubox-dpi 1.0.5 → 1.1.2)

  • Phase 1 nDPI flow-DPI on wg-toolbox (ndpiReader, ~1% CPU on the Armada).
  • Phase 2 Go collector (secubox-dpi-collector, pure stdlib, arm64): attributes flows to devices via sha256(wg_pubkey)[:16], classifies SNI into nDPI-style categories (cloud/filehost/messaging/ai/media/game/social/adult), fires exfil scenarios (exfil_volume, new_cloud, beaconing, unclassified_external). Producer = secubox-dpi-flowcap (60s windows) → GET /api/v1/dpi/exfil.
  • Dashboard (#693/#695): "Cloud Exfiltration Watch" panel + stat cards + all list cards repointed off the inactive netifyd to the live exfil engine.
  • #692 beaconing tuned to a C2-plausible cadence (1s1h, CV≤0.25, external).
  • #705 cumulative 7dcumulative.json so the report shows history, not just the last 60s window (was: idle device → all zeros).
  • Packaged secubox-dpi 1.1.x (arch arm64, Go built in debian/rules offline, flowcap auto-enabled, Depends: libndpi-bin).

kbin report — Cyberpunk-Netrunner character sheet (#707, HTML + PDF)

  • #699 report tabs (Pistage / DPI-Exfil / Overall) with donut charts.
  • #701/#703 DPI stats + visual donut charts in the PDF (mitm/certs/ads/dpi).
  • #707 persona sheet: class+emoji from the request UA (live device), level=R3 for wg peers, ICE/Exposition bars, XP, 4 pip-bar CARACTÉRISTIQUES, Inventaire, Bestiaire, Quêtes — HTML neon + PDF _persona_block.
  • #709 carto hub map + emoji tables (Traceurs/Pays/DPI) in the PDF.
  • #711/#712 "En un coup d'œil" added to the PDF.
  • #714 charts switched to matplotlib PNG embeds (fpdf2 vector donuts were blank in iOS/Chrome viewers).
  • #716 donut grid → ONE combined 2×2 image (was spilling each donut/legend onto its own page → 24 pages). Report back to a clean 4 pages. User: "report parfait".

sbxmitm (Go MITM engine, #662 line)

  • #689 forged leaf cert TTL 24h → 365d — root cause of recurring "certificat expiré" on clients (cache never evicts; 24h leaves expired daily). Interception kept.
  • #697 stop truncating responses >8MiB — streamResponse() streams non-injected bodies verbatim; large Gmail messages/attachments rendered again over R3.
  • #688 own-domain splice approach REJECTED (decision: intercept all vhosts) — reverted.

Ops notes

  • Surf-break incident: R3 mitm CA rotated 2026-06-05 → clients must re-import the CA root (the "expired cert" was client-side trust, not the board).
  • R3 engine is the Go sbxmitm (secubox-toolbox-ng-worker@1..4, 10.99.1.1:8091-8094) — NOT the Python mitm; restart THOSE for R3 changes.

2026-06-20 — kbin Tor shipped + client releases + ad-block/mitm hardening

  • #683 MERGED (PR #684) — kbin Tor egress quick-switch (switch + nft owner-match tunnel, own-services exemption, reconciler+timer), dashboard/landing/banner metrics fixes, 🧅 indicators (banner/webext/APK), APK persistent WG identity, landing+report redesign (verdict gauge + donut/bars + collapsible details). Live on gk2; Tor armed.
  • Client releases served from kbin: android-v0.4.0 (Latest) + webext-v0.1.5 published by CI; pinned webext tag bumped; board fetch-helpers pull them → /wg/toolbox.apk (0.4.0) + /wg/toolbox.xpi (0.1.5). toolbox 2.7.12.
  • #685 ad-learner hardened (2.7.13) — NEVER_LEARN guard (Google/CDN/fonts/captcha/ auth/payment), AD_MIN_SITES 1→2, prune existing. Root cause of euronews breakage: the learner had 204'd www.google.com → broke reCAPTCHA/consent. Also allowlisted www.google.com/.fr live.
  • mitm-wg stream_large_bodies=1m (2.7.14) — large binary downloads (APK, CA) were corrupted ONLY through the R3 tunnel (HTTP/2 buffer/reframe); now passed verbatim.
  • OPEN [#686] — android-toolbox non-root flow broken (CA auto-install needs root, WG handoff → Play Store, tunnel not detected). Needs on-device dev/testing; rooted-vs- non-rooted decision pending. #685 signing was a red herring (corrupt = mitm buffering).

2026-06-19 — kbin Tor egress quick-switch implemented DARK (#683, ToolBoX 2.7.1)

  • Switch + tunnel for routing kbin surfing through Tor, shipped default-OFF / fail-closed on feature/683. Reuses existing secubox components per the user ask.
  • Transport decision (USER): torify the MITM egress. nft owner-match on the secubox-toolbox (mitm-wg) uid → Tor TransPort 9040 / DNSPort 5353. Clients → TPROXY → mitm decrypts/ad-blocks/poisons/banners/re-encrypts → exits via Tor. Inspection fully preserved; only the exit IP + network identity change. (Rejected: SOCKS5 Go-core dialer = blocked on #662; transparent client torify = breaks inspection.)
  • Switch: filters.json flags tor_mode/tor_preset; API (kbin-gated, admin.gk2 only for actions) GET/POST /admin/tor/{state,on,off,newnym,check-leaks}; 🧅 WebUI tab (badge bootstrap/circuits/exit-IP, toggle, NEWNYM, SOCKS leak probe). tor_ctl.py reuses secubox-tor's control-port code — no cross-service JWT.
  • Tunnel arms via reconciler: root, path-triggered (secubox-toolbox-tor.path watches filters.json) → portal stays NoNewPrivileges=true, no sudo. nft loaded BEFORE tor (no clearnet window); IPv6 worker egress dropped (no v6 leak); prerm disarms on real removal (not upgrade). Depends jq; Recommends tor + python3-socksio; postinst adds secubox-toolbox to debian-tor group.
  • Verified: 166 toolbox tests green (10 new), nft syntax valid (user-resolve only), maintainer scripts sh -n clean, license headers OK, changelog parses 2.7.1.
  • Granularity = global kbin Tor mode (owner-match can't be per-client). Per-client (WG-hash) Tor tracked under #662 (Go-core SOCKS5 dialer). NOT yet flipped/deployed — needs soak + off-board leak test + tls_splice(#649)-OFF before arming.

2026-06-19 — kbin milestone: ToolBoX 2.7.0 (middle release) + Tor chapter staged (#683)

  • End-of-session checkpoint — docs + positioning + version, no runtime behaviour change.
  • secubox-toolbox 2.6.59 → 2.7.0 (middle release) — caps the 2.6.x line (ad-intelligence / Anti-Track v2 / anti-bot uTLS #662) and opens the kbin chapter: kbin (kbin.gk2.secubox.in, the public ToolBoX portal) framed as the first tool of the CyberMind Swiss-army cyber kit — transparent performance, full-encrypted MITM inspection, ad poison/smog injection, adware-ban transparency banner, safe browsing.
  • Docs — new wiki use-case docs/wiki/Kbin-Toolbox.md, docs/FAQ-KBIN-TOR.md, README positioning blurb.
  • Plan #683 (issue + spec) — kbin Tor endpoint: a quick-switch re-routing consenting client surfing through Tor (outbound egress, pseudo-network) so the kbin exit is anonymized. Spec docs/superpowers/specs/2026-06-19-kbin-tor-anonymized-surfing-design.md. Invariants: inspection preserved (Tor after the forging core), fail-closed, opt-in/default-OFF, no DNS leak, CSPN audit-logged. Opposite direction of secubox-exposure (inbound hidden services); reuses its Tor control. Depends on the #662 Go core for the preferred SOCKS5-dialer transport.
  • Caveat recorded — Tor mode must force tls_splice (#649) OFF per-client or asset flows leak the real IP.

2026-06-19 — #662 anti-bot: Chrome TLS fingerprint (uTLS) — defeat DataDome without splice (PR #674)

  • lemonde.fr (DataDome) blocked R3 navigation at the 2nd level: the engine re-origined upstream TLS with a Go JA3/JA4 → flagged as bot. Splice rejected (don't exempt a tracking site). Fix: upstream transport now presents a real Chrome fingerprint via uTLS HelloChrome_Auto + h2-over-uTLS. Verified live: JA4 t13d1516h2_8daaf6152771_02713d6af862 (Chrome), was Go.
  • Cert verification preserved (manual verifyUConn: system roots + intermediates + hostname; adversarially tested). Stopped the Accept-Encoding downgrade (was a tell) + added brotli/zstd decode-inject-reencode. H1 response-header timeout.
  • First vendored deps (utls/brotli/zstd/x-net, pure-Go), offline arm64 via -mod=vendor. Canary 1 worker → verified Chrome FP + cert chains + ad-block + banner → widened to 4.
  • Caveat: DataDome also fingerprints HTTP/2 + behaviour — uTLS helps strongly, not a 100% guarantee. Browser test is the real confirmation.

2026-06-19 — #662 post-cutover restore: ad-block metrics + popup CSS (PR #673)

  • Found by verification: the cutover ported the 204-block but NOT ad_ghost's metrics recording (frozen since 2026-06-18 18:59) nor its cosmetic/popup-hiding CSS (popups returned — they're 1st-party DOM, never touched by host-204).
  • Metrics: Go aggregates blocks in-memory (per ad_host/site + per mac_hash), flushes every 10s to a new portal POST /__toolbox/ad-event (unauth R3-perimeter, body-bounded, never 500s) → SQLite store → #ads dashboard live again (total_blocked rising).
  • Popups: Go injects <style id="sbx-ghost-style"> on R3 HTML (wg-gated, idempotent, on the gzip path with the banner) — ports _COSMETIC + ad-specific popup tokens (interstitial/ad-overlay/popup-ad/popunder/exit-intent), conservative (no bare modal/popup/overlay, regression-tested). Verified live on the R3 path.
  • toolbox-ng 0.1.5 deployed (rolling restart) + portal api.py hot-deployed (drift closed at next .deb build). Portal uvicorn boot ~14s.

2026-06-18 — #662 Phase 7: Python R3 engine DECOMMISSIONED + nft persistence

  • nft persistence (master eea46326): the boot re-apply source is the drop-in /etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft (loaded by nftables.service). Edited it 808x→809x (live already 809x → zero disruption), nft -c -f validated reboot-safe; patched the repo source packages/secubox-toolbox/nftables.d/secubox-toolbox-wg-fanout.nft.
  • Python decommissioned: disable --now secubox-toolbox-mitm-wg-worker@{1..4} + -mitm-wg-dynreload.path → 8081-8084 free, ~240M RAM freed. Units kept (disabled) for emergency rollback. Kept secubox-toolbox-mitm.service (R2 captive-AP mitm on 10.99.0.1:8080 — a different path; the cutover was R3-only). Also pointed the board's /usr/share/.../secubox-toolbox-wg-fanout.nft → 809x so a postinst re-run can't revert to dead ports.
  • Verified self-sufficient with Python gone: banner injects on gzip HTML, ads 204, redirects relayed 301.
  • Deliberately did NOT rebuild+reinstall the secubox-toolbox .deb (portal-restart blip + board-wide nft reload, gratuitous) — repo source is 809x, the next natural build closes the installed-payload drift. #662 epic complete: Go engine sole R3 MITM, fast, ~64MB vs ~280-470MB, persistent, ad-block + banner + redirects all correct.

2026-06-18 — #662 R3 CUTOVER to the Go MITM engine (PR #670) — LIVE + banner ported

  • Cutover executed and live. The Go engine now serves 100% of R3 traffic, replacing the Python mitmproxy workers. Found + fixed 4 blockers that made the dark package unable to serve the live path: (1) it forged with the wrong CA (ca-wg "WG CA" vs the "R3 CA" clients trust) → now uses the mitmproxy confdir bundle; (2) root-only key vs non-root user → R3 CA bundle is group-readable; (3) bound 127.0.0.1 vs the 10.99.1.1 DNAT target → now binds 10.99.1.1; (4) ran CONNECT vs transparent → now --transparent. loadCA scans PEM blocks by type (combined cert+key bundle).
  • Validated on real arm64 hardware then rolled out gated: localhost forge against the real R3 CA → scoped-DNAT transparent capture → canary slot 3 (~25%, dead-man armed)widen to 100%. At 100%: 0 restarts, 0 errors, ~64MB total (vs Python ~280-470MB), even round-robin, 142 distinct SNIs/75s.
  • Banner ported (the one regression the user caught — "no more banner but fast"). Go now injects the real loader <script src="/__toolbox/loader.js" data-mh=.. data-wg=..> (guard-idempotent, R3 wg flag, mac_hash identity) and reverse-proxies /__toolbox/loader.js+/__toolbox/bundle to the portal (127.0.0.1:8088, fail-open), keeping bundle/level logic in Python. Verified live: loader injected + assets 200.
  • Rollback = one nft replace (Python workers kept warm). Persistence gap: the nft flip is a live edit, not yet in the drift-managed generator → reboot safely falls back to Python (workers enabled, banner intact). Phase 7 (decommission Python + persist nft) deferred to a soak'd follow-up.

2026-06-18 — #662 MITM engine migration: P5-prep + P6-prep (PRs #668, #669, all DARK)

  • P5-prep (PR #668). Wired the ported Decide+jar into the Go engine's request/ response handlers: handleConnect runs allow/splice/block/mitm; anonymizeRequest (strip operator/re-id headers + DNT/GPC) on every MITM'd flow; cookie-poison gated to mitm+tracker only (never allow/own-infra; fail-closed-to-clean; benign cookies + Set-Cookie attrs preserved). New secubox-toolbox-ng debian pkg builds an arm64 .deb shipping /usr/sbin/sbxmitm + a DISABLED worker@.service on :809%i (no enable/start, no nft). 22 Go tests, reviewed APPROVED.
  • P6-prep (PR #669). No-traffic build-out of the live transparent path, still DARK. machash.go ports mac_hash_of/_wg_hash_of (WG peers → sha256(pubkey)[:16], mtime-cached, fail-open) wired into clientHashFromConn, cross-engine parity vs Python (anti-rig verified). Transparent SO_ORIGINAL_DST accept (--transparent, default off): peeks ClientHello SNI WITHOUT decrypting → Decide → splice = true raw passthrough (never tls.Server) / else forge via replayable prefixConn; upstream TLS verifies by SNI, pins captured ip:port. Two-stage review caught + fixed a splice-decrypt defect. Builds linux/arm64+amd64+darwin, vet clean, race green, Python parity 10 passed. CONNECT path + poison gate byte-unchanged.
  • Engine now functionally complete + packaged, entirely DARK. Remaining work = the production DEPLOYMENT phases (shadow → cutover → decommission), which touch live R3 traffic and are deferred to a deliberate watched session — NOT chained off "go".

2026-06-18 — #656 Ad Intelligence (PR #657, toolbox 2.6.56) + splice reverted

  • Ad Intelligence — learn/act/measure. ad_ghost now records every block/silent per (ad_host, site=registrable(Referer), action) into a new ad_block_stats store (in-memory dicts, bg-thread flush — no SQLite on the proxy hot path), exposed via GET /admin/ad-stats + a new #ads dashboard tab (top ad hosts, ads-blocked-per-site, action split, KB saved). Aggressive learning: 3rd-party ad-shape requests captured as ad_candidates; autolearn _ad_feed promotes hosts on ≥AD_MIN_SITES (default 1) distinct sites into the 204'd blocklist. Safety (inverts the splice mistake — learning to BLOCK is reversible): ad-allowlist.txt always wins, ad_learn toggle, every block visible in metrics, no IP-drop, no CSP weakening. 115 tests green; deployed + verified (/admin/ad-stats 200, metrics flowing, ad_ghost intact).
  • Splice (#649/#651) REVERTED to off. tls_splice=on bypassed the whole addon chain → autolearn promoted telemetry/tracker hosts (datadog/MS/newsroom) to splice → ad_ghost/anti-track bypassed → ads returned. Flipped tls_splice=off (full MITM, ad-blocking restored). Splice perf vs ad-blocking is a fundamental conflict; needs media-only-no-learn rework before any re-enable.
  • Banner #653 reverted (async loader can't read currentScript → inline-bundle was dead code; setupReassert regressed the banner). Board on 2.6.55-equivalent banner. The strict-CSP/SPA banner gap (YouTube) is the browser-extension's job (webext content-script WIP on feature/655, paused).

2026-06-18 — #649 selective SNI-splice (Lever A) shipped dark (PR #650, toolbox 2.6.54)

  • Architecture decision. Asked "do we need a full mitm for R3 HTTPS?" Answer: outbound HTTPS interception intrinsically needs per-host cert forging (the WAF/own-cert analogy doesn't transfer) — so we keep a forging MITM but only decrypt flows we'd actually modify. Plan = A-then-B: A = selective SNI-splice (this), B = Go/Rust core (strategic, later). WAF deferred.
  • Lever A. New tls_splice addon (first in the mitm-wg chain) decides at the TLS ClientHello, from the SNI alone, whether to MITM or splice (raw passthrough — no forge/decrypt/parse/16-addons). Policy: curated media-only seed (googlevideo/ytimg/fbcdn/twimg/scdn…, deliberately NOT generic CDN edges) autolearn-promoted never-HTML hosts (splice_host_obs table, ≥20 obs, html_hits==0). Never splices trackers/fortknox/no-SNI/media_cache-on. Learning obs recorded off the event loop (bg thread), only for undecided hosts.
  • Dark-launch. Ships tls_splice=observe (classify + log would-splice, still MITM — zero behavior change); on flip is post-soak; off kill-switch.
  • Built TDD (7 tasks, 102 tests), two-stage reviews per task + whole-branch review (APPROVED; closed a hot-path sync-SQLite issue → bg-thread offload, and a fortknox-WebUI never-set refresh gap). Deployed gk2 2.6.54, rolling restart of the 4 workers, addon loads clean, 0 runtime errors, dark default confirmed. Next: soak → review → flip on.

2026-06-18 — #623 systemic shared-parent clobber resolved at source (PR #648)

  • Root cause corrected. The recurring /var/{lib,log,cache,…}/secubox parent clobber was NOT the install -d -m 0750 /parent/leaf leaf form (empirically proven harmless: GNU install -d -m modes only the final component). It was the scaffold boilerplate install -d -m 750 /var/lib/secubox + /run/secubox (BARE parents) in ~56 module postinsts — written -m 750 (3-digit), which is why prior greps/sweeps (#511/#627/#631) missed it.
  • Source-wide fix. Scripted rewrite of all bare-parent targets → /run/secubox 1777 root:root, /var/lib|log|cache|etc|usr/share/secubox 0755; 6 multi-arg lines split per-parent (4 were setting /var/lib/secubox world-writable 1777 — a security regression); 3 chmod 750 /var/log/secubox (soc-gateway/soc-agent/ ui-manager) → 0755. Module-private leaves (/var/lib/secubox/<mod> 0750) left untouched. Scaffold new-package.sh + .claude/PATTERNS.md fixed so new packages don't reintroduce it. secubox-core 1.1.8 tmpfiles.d now declares all 5 shared parents at 0755 (mode-only) for boot/install-time self-heal.
  • Verified: all 64 changed maintainer scripts bash -n clean; zero bare-parent restrictive lines remain (install-d + chmod forms); saas-relay + core rebuilt and packaged postinst/tmpfiles confirmed. Two-stage review (found + closed 2 gaps: the chmod-form clobbers + tmpfiles coverage). NOT mass-deployed (60-pkg restart = thundering-herd risk); live covered by secubox-dirs-guard.timer; lands at next CI image build / reflash.

2026-06-18 — perf sprint (hub latency, R3 tunnel encoding) + crowdsec unblock

  • Hub dashboard latency (#644, PR #645, hub 1.4.6). The hub runs mounted in secubox-aggregator (no sub-app lifespan → cold caches); cold /dashboard fanned out ~16 sequential systemctl is-active (9-12 s) and /public/health-batch did an uncached 3.3 s list-units. Fix: _ensure_services_warm() (one batched offloaded is-active, double-checked lock vs thundering herd) on dashboard/status/modules/ alerts; _refresh_health_batch() TTL snapshot served by the bg loop, cold-miss = one offloaded call. Verified live: health-batch 3.3 s → 8 ms (77 modules, shape unchanged). Toolbox /admin/clients/rich enrichment capped to the 12 most-recent.
  • R3 tunnel web-load (#646, PR #647, toolbox 2.6.53). Diagnosed live: 4-core board at load ~5; the 4 mitm-wg workers are GIL-bound (~1 core total, ceiling ~30%/ worker) competing with R2-mitm/gitea/metrics/crowdsec. Hot path already cached. The one code fix: inject_banner forced Accept-Encoding: identity on EVERY document for stream-inject, but streaming is disqualified on CSP-strict sites + when upstream compresses → those pages pulled uncompressed (3-5× bytes) through the worker for zero benefit. Now adaptive: keep gzip/br by default, learn per-host eligibility (_STREAM_VERDICT, capped/self-healing), strip identity only on proven-eligible hosts' next visit. No feature loss; workers came back leaner (72 MB vs 117 MB). Deploy via detached dpkg -i + rolling sequential restart of the 4 workers.
  • crowdsec unblocked. Its postinst's cscli hub update had 403'd (cdn-hub.crowdsec.net) leaving it half-configured (blocking apt). Re-tested → the 403 was TRANSIENT CloudFront throttling (HTTP/2 200, real Amazon cert, not WAF- intercepted); dpkg --configure crowdsec → RC=0, dpkg --audit clean. No patch.

2026-06-15 — gitea mis-route fix + robust WAF route propagation

  • gitea (git.maegia.tv) 404 → 200. Pure routing-table error: its WAF route pointed at 192.168.1.200:8000 (unrelated nginx) instead of the gitea LXC 10.100.0.40:3000. Corrected the route; gitea container was healthy throughout. (gitea.gk2→nginx:9080 and git.gk2→gitea:3000 were already OK.)
  • Robust route propagation (#609/PR #610, mitmproxy 1.0.8 + waf 1.2.6). Fixing gitea surfaced that the #603 file bind-mount binds an inode, so route tools (jq > tmp && mv = new inode) didn't reach the addon until a container restart. Now: directory bind-mount (host /srv/mitmproxy/var/lib/secubox-waf-routes, ro) + symlink, and the addon live-reloads haproxy-routes.json on mtime change (10 s throttle, in requestheaders). Verified live: jq+mv add → [routes] live-reloaded 256 routes, 0 restart. Ported to source (both synced secubox_waf.py copies + wafctl) + rebuilt into apt.secubox.in.

2026-06-15 — WAF hardening + perf: close open-proxy, behind-WAF media cache

Follow-up to the WAF restoration. Three findings investigated; two fixed.

  • Open forward-proxy / loops (#605/PR #606, mitmproxy 1.0.6 + waf 1.2.4). --mode regular + HAProxy default_backend mitmproxy_inspector made the WAF an open proxy: internet scanners (114.66.25.146, 211.154.17.165, hashtagbrock.nl) drove a 72% backend-error rate + 11 self-loop 508s/hr. The requestheaders hook now serves ONLY our vhosts (routes / our domains via routes-derived local_suffixes → nginx :9080 / SELF_HOSTS) and returns 421 with no upstream connect otherwise. Live: 0 external server-connects, 0 loop-508s, apt/admin/kbin 200, scanners 421.
  • Behind-WAF media cache (#607/PR #608, mitmproxy 1.0.7 + waf 1.2.5). New media_cache.py addon caches cacheable GET media/static (image/video/audio/ font/css/js) from our vhosts on disk (URL key, 16 MB/obj, 2 GB LRU, TTL from max-age) and serves repeats from cache — backend-load + latency win for hosted media. Not a bypass: requests still pass secubox_waf inspection; only the response body is served from a WAF-populated cache. Toggle /data/mitmproxy/media-cache.json (default on). Live: X-SecuBox-Cache: HIT. Gate fix vs the toolbox copy: cache on body length (our nginx is chunked).
  • WG R3 tunnel (wg-toolbox, 4 peers, 4 mitm-wg-worker@{1..4}) is healthy — not the bottleneck; the WAF open-proxy churn was. All fixes ported to source (both synced secubox_waf.py copies) + rebuilt into apt.secubox.in.

Still optional: relax the forced Connection: close (FD-leak fix #496) to bounded keep-alive now that scanner churn is gone — lower per-request latency.

2026-06-15 — APT repo: all packages published + signed (apt.secubox.in)

Made the apt repo at https://admin.gk2.secubox.in/repo/ (served from /var/www/apt.secubox.in, manager repoctl/reprepro) carry all packages.

  • Was broken: pool had 15 orphan debs with an empty reprepro DB and no working signature — the published signing key packages@secubox.in (fp 31848880…) has no private key on the board.
  • Signing (user chose on-board apt@secubox.in, fp 219BA872…): imported its secret into the repo GPG home (/var/lib/secubox-repo/gpg), wrote conf/distributions (SignWith: 219BA872…) + conf/options, re-published secubox-keyring.gpg + FINGERPRINT.txt. InRelease/Release.gpg now Good signature. (install.sh doesn't pin the fp — transparent.)
  • Built all 144 packages (-d, arch:all) + reprepro includedeb bookworm → 288 entries (×2 arch), 145 debs in pool, current versions (core 1.1.6, threat-analyst 1.4.4, vm 1.0.1, toolbox 2.6.37, hub 1.4.3). WebUI /api/v1/repo/packages lists 288. Served + signed via nginx :9080.
  • Tooling fix: scripts/build-packages.sh now passes -d to dpkg-buildpackage (it omitted it → dpkg-checkbuilddeps silently dropped secubox-core and others from every build). 1 pkg failed (sentinelle-gsm, buildinfo artifact race — deb still produced).

Public HTTPS now works — WAF mitmproxy restored (3 stacked bugs). The WAF LXC (mitmproxy, served via HAProxy mitmproxy_inspector → 10.100.0.60:8080) was down board-wide (every inspected vhost 503/400), blocking public apt.secubox.in. Three compounding faults, all fixed live on gk2:

  1. Crash-loop (restart #45552): the cookie-audit.conf systemd drop-in (added #156) overrode ExecStart but dropped --set confdir=/data/mitmproxy → mitmdump fell back to ~/.mitmproxy, which ProtectHome=true blocks → PermissionError: config.yaml. Restored the flag in the drop-in (+ copied the existing CA into /data/mitmproxy to preserve identity).
  2. mitmproxy-11 routing: the LXC addon (secubox_waf.py, pre-#499) only redirected upstream in the request hook, but mitmproxy 11 opens the upstream connection before request → traffic went to the public IP (82.67.100.75). Added a requestheaders hook that sets flow.server_conn.address (+ request host/port) before the connect.
  3. Route-file drift (the real killer, routes_count: 0): the addon reads /data/mitmproxy/haproxy-routes.json, but the system maintains /srv/mitmproxy/haproxy-routes.json (255 routes). The addon's file was missing. Fixed by bind-mounting the host file into the container at the addon's path (/var/lib/lxc/mitmproxy/config) so they stay in sync.

Verified: apt-get update against https://apt.secubox.in fetches a GPG-signed InRelease + Packages (no signature errors), apt sees 130 secubox packages, .deb downloads (200). Other inspected vhosts recovered. Live fixes are durable (container rootfs + LXC config survive restarts); porting them into the provisioning package is a follow-up.

2026-06-15 — threat-analyst: global security overview (1.4.3, live on gk2)

secubox-threat-analyst 1.4.1 → 1.4.3, merged via PR #598 (closes #597), built + deployed live on gk2.

  • #597 — threat-analyst page becomes a global security overview: all metrics dynamic, fed live from WAF + CrowdSec + firewall. New cached /overview endpoint (double-buffer, 60 s background refresh → overview.json) aggregating WAF (/run/secubox/waf.sock /stats: threats today, blocked 24 h, rules loaded), CrowdSec (detection: alerts), firewall (enforcement: IPs blocked in nft via crowdsec-firewall-bouncer). WebUI gains a "Vue globale sécurité" card row + source health line (loadOverview() in loadAll()).
  • Privilege-safe sourcing: daemon runs as unprivileged secubox user → cscli/nft list (both root-only) failed silently. Switched to CrowdSec's privilege-free Prometheus :6060 (cs_alerts + cs_active_decisions). No privilege escalation, no coupling to broken secubox-blacklist-sync.
  • Also carried the 1.4.2 build-safe postinst fix (#595/#596) which had not yet reached the board (was at 1.4.1; deb-systemd-helper enable).
  • Live verified: CrowdSec 3712 alerts / 29312 active decisions, firewall 29312 blocked, WAF 140 rules; /overview 200 via socket and aggregator proxy (aggregator restarted to re-discover the new route).

Found, not fixed (separate): secubox-blacklist-sync.service is failed (#521, exit 2) → secubox_blacklist nft sets empty. Does not affect the overview (firewall count comes from the bouncer via Prometheus).

1.4.4 — real CrowdSec ingestion (#599, PR #600)

The overview cards populated, but the headline stats + Top-N leaderboards stayed 0: collect_crowdsec_alerts() shelled out to bare cscli, which fails for the unprivileged secubox user → alerts.jsonl empty.

  • Read-only sudo ingestion (backend only; frontend stays value-only): collector now runs sudo -n /usr/bin/cscli alerts list -o json -l 200. Ships /etc/sudoers.d/secubox-threat-analyst (only cscli alerts/decisions list *, read-only), visudo-validated in postinst (self-removes if bad).
  • NoNewPrivileges=no on the unit so sudo can escalate — matches the sibling secubox-crowdsec / secubox-waf units (NoNewPrivileges=yes had blocked sudo: "no new privileges flag is set").
  • Auto-collect loop (~5 min) fills the DB without the page open; severity mapped correctly (remediation is a bool).
  • Dedup + 48 h compaction: get_recent_alerts dedups by id, compact_ alerts() bounds the append-only log (was inflating counts/leaderboards).
  • Live verified (1.4.4): alerts_24h=12, 13 unique IPs, 10 countries (BG/BR/DE/FR/ID/IE/JP/NL/SG/US), 6+ scenarios → stats + leaderboards real.

secubox-vm 1.0.1 — /vm/ showed 0 containers (#601, PR #602)

https://admin.gk2.secubox.in/vm/ reported 0 containers though gk2 runs 20 LXC (16 running). Two compounding bugs:

  • Privilege: the aggregator mounts each module in-process as the unprivileged secubox user (serving model confirmed: /usr/lib/python3/dist-packages/aggregator/main.py imports /usr/lib/secubox/<name>/api/main.py). Bare lxc-ls can't see root's /var/lib/lxc → empty.
  • Wrong -F key: lxc-ls -F MEMORY is rejected (Invalid key) and emits no rows — valid key is RAM.

Fix (backend-only): LXC read+lifecycle via sudo -n (run_priv); ships /etc/sudoers.d/secubox-vm (lxc-ls/info/start/stop, visudo-validated); lxc-create/destroy stay root-only (endpoints carry no JWT); lxc-ls -F …,RAM; postinst reloads secubox-aggregator. KVM/libvirt readings were already correct (/dev/kvm absent, libvirtd off). Live: containers {total: 20, running: 16}, /vms lists all 20.

2026-06-14 — ToolBoX privacy/perf sprint : 2.6.23 → 2.6.36, all live on gk2

Large feature sprint on secubox-toolbox (built + merged + deployed live, kbin healthy) + clients + two live fixes. Each shipped via PR + merge + build + deploy.

Toolbox (secubox-toolbox 2.6.23 → 2.6.36):

  • #560 protective mode — tracker alerting + active spoofer (strip operator/tracking headers, drop 3rd-party cookies, DNT/GPC). Live in spoof on the 4 R3 workers + R2.
  • #566 modular filters (/etc/secubox/toolbox/filters.json, WebUI /admin/filters/ui) + R3+/R4 ad/banner ghoster (ad-hiding CSS + 204 ad/tracker hosts ; savings → banner quick-stats).
  • #584 ad ghosting = collapse (no placeholder ; reverted #576 black-hole).
  • #577 shared media proxy-cache (image/video-segment, 16 MB/obj cap, 2 GB LRU, default OFF/opt-in) — /admin/cache.
  • #589/#591 autolearn bad trackers → ad_ghost block set (threat-intel domains + operator-grade cross-site ; anti-bot excluded) + hourly timer.
  • #553/#549 cartographie donut (continent→country) + #587 domain-nugget cloud (country→eTLD+1) + #575 IP nodes hidden (flag+name only) + #555 favicons of major sites (never IPs).
  • #545/#572 banner: neon → colourful emoji-chip guirlande ; inspected→protected on R3+/R4 ; #578 shared pin broadcast (/admin/pin/ui).
  • #570 DPI media/content-type statistifier + donut (/admin/media/ui).
  • #574 webext popup protection panel ; #568 top-tracker list capped 5.
  • #562 /ca/fingerprint surfaces the R3 CA (D5:E4:3A) on the tunnel.
  • #581 postinst fix : enabled units get a real restart on upgrade (was leaving the portal dead → kbin 503 ; bit us twice).
  • #516 review (#564): detect_antibot → (vendor, is_challenge), response-level (cf-mitigated / non-200 token) — deployment vs challenge.

Clients: Android APK v0.3.0 (real zero-tap : launch + boot auto-onboard) ; webext v0.1.4 (crash-fix const-ext, favicons, popup protection panel) — both served from the cabine + GitHub releases.

Live fixes: Nextcloud iPhone photo sync (disabled broken files_antivirus + raised PHP upload limits) ; kbin 503 root-caused → #581.

Open / blocked: #592 unified webmail-hub (Gmail OAuth2 + Gandi + OVH) — design filed, BLOCKED on a Google OAuth client + operator decisions.

2026-06-13 — Browser extension : emancipate cartographie live (ref #532)

Nouveau client clients/webext-toolbox/ (MV3 Firefox .xpi + Chromium), sœur de l'app Android. Surface la cartographie sociale R3 dans le navigateur : badge live des traceurs + popup (4 tuiles + mini Round-Eye graph SVG sans dépendance + top-traceurs taggés CDN/anti-bot/opérateur + actions cartographie/PDF/RGPD-wipe). Parle uniquement à la cabine via R3 (pas de CORS backend grâce à host_permissions).

secubox-toolbox 2.6.14 : GET /wg/toolbox.xpi (local sinon 302 → release), bouton onboard, helper secubox-toolbox-fetch-xpi, postinst dir. CI build-webext.yml (web-ext lint + build, release asset sur tag webext-v*). Suivi : signature AMO, SSE /social/live, icône PNG Chromium, Poke/Emancipate (#525).


2026-06-13 — Android ToolBox app : serve + root-mode silent onboarding (ref #531/#536/#538)

App compagnon Android one-tap R3 (clients/android-toolbox/, Kotlin + Compose).

  • #531 — scaffold Gradle/Compose + CI build-android-apk.yml (debug APK artifact, release asset sur tag android-v*). CI green.
  • #536GET /wg/toolbox.apk (build local sinon 302 → release GitHub) + bouton onboard kbin + helper secubox-toolbox-fetch-apk.
  • #538 (PR #539) — root-mode silent onboarding : install CA système (bind-mount cacerts + APEX conscrypt, SELinux ctx, subject_hash_old pur Kotlin) + WireGuard natif noyau + vérif R3 auto, gated derrière le tap ⚡ Installation automatique (root). Fallback handoff app WireGuard. Fichiers RootShell.kt, RootOnboard.kt, step RootAuto. CI APK build green (code compile).
  • Suivi : release signing (keystore CI) pour empreinte publiée stable.

2026-06-11 — Phase 12.C + Phase 13 protection enforcement plane COMPLETE (ref #518-#528)

secubox-toolbox 2.6.6 → 2.6.11, tags v2.13.16 → v2.13.19.

Phase 12.C — operator-grade / state-adjacent (#518, v2.13.16, 2.6.7)

detect_operator_grade : telco header-enrichment (MSISDN/x-acr/WAP), operator-consortium (Utiq/TrustPid), data-broker / state-adjacent hosts (LiveRamp/BlueKai/Acxiom/Neustar/Tapad/Experian/Palantir-class). Top severity void-purple lens + double ring + banner + PDF evidence section. Detection only.

Phase 13 — protection enforcement plane (#519) COMPLETE

Made the SecuBox ban plane (Vortex DNS + WAF + CrowdSec) actually enforce on device browsing across every egress path.

  • 13.A (#521, v2.13.17, 2.6.8) — inet secubox_blacklist nft table, v4/v6 interval+timeout sets, single forward-hook drop chain (covers captive/WG/br-lxc/LAN); secubox-blacklist-sync unions CrowdSec bans + threat-intel C2 (2h timeout); /admin/blacklist. Also fixed the override_dh_strip latent bug (never runs for arch:all → nft/unbound/ nginx/perf drop-ins had stopped shipping; root cause of live-config drift) by moving to execute_after_dh_auto_install. Memory saved.
  • 13.B (#522, v2.13.17, 2.6.9) — DNS-guard: resolve blocklisted domains → IPs into the set (closes DoH/hardcoded-IP bypass); count-only DoH/DoT detection chain (15 v4 + 6 v6 providers); SECUBOX_DOH_BLOCK opt-in. create-or-replace idiom → idempotent reloads.
  • 13.C (#524, v2.13.18, 2.6.10) — per-device attribution: rate-limited SBX-BL-DROP/SBX-DOH nft logs → journald tailer → device_blocks (anonymous WG/lease hash); quarantine set + /admin/quarantine + one-click operator action.
  • 13.D (#527, v2.13.19, 2.6.11) — feedback loop: escalation evaluator reads opgrade/antibot/device-blocks aggregates, escalates over threshold to blacklist IPs / cscli decision / device quarantine. Audit-logged, reversible, all sources default OFF (opt-in via SECUBOX_ESCALATE_*).

Doctrine : DEFAULT DROP preserved (policy accept only adds drops); no WAF bypass; anonymous (rotating mac_hash); all escalations TTL'd + reversible + opt-in. Verified live on gk2 (18 C2 IPs enforced, quarantine add/remove, synthetic escalation + audit entry).

Future idea captured (#525)

Phase 14 deception plane — pseudo-responses from a proxy instead of dropping tracker IPs (indistinguable, pollutes the profile) + neutralizing CDN-preloaded tracking scripts. For later.


2026-06-10 (soir) — Phase 11 COMPLETE + Phase 12.A/B + toolbox tabs — v2.13.15 (ref #502-#516)

Consolidated stack merged via PR #517. secubox-toolbox 2.5.2 → 2.6.6, tag v2.13.15.

Package progression

2.6.0 (11.A backend) → 2.6.1 (11.B frontend) → 2.6.2 (#513 toolbox tabs) → 2.6.3 (11.C consent+PDF) → 2.6.4 (12.A CDN) → 2.6.5 (12.B anti-bot) → 2.6.6 (Carto kbin-redirect fix).

Phase 11 — social mapping per device (#502) COMPLETE

  • 11.A (#505) : social.py correlation engine, 3 SQLite tables, social_graph.py addon (cookie_id_hash = sha256, never raw values), /social/graph|wipe/{token} + /admin/social-aggregate.
  • 11.B (#507) : d3 force-directed view, FR/EN i18n, server-side favicon proxy, wipe modal (3s countdown), /social/me splash link. Critical live fixes : PYTHONPATH in mitm-wg launcher (every addon's secubox_toolbox import was silently degraded), i18n in <script> block, StaticFiles mount + 0755 www, X-R3-Peer resolution.
  • 11.C (#508) : consent-platform probe (OneTrust/Didomi/Quantcast/ Sourcepoint), pre-consent + extra-EU evidence, bilingual FR/EN PDF (fpdf2). Live PDF 200 / valid v1.3.

Toolbox WebUI (#513)

5-tab nav (Vue d'ensemble / Clients / Filtres / Cartographie sociale / Config). Inline kbin /admin/ HTML route (~230 lines) deleted ; canonical operator surface is now admin.gk2.secubox.in/toolbox/.

Phase 12 — anti-human-detection platform (#514)

  • 12.A (#515) : detect_cdn() from response headers (11 vendors + generic edge-cache), social_host_meta table, by_cdn aggregate. Round-Eye central-hotspot graph : device = pulsing eye at centre, sites on inner forceRadial ring, trackers outer ring, CDN-tinted nodes.
  • 12.B (#516) : detect_antibot() (reCAPTCHA/hCaptcha/Turnstile/ Datadome/PerimeterX-HUMAN/Arkose/Kasada/Akamai-BotManager) from URL + cookies + headers — DETECTION ONLY, bypass gated behind doctrine. Severe cinnabar lens + spinning warning ring + "challenged your humanity" banner. Visible ring levels (dominant radial + ring guides + cache-bust). Per-client operator tools : 🕸️ Carto (token-minted graph link, absolute kbin redirect), ↺ Reset/RAZ (store.reset_client + social.wipe_mac).

Round Eye gadget — diagnosed, physical fix required

OTG USB CDC-Ethernet TX queue wedged (NETDEV WATCHDOG, probe -110). Gadget enumerates but data path dead. gk2-side recovery exhausted (link bounce, USB unbind/rebind). Needs Pi power-cycle / cable re-seat.

Live + verified on gk2

Graph renders real cross-site tracking (35.214.136.108 relay bridging 4 publishers), PDF valid, CDN + anti-bot read paths green end-to-end, reset works, Carto opens the client graph on kbin.


2026-06-10 — Phase 11 social mapping (A+B) + system triage + v2.13.14 (ref #502-#509)

Package bumps

Package from → to
secubox-toolbox 2.5.2 → 2.6.0 (#505 Phase 11.A backend)
secubox-toolbox 2.6.0 → 2.6.1 (#507 Phase 11.B frontend)
secubox-waf 1.2.1 → 1.2.2 (#509 double-buffer cache)
Release tag v2.13.14

Phase 11 — Social mapping per device (#502)

11.A backend (secubox-toolbox 2.6.0, PR #506) — social.py correlation engine + 3 SQLite tables (social_edges / social_nodes / social_links), social_graph.py mitm addon (cookie_id_hash = sha256, never persists raw values), /social/graph/{token} + /social/wipe/{token} (RGPD art. 17) + /admin/social-aggregate endpoints, fold + purge background tasks.

11.B frontend (secubox-toolbox 2.6.1, #507) — d3 force-directed graph view at /social/{token}, FR/EN i18n, server-side favicon proxy (7d cache), wipe modal with 3s countdown, full-viewport layout with pan/pinch-zoom + pre-warm + autoFit. Splash menu link /social/me (🕸️ Ma carto) resolving R3 peers via X-R3-Peer sentinel.

Live result : graph renders real cross-site tracking on gk2 — the ad-tech relay 35.214.136.108 bridging 360yield + seedtag + smartadserver + smilewanted publishers, surfacing exactly the fingerprint reuse Phase 11 targets.

Critical live-deploy fixes : addon relative-import never resolved (mitmproxy loads addons top-level) → inlined; PYTHONPATH missing in mitm-wg launcher silently degraded every addon's secubox_toolbox imports → fixed globally (also un-degraded inject_banner's host classification + GeoIP); i18n moved to <script> block (FR apostrophes broke JSON.parse); StaticFiles mount + chmod 0755 www (kbin HAProxy path bypasses nginx).

11.C (#508) — WIP checkpoint 55626e51 : schema (consent_state + GeoIP columns), EU/EEA whitelist, GeoIP fold enrichment, evidence() helper. PDF generator + consent-probe addon + frontend wire pending.

System triage on gk2

  • CrowdSec firewall — bouncer ran healthy but had no nft tables (external flush). Restart recreated ip crowdsec + ip6 crowdsec6, 100 live decisions.
  • WAF + SOC empty cards/var/log/secubox was 0750 secubox-toolbox, blocking the aggregator (user secubox) from traversing to read waf-threats.log. chmod 0755 live.
  • WAF /stats 30s+ timeout_get_threat_stats() re-parsed the full 110 MB / 332k-entry JSONL on every request (89% aggregator CPU). Fixed via #509 double-buffered cache : disk-persisted counters + byte-position incremental tail reading. /waf/stats now 30-37 ms.
  • PeerTube + PhotoPrism 502 — LXCs were STOPPED; lxc-start → live.

CI + release

  • #503/PR #504 — drop espressobin-v7 + ultra from the scheduled build-image matrix (cause of the v2.13.9-12 release failures).
  • #509/PR #510 — double-buffer WAF cache.
  • Merged both to master (3ebb4477, a6f44807), tagged v2.13.14.

Carried forward

  • Round Eye gadget remote-link to gk2 (shows local metrics only) — needs Pi-side investigation.
  • admin.gk2/toolbox/ tab surfacing decision (proxy/iframe/sub-tab).
  • /var/log/secubox 0755 source-side postinst patch (live-only for now).

2026-06-09 — Phase 10 banner injection perf quick wins + postinst regression fix (ref #501)

Package bumps

Package from → to
secubox-toolbox 2.5.0 → 2.5.1 (banner perf, déployé live)
secubox-toolbox 2.5.1 → 2.5.2 (postinst regression fix, code-only)

What landed

1. Banner injection quick wins (secubox-toolbox 2.5.1, commit ce059d0f)

User signal : "la banner n'apparait qu'en fin de chargement et les chargements de pages sont très lents". Quatre changements ciblés :

  • _host_signals(host) — nouvelle fonction LRU-cachée (maxsize=2048) retournant (app_emoji, app, flag, country, asn, status, status_icon). Re-hits coûtent un dict lookup au lieu de 5-50 ms (classify_host + whitelist match + GeoIP DNS+mmdb).
  • _count_trackers_in_body() retiré du chemin chaud. Le flag is_tracker_host (regex cheap sur l'host de la requête) couvre le signal privacy ; le scan plein-corps économise 30-200 ms sur les publishers lourds.
  • _MAX_INJECT_BYTES = 2 MB — skip injection sur gros corps via pré-check Content-Length + garde défensive len(body) pour les streamed bodies sans CL.
  • Tile 🎯 N trackers (corps) supprimée ; cookies + ⚠ tracker-host conservés.

Confirmation utilisateur post-déploiement gk2 : "browsing performance on iPhone is better... perfect work".

2. Postinst regression fix (secubox-toolbox 2.5.2, commit 15f48d9d)

Deux régressions silencieuses pendant le déploiement 2.5.0 → 2.5.1 sur gk2 :

  • kbin.gk2.secubox.in 503 pendant 5 min : dpkg upgrade a SIGTERMé secubox-toolbox.service (FastAPI kbin landing) et ne l'a jamais redémarré, car dh_installsystemd --no-start --no-enable dans debian/rules. Détecté quand l'utilisateur a signalé "kbin 503".
  • iPhone tunnel inutilisable : postinst a écrasé /etc/nftables.d/secubox-toolbox-wg.nft avec la version single-port DNAT, supprimant le fanout Phase 9 (que l'opérateur avait déployé en runtime avec nft -f sans persistence côté package). Résultat : tout le trafic WG R3 pinné sur worker@1 à 97 % CPU, w2-w4 idle. Détecté quand l'utilisateur a signalé "browsing excessivement trop lent".

Fixes postinst-only :

  • Postinst déploie maintenant secubox-toolbox-wg-fanout.nft en /etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft. Le préfixe zz- garantit le tri alphabétique après le base file dans le glob include de /etc/nftables.conf → le base file crée la table + chains + UDP 51820 input rule, puis le zz drop-in flush+repeuple prerouting avec le numgen fanout map sur ports 8081..8084.
  • Sur upgrade ($2 set), systemctl try-restart sur secubox-toolbox.service, secubox-toolbox-mitm.service, et les 4 instances secubox-toolbox-mitm-wg-worker@{1..4}.service. try-restart est no-op si l'unité n'est pas active, donc safe sur fresh install.

Mitigations live appliquées sur gk2 (2026-06-09)

  • systemctl start secubox-toolbox.service — restaure kbin landing.
  • cp .../secubox-toolbox-wg-fanout.nft /etc/nftables.d/zz-secubox-toolbox-wg-fanout.nft
    • systemctl reload nftables.service + systemctl restart secubox-toolbox-mitm-wg-worker@1.service (pour drop les sticky flows pinnés sur w1) — restaure le fanout 4-worker.

Mémoire ajoutée

  • feedback_nft_layered_dropins_persistence.md — Phase 9 fanout doit trier APRÈS son table-creator (zz- prefix) ; ne jamais symlinker en place du base file.
  • feedback_postinst_preserve_runtime_state.md — dpkg upgrade SIGTERMe l'unité ; postinst doit try-restart + redéployer les drop-ins nft appliqués en runtime.

Branche

perf/501-banner-injection-quickwins poussée sur origin (commits ce059d0f + 15f48d9d). Pas de PR ouverte par défaut (rule feedback_no_unprompted_prs.md).

À faire ensuite

  • Build + deploy secubox-toolbox 2.5.2 sur gk2 (postinst-only — pas de code change ; attendre fenêtre de maintenance).
  • Ouvrir PR #501 sur instruction.
  • Phase 10 future : refactor banner vers JS-driven async (élimine le buffer-read pour TOUS les corps, pas seulement < 2 MB).

2026-06-08 — Phase 7.E.x LXC hygiene + auth recovery + Phase 8 opening (ref #498, #500)

Package bumps

Package from → to
secubox-waf 1.2.0 → 1.2.1
secubox-toolbox 2.2.0 → 2.3.2
secubox-auth 1.0.1 → 1.0.2
secubox-users 1.4.1 → 1.4.2

What landed

1. LXC mitm WAF hygiene (secubox-waf 1.2.1, commit c5f0482e)

mitmproxy.service inside la mitmproxy LXC tournait 1 d 16 h avec 51 % d'eresp HAProxy. Phase 6.P avait shippé le RuntimeMaxSec=21600 drop-in pour les host units mais avait oublié la LXC. RuntimeMaxSec=21600 + memory caps maintenant en source.

2. Auto-detect /wg/onboard (secubox-toolbox 2.3.0, commit c5f0482e)

Une URL unique sniff User-Agent et rend le panneau de la bonne plate-forme (iOS / Android / Linux / macOS / Windows) ouvert en premier. Tous les artefacts inchangés ; l'onboard compose juste.

3. NM connection name fix (secubox-toolbox 2.3.1, commit f1418b53)

Switch de l'UA brut (Mozilla/5.0 (X11; Linux x86_64; rv:151.0)) à village3b-r3-<dernier-octet> pour le champ NetworkManager id.

4. LXC mitm-wg scaffolding + finding architectural (secubox-toolbox 2.3.2, commit 3c4d1cc1)

  • Provision script accepte wg target → LXC privilégié à 10.100.0.62.
  • Launcher mitm-wg paramétré par MITM_WG_LISTEN_HOST.
  • Cutover live tenté + rollback : transparent-mode mitm dans un LXC échoue parce que SO_ORIGINAL_DST est conntrack-backed et conntrack est namespace-scoped — les entries DNAT en netns hôte sont invisibles depuis le netns LXC.
  • mitmproxy --mode wireguard (alternative) est strictement single-peer. Migration de 35 peers = re-onboarding complet.
  • Décision : mitm-wg reste sur l'hôte. Scaffolding LXC reste shippée pour une éventuelle archi multi-instance Phase 9.

5. Auth recovery (secubox-auth 1.0.2 + secubox-users 1.4.2, commits 87bd8e51 + 64e4de16)

Symptôme : utilisateur locked out de l'admin UI. Trois bugs empilés :

  1. ntp_health.probe() n'avait que chrony ; fallback timedatectl ajouté (les SecuBox ship timesyncd).
  2. Clock à +3.28 s vs NTP, force-step via ntpdate.
  3. Auth complètement cassée : users.json + auth.toml étaient root:root → user secubox ne pouvait rien lire → tous les logins "Identifiants incorrects". auth.toml n'avait JAMAIS été chown'd depuis l'install initiale. Source fix : secubox-users 1.4.2 chowne les deux. Plus drop-in 40-etc-secubox-rw.conf (ReadWritePaths=/etc/secubox) pour l'engine puisse persister last_step après verify TOTP.

6. Phase 8 — anti-tracking opérateur (Utiq) (issue #500)

Ouverture du tracker Phase 8. Plan complet 4 niveaux R0 / R1 / R2 / R3, schéma SQLite, banner UI, doctrine CSPN, Quick Win 1 j + Phase 2 1 sem + Phase 3 doctrine. Prompt Gemini fourni pour design exploratoire.

Ref

  • Commits : c5f0482e, f1418b53, 3c4d1cc1, 87bd8e51, 64e4de16
  • Public trackers : #498 + #500

2026-06-07 — Phase 7 reboot follow-up sprint SHIPPED (ref #498)

Same-day follow-up after the Phase 7.D mass reboot. Two user-facing regressions — iPhone tunnel broken end-to-end, kbin R3 verification card permanently "Hors tunnel R3" — plus several collateral fixes.

Package bumps

Package from to
secubox-toolbox 2.1.0 2.2.0
secubox-aggregator 0.1.0 0.2.0
secubox-waf 1.1.3 1.2.0
secubox-magicmirror 1.1.0 1.1.1
secubox-openclaw 1.0.0 1.0.1

What broke and what fixed it

1. iPhone tunnel browsing cut after reboot. Three independent boot-time gaps :

  • /etc/nftables.conf had no udp dport 51820 accept on the inet filter input chain (policy=drop), so every WG handshake initiation arriving from 192.168.1.254 was silently swallowed.
  • secubox-toolbox-wg-provision created the inet wg-toolbox NAT table only at package install. After reboot the table was gone — even if handshake had completed, packets had no MASQUERADE rule.
  • The wg-quick config has no [Peer] blocks by design ; the 35 enrolled peers live in wg-peers.json and are added via wg set at runtime. Boot wipes them.

Fix : new nftables.d/secubox-toolbox-wg.nft drop-in (nftables.service replays at boot) + new secubox-toolbox-wg-restore.service (oneshot After=wg-quick@... that re-injects every peer from JSON).

2. iPhone DNS resolution broken even with tunnel up. Existing peer configs handed out DNS = 10.99.0.1 (captive AP IP) but the wlan iface is linkdown most of the time, so resolution returned ICMP port unreachable. Unbound only listened on 127.0.0.1.

Fix : unbound/99-secubox-wg.conf binds unbound on 10.99.1.1 + 10.99.0.1 with ip-transparent ; nftables drop-in adds DNS DNAT 10.99.0.1:53 → 10.99.1.1:53 for legacy peers ; wg.py now emits DNS = 10.99.1.1 on new profiles.

3. R3 verification card permanently "Hors tunnel R3". Two bugs in the detection chain, then a third in the cert-trust probe :

  • The toolbox FastAPI's R3 check read request.client.host — which is the upstream proxy peer (unix socket / HAProxy IP) after the wg → mitm-wg → HAProxy → nginx loop. The real client IP was always lost.
  • mitm-wg didn't propagate the original peer IP upstream at all.
  • The JS verification probe loaded http://10.99.0.1:8088/qr/... from an HTTPS page : iOS Safari blocks mixed content, so the request never fired regardless of whether the network was reachable.
  • The cert-trust probe targeted https://www.gstatic.com/generate_204 — but gstatic.com is in mitm-wg's ignore_hosts whitelist, so mitm-wg passed it through with the real Google cert. The probe wasn't testing CA trust at all. Plus generate_204 returns HTTP 204 with no body, which <Image>.onerror treats identically to a cert error — every CA install reported as failed.

Fix : new inject_xff.py mitm-wg addon (loaded first) adds X-Forwarded-For, X-R3-Peer, X-Through-R3-Tunnel: 1 headers. New _client_ip(request) helper in api.py reads those. New GET /wg/r3-check endpoint returns {tunnel:bool}. landing.html.j2 JS rewritten : same-origin fetch('/wg/r3-check') then fetch('https://duckduckgo.com/favicon.ico', {mode: 'no-cors'}) — duckduckgo is not whitelisted so mitm-wg actually terminates TLS with the R3 CA, and fetch(no-cors) distinguishes a successful TLS handshake from a cert error unambiguously.

4. Linux operators can't import the WG profile. Cosmic/GNOME/KDE nmcli refused the standard wg-quick.conf with "le mot de passe pour «wireguard.private-key» n'est pas indiqué dans le paramètre «passwd-file»". The private key flag defaults to agent-owned (1), which makes nmcli require --ask.

Fix : new GET /wg/profile/new.nmconnection emits the same fresh peer in NetworkManager keyfile format with private-key-flags=0 (system-owned). Drop into /etc/NetworkManager/system-connections/, nmcli c reload, click Connect.

5. Aggregator load spike (83 % CPU, free RAM 137 MB). A single runaway Firefox tab on the toolbox admin dashboard was hammering /api/v1/toolbox/admin/{health,config,clients,metrics,filter-control/list} at ~530 req/sec (stacked setInterval(refreshAll, 10000) timers accumulating across session-restore + aggregator restarts).

Fix : nginx limit_req zone=sbx_toolbox_admin rate=20r/s burst=40 nodelay on /api/v1/toolbox/admin/*. Live numbers : load avg 9.16 → 2.34, free RAM 137 → 353 MB, aggregator CPU 83 % → 18 %.

6. Other follow-up fixes :

  • secubox-magicmirror 1.1.1 — debian/rules now ships api/routers/ (was missing → from .routers import mmpm always failed).
  • secubox-openclaw 1.0.1 — postinst chowns /var/lib/secubox/openclaw to secubox:secubox so the aggregator user can traverse it.
  • Migration helper skips legacy secubox-* prefixed twin dirs whose canonical unprefixed sibling already exists.
  • Live-masked secubox-ui-manager.service (was looping in activating(start) consuming 74 % of a core). Source fix pending.

Numbers after the sprint

Before After
Load avg (1 m) 9.16 2.34
Free RAM 137 MB 353 MB
Aggregator CPU 83 % 18 %
mitm-wg CPU 2.1 % 4.3 %
Modules mounted 117/121 117/121

Files changed

  • packages/secubox-toolbox/ : 13 files (nftables.d/, sbin/, systemd/, unbound/, mitmproxy_addons/, secubox_toolbox/, conf/, nginx/, debian/)
  • packages/secubox-aggregator/ : 4 files (aggregator/, sbin/, debian/)
  • packages/secubox-waf/ : 2 files (api/, www/)
  • packages/secubox-magicmirror/debian/rules
  • packages/secubox-openclaw/debian/postinst

Ref

  • Branch landings : 8b0d4840, 22748b29, 613de765, 9ac35005, 7e5c510c, 6812dfb0
  • Public tracker : #498

2026-06-06 — Phase 7.D ASGI consolidation SHIPPED (ref #498)

Memory pressure on gk2 was driven by ~100 per-module uvicorn processes each carrying its own Python interpreter + FastAPI + Pydantic + Starlette in a separate address space (~30 MB duplicated × N modules). Phase 7.D collapses that into one master uvicorn that mounts every module's FastAPI as a sub-app.

What ships

New package — secubox-aggregator 1.0.0-1

  • aggregator/main.py reads /etc/secubox/aggregator.toml, walks /usr/lib/secubox/<name>/api/main.py for each entry and mounts the resulting FastAPI under /api/v1/<name> via app.mount(). Failures are isolated (try/except per module) and surfaced at /health.
  • The loader synthesises a per-module parent package sbx_pkg_<name> whose __path__ points at <name>/api, then loads api/main.py as <pkg>.main with __package__ set. This makes from .deps import X resolve to the right deps.py even though the module is not pip-installed. sys.modules["api"] / api.* is cleared before and after every load so one module's api/ cannot shadow another's (the bug that hit haproxy after auth loaded — both have api/webui_identity.py).
  • systemd/secubox-aggregator.service : single uvicorn on /run/secubox/aggregator.sock, --workers 1 --backlog 400 --limit-concurrency 200, PYTHONOPTIMIZE=1, MemoryMax=1G, MemoryHigh=768M.

Cutover helper — secubox-aggregator-migrate

  • Discovers every /usr/lib/secubox/<name>/api/main.py, generates /etc/secubox/aggregator.toml, restarts the aggregator, reads /health to learn which modules actually mounted.
  • Rewrites every nginx proxy_pass that pointed at a per-module unix socket (or hub's old TCP 127.0.0.1:8001) to target aggregator.sock AND preserve the /api/v1/<name>/ prefix upstream. Three places get patched : /etc/nginx/secubox.d/, /etc/nginx/secubox-routes.d/, and inline blocks + location /api/ catch-all in sites-enabled/webui.conf.
  • Stops + disables per-module systemd units for migrated modules, prints rollback recipe. Idempotent.

Numbers (live on gk2 after reboot)

Before Phase 7.D After Phase 7.D
uvicorn procs 103 41
RAM uvicorn (RSS sum) 3499 MB 1984 MB
Free RAM 153 MB 2.7 GiB (+18×)
Modules mounted (each on own port) 119/121 in one proc
Aggregator RSS ≈170 MB
Sample endpoint p50 unchanged unchanged

Bug fixes during the same sprint

  • Loader v1 flat-loaded api/main.py as sbx_mod_<name>, leaving __package__ = None. Nine modules (crowdsec, eye-remote, haproxy, health-doctor, mail, modem, users, secubox-crowdsec, secubox-modem) failed with "attempted relative import with no known parent package" or with a stale sys.modules["api"] from a previously-loaded module. Loader v2 builds a synthetic parent package and clears the api* namespace per load — all 9 now mount.
  • Migration helper v1 only walked /etc/nginx/secubox-routes.d/ and used a naive socket-name sed that kept the :/ strip-prefix rewrite. Result on first run : /api/v1/hub/public/menu returned 502, navbar empty. Migration helper v2 walks all 3 nginx config locations and rewrites every variant to preserve the /api/v1/<name>/ prefix the aggregator expects.

Remaining edge cases (carried as follow-ups)

  • magicmirror : api/__init__.py + api/main.py ships in the deb but from .routers import mmpm references a missing routers/ directory. Pre-existing bug — was broken under per-module systemd too.
  • openclaw : /var/lib/secubox/openclaw/ is 0750 root:root, the aggregator user secubox can't traverse it during the module's import-time mkdir. Fix is a postinst chown in the openclaw deb.
  • 4 nginx 503/404 (heartbeat, secubox-haproxy, secubox-hub, sentinelle-gsm) — mount fine but each module's own /health route is missing or hangs ; investigate per module.

Files changed

  • Source : packages/secubox-aggregator/ (new package, ~430 lines total — main.py, migration script, systemd unit, debian/*)
  • Live : /etc/nginx/secubox.d/*.conf, secubox-routes.d/*.conf, sites-enabled/webui.conf (110+ files, mass-rewritten + backed up under .bak.phase7 / /root/webui.conf.bak.phase7)

Ref

  • Branch feature/498-asgi-migrate
  • Commits 5ef13b56 (skeleton) → 7b8cc301 (migration helper) → b1ee9427 (nginx rewrite fix) → 705476cf (loader v2)
  • Public tracker : #498

2026-06-05 — Phase 7.A.2 + 7.B WAF dashboard + rate-limit + honeypot SHIPPED (ref #498)

Same-day extension after Phase 7.A. Adds operator-facing dashboard, pre-mitm TCP rate-limit, and honeypot routes for known bot signatures.

What ships (live on gk2)

Phase 7.A.2 — backport + automation + dashboard

  • packages/secubox-waf/mitmproxy/secubox_waf.py (older 756→878 lines) : backported _load_crowdsec_cfg, _cs_jwt, _ban_via_crowdsec, Phase 6.J Connection: close upstream. Both WAF copies now in sync.
  • debian/postinst : auto-invokes secubox-waf-cs-bridge-setup on package install/upgrade if cscli present. Writes config to /etc/secubox/waf/ on host AND bind-mounts into the mitmproxy LXC if /var/lib/lxc/mitmproxy/ exists. Fully idempotent.
  • api/routers/waf.py GET /enforcement returns : bridge_enabled, bans_pushed, bans_failed, requests, blocked, warnings, rate_limit_offenders (live nft set count), honeypot_hits_last_hour (from nginx log scan), recent_bans (cscli filtered origin=secubox-waf), recent_threats (last 20 lines of threats.log).
  • www/mitmproxy/threats.html : new dashboard tab with 6 KPI cards (Bans pushed / Bans failed / Rate-limit offenders / Honeypot hits 1h / WAF blocked / WAF warnings) + 2 tables (recent bans, recent threats), auto-refresh /5s.

Phase 7.B — pre-mitm enforcement + honeypot

  • nftables/secubox-waf-ratelimit.nft : standalone nft table inet secubox_waf_ratelimit with offenders_v4/v6 dynamic sets (5-min timeout) + whitelist_v4 interval set (LAN/loopback). Input hook priority -10. Rule : tcp flags syn tcp dport {80,443} limit rate over 30/second burst 50 packets add @offenders_v4 { ip saddr timeout 5m } log prefix "[secubox-rl] " counter drop. Self-healing 5-min TTL on entries.
  • debian/secubox-waf-ratelimit.service (systemd) reapplies the table on boot, RemainAfterExit=yes.
  • nginx/honeypot.conf : 5 location blocks for common bot recon (/wp-admin, /.env, /.git/config, /phpmyadmin, /actuator, /autodiscover, etc.). Returns empty 200, logs to /var/log/nginx/honeypot.log with custom secubox_honeypot log_format (ISO timestamp + IP + request + UA + referer).
  • debian/postinst installs into /etc/nginx/secubox-routes.d/ + creates log_format snippet in conf.d/.

Verification

nft list table inet secubox_waf_ratelimit  → table loaded, chain attached
curl admin.gk2.secubox.in/wp-admin           → HTTP 200 (honeypot)
tail /var/log/nginx/honeypot.log              → log lines appearing
systemctl is-enabled secubox-waf-ratelimit    → enabled (boot persist)

Files added

  • packages/secubox-mitmproxy/nftables/secubox-waf-ratelimit.nft (NEW)
  • packages/secubox-mitmproxy/nginx/honeypot.conf (NEW)
  • packages/secubox-mitmproxy/www/mitmproxy/threats.html (NEW)
  • packages/secubox-mitmproxy/debian/secubox-waf-ratelimit.service (NEW)

Master merged

Commit a35ab5c5 (8 files, 938 insertions). Merge 4f89bd8b.

Remaining (Phase 7.C, kept in #498)

  • eBPF/XDP filter (replace Python WAF hot-path)
  • ModSecurity in HAProxy with OWASP CRS rules
  • Federation : CrowdSec Hub + AlienVault OTX + Spamhaus DROP

2026-06-05 — Phase 7.A WAF active enforcement SHIPPED (ref #498)

Same-day landing right after Phase 6 closure. WAF detections now become real nft drops within seconds.

Pipeline live-verified end-to-end on gk2

mitm WAF (LXC) detects threat (count >= BAN_THRESHOLD)
  → _ban_via_crowdsec() POST /v1/alerts
    → socat bridge 10.100.0.1:8080 → 127.0.0.1:8080
      → CrowdSec LAPI insert decision
        → crowdsec-firewall-bouncer poll + apply
          → nft table ip crowdsec DROP

login    : 200 JWT 160 chars
alert    : 201 ← decision id 13244
cscli    : Ip:192.0.2.99 ban origin=secubox-waf 1h
nft      : 192.0.2.99 in table ip crowdsec (kernel drop)

Round-trip ~12s. Merged in 3eb5378e. Issue #498 stays open for 7.B/7.C.

Files added

  • packages/secubox-mitmproxy/addons/secubox_waf.py_ban_via_crowdsec(), _cs_jwt() with 25-min cache, _load_crowdsec_cfg(), ban_ip() now bridges via HTTP first (fallback to cscli subprocess)
  • packages/secubox-mitmproxy/bin/secubox-waf-cs-bridge-setup — idempotent setup : creates socat unit, registers machine via cscli machines add -f -, writes config TOML
  • packages/secubox-mitmproxy/crowdsec/crowdsec.toml.example — schema doc

Discoveries / decisions

  • LAPI POST /v1/alerts needs WATCHER (JWT) auth, NOT bouncer (X-Api-Key). Bouncers only PULL via /v1/decisions/stream.
  • cscli machines list -o json uses key machineId not name.
  • cscli machines add writes to /etc/crowdsec/local_api_credentials.yaml by default — conflicts with local agent. Use -f - to suppress.
  • Alert envelope requires leakspeed (one word, not snake_case).
  • urllib stdlib used instead of httpx (mitm venv lacks httpx, +1ms vs no dep).
  • Socat bridge chosen over changing LAPI listen_uri : changing the bind address breaks the local agent + existing bouncer (hard-coded 127.0.0.1 in their credentials).

2026-06-05 — Phase 6 R3 WIREGUARD : MAJOR RELEASE shipped (ref #496) + WAF leak FIXED

Phase 6 (R3 mode = portable WireGuard tunnel + mitm interception from anywhere) went from rough plumbing this morning to a polished public-grade release by evening. Branch feature/496-phase-6-wireguard-mitm-autocert-mode-r3 carries 30 commits, 2918 insertions across 26 files.

What ships

R3 architecture (#496, deployed live on gk2)

  • WireGuard server wg-quick@wg-toolbox on UDP 51820, multi-peer
  • DNS A record kbin.gk2.secubox.in → 82.67.100.75 provisioned via Gandi API
  • Dedicated mitm CA Gondwana ToolBoX R3 CA (separate from R1/R2 captive CA) — final version has no SAN (Android Chrome rendered "émis par null" when SAN contained spaces) and is served as PEM (/wg/ca.pem), DER (/wg/ca.crt), Apple mobileconfig (/wg/ca.mobileconfig)
  • mitm-wg transparent on 10.99.1.1:8081 with launcher wrapper composing --set ignore_hosts=<regex> from /var/lib/secubox/toolbox/mitm-bypass.conf on every restart (15 patterns default : Signal, WhatsApp, Telegram, Apple Push, French banks)
  • HAProxy vhost kbin.gk2.secubox.in with wildcard cert → backend toolbox_landing → 10.99.0.1:8088 (FastAPI). All endpoints reachable from Internet without captive constraints.

R3 first-class identity (Phase 6.H, commit 2fab850a)

  • mac_hash_of() is now WG-aware: 10.99.1.x → sha256(peer_pubkey)[:16] (cached, mtime-reloaded). Cascade fix : all addons (cookies, dpi, ja4, soc, avatar, inject_banner, local_store) now write R3 events under the correct stable mac_hash. Backfilled 21 existing peers into the clients table with level=r3.

Banner enrichment (Phase 6.G, commit 67985d93)

  • inject_banner now computes per-flow : cookies set+sent count, tracker domains in body (200kB scan), is_tracker_host flag (1st-party host matches ads*/pixel/analytics patterns). Display : 🍪 N · 🎯 N · ⚠ tracker-host. Privacy-safe : counts only, no names embedded.

Splash R3 detection (Phase 6.I, commit 13e48c93)

  • When request to / comes from 10.99.1.x, splash shows large gradient "🌐 Mode R3 — Tunnel WireGuard ACTIF" banner + report/PDF/reinstall links, and HIDES the captive R0/R1/R2 chooser form (useless to R3 clients who are by construction at max-analysis).

Landing + admin polish (commits b865636a, d81e16ac, e2b89314)

  • Public landing at https://kbin.gk2.secubox.in : 8-icon quicknav grid, 8 live KPI cells (auto-refresh /5s via /cumulative-stats.json with pulse
    • flash animations), 🔬 cert R3 2-step probe (detects tunnel + mitm CA trust separately), 4-level explanation cards, SVG charts.
  • /admin/ webui tabbed (👥 Clients + 🛡 Mitm filtering) with rich client table, level switch modal, per-row × delete on filter patterns.
  • /admin/filter-control on public kbin = READ-ONLY (banner links to private editor). Edit happens at https://admin.gk2.secubox.in/toolbox/ (SSO-gated) in a new Filter card added to the existing toolbox webui.

Multi-OS install (commit 645ce572)

  • /wg/r3-install page with 5 OS tabs (🍎 iOS, 🤖 Android, 🐧 Linux, 💻 macOS, 🪟 Windows), each with copy-paste-ready commands. Android tab explicitly warns Chrome cannot install + walks Settings → Security → Encryption & credentials path.
  • Wiki page : https://github.com/CyberMind-FR/secubox-deb/wiki/R3-WireGuard-install (237 lines, same install matrix + architecture + whitelist + troubleshoot).

Bugs fixed during the session

  • R3 banner not firing : inject_banner gate accepted only "r2", not r3 (0a6073d5)
  • /report/me/html 400 on WG : added ?mh= bypass (67985d93)
  • iPhone HTTPS broken in R3 : mitm 11 had no CA loaded, served default CN=mitmproxy null cert (38461de4ba7144a3 final clean CA)
  • Android "émis par null" : CA had SAN with spaces → Chrome parser failed → cert install dialog showed null. Regenerated CA minimal DN, no SAN (ba7144a3).
  • mitm-wg restart loop 191x : /etc/secubox/toolbox/ca-wg/mitmproxy-ca.pem was 0600 root:secubox-toolbox, mitm couldn't read as group member (owner bit only). Fix : 0640 group-readable (3ba9e4ad).
  • chess.maegia.tv 504 + general perf : 1) streamlit LXC consuming 15 Python procs stopped + auto-start disabled (saved ~250 MB RAM) ; 2) mitm WAF leak — pool of upstream keep-alive sockets accumulated 1500+ FDs over 4h → workers saturated → HAProxy 504. Fix : single line in addon flow.request.headers["Connection"] = "close" forces upstream nginx to close after each response (e2b89314 live + source backport this commit). Cost: ~1ms loopback TCP handshake per request. FDs 1513→3, scur 812→87.

What still needs eyes

  • iPhone + Android : reinstall the FINAL CA (SHA1 D5:E4:3A:C1:AD:4E:25:8A:A9:D4:2A:26:52:2C:D8:82:50:63:EA:0E) and delete any older "Gondwana ToolBoX..." profile first. Banner appears on HTTPS pages once new CA is trusted.
  • PR #495 (Phase 5 LXC) + PR #496 (Phase 6 WG) to be opened once banner E2E confirmed on both phones.

2026-06-02 — Pi 400 BOOTS TO KIOSK + login works (live patches + #442 closed)

The v2.13.10 image flashed yesterday booted but never reached graphical.target. Diag revealed two stacked issues : a build-time boot storm and a missing admin password seed.

What was happening

  • multi-user.target.wants/ had 150 enabled services. Pi 400 (4 GB) couldn't bring them all up — many LXC-driven apps (jellyfin, jitsi, peertube, photoprism, matrix, gotosocial, mail, ollama, localai, gitea, …) and hardware-specific daemons (secubox-otg-gadget, secubox-led-trigger, secubox-healthbump, secubox-picobrew) failed and entered restart loops. Journal showed restart counter is at 13, then kernel task blocked for more than 120 seconds. multi-user.target never reached, so graphical.target never activated, so secubox-kiosk.service never tried to start. Boot stopped at tty1.
  • secubox-bootmenu.service failed with exit 1 in the early-boot tty context (read -t on tty1 before getty is ready). Not blocking but noise in the journal.
  • The kiosk web UI showed up after the fix but rejected admin/secubox login. Root cause : /etc/secubox/users.json shipped with password_hash: null for admin and must_change_password: true, and the kiosk login form doesn't implement the "set new password on first login" flow.

What was done

  • Live SD patch : removed 130 non-essential symlinks from multi-user.target.wants/, kept a 20-entry kiosk-essential whitelist :
    • Debian core : avahi-daemon, console-setup, cron, e2scrub_reap, networking, nginx, remote-fs.target, ssh, systemd-networkd, wpa_supplicant
    • SecuBox web stack : secubox-auth, secubox-certs, secubox-cookies, secubox-core, secubox-defaults, secubox-hardening, secubox-hub, secubox-portal, secubox-runtime, secubox-system, secubox-users
  • Live SD patch : seeded admin password = secubox (argon2id hash), cleared must_change_password, removed the bogus runnervm3jyl0 user that the CI runner had auto-created.
  • Pi 400 rebooted on patched SD : Reached target multi-user.target, Started secubox-kiosk.service, Reached target graphical.target — the kiosk Chromium displayed the SecuBox login screen on HDMI.
  • Login admin / secubox accepted, dashboard loaded.

What was committed as proper CI fix

  • #442 filed + PR #443 merged → v2.13.11 tagged. Adds Step 5.3 to build-rpi-usb.sh after the dpkg -i loop : trims multi-user.target.wants/ to the same 20-entry whitelist + removes secubox-bootmenu.service from sysinit.target.wants/. v2.13.11 build-rpi-usb CI is still in progress at HISTORY-write time.

Remaining cosmetic items (low priority)

  • secubox-kiosk.service runs startx -- :0 vt7 -nocursor — the -nocursor hides the mouse pointer (intentional for a touch kiosk, unwelcome on the Pi 400 keyboard+mouse form factor).
  • Kiosk login UI doesn't show the LAN IP — operator can't easily SSH in without finding it via the router or another method.
  • The password_hash: null admin in users.json shouldn't ship at all — the CI build should seed a known default OR force the first-login wizard. Filed as a follow-up.

Lessons (added to memory)

  • feedback_no_mass_daemon_restart was about runtime on gk2 ; the same pattern bites at build time on weaker boards (Pi 400). Image builds need a per-board service profile, not enable-all-by-default.
  • When debugging "boot stops at tty1", check the journal for Reached target multi-user.target and Started getty@tty1.service in the SAME boot. If getty starts but multi-user never reaches, you have a wants/ overload. Look at restart counter and task blocked for more than N seconds.

2026-06-01 — kiosk-on-rpi400 actually ships : the #436 chain (PRs #437#441)

Closing the kiosk regression discovered the day before. Five back-to-back PRs, six tags (v2.13.5 → v2.13.10) to make --kiosk actually produce a working image. The previous day's PR #435 made the assertion fail-loud, this day made it pass.

The full chain

Tag PR Issue Failure exposed Fix
v2.13.5 #435 (D-1) #433 [OK] Kiosk installed lied fail-loud + pre-rsync assertion
v2.13.6 #437 #436 Failed to connect to bus: Host is down (cascade through secubox-* postinsts) systemctl wrapper exports SYSTEMD_OFFLINE=1; policy-rc.d blocks invoke-rc.d; tmpfs at /boot/firmware (64M)
v2.13.7 #438 #436 ENOSPC on tmpfs + findmnt: can't read /proc/mounts tmpfs 64M → 512M + bind-mount /proc,/sys into chroot
v2.13.8 #439 #436 secubox-kiosk.service not in graphical.target.wants/ (SYSTEMD_OFFLINE doesn't always materialise WantedBy symlinks) explicit ln -sf of the WantedBy symlink
v2.13.9 #440 #436 assertion still failed on the symlink — symlink target was absolute, host [[ -e ]] couldn't follow switch to relative target (../secubox-kiosk.service)
v2.13.10 #441 #436 Step 7 : config.txt: No such file — tmpfs at /boot/firmware discarded raspi-firmware's writes on umount tmpfs → mount --bind ${ROOTFS}/boot/firmware ${ROOTFS}/boot/firmware (self-bind preserves writes)

Verification

  • secubox-rpi-arm64-bookworm.img.gz v2.13.10 downloaded from the build-live-usb workflow artifact (1940 MB) — the GitHub release didn't publish (create-release failed on the OTHER arch jobs that are still broken : x64 live, mochabin live, espressobin-v7, espressobin-ultra) but the rpi400 image itself built clean.
  • sha256sum matched.
  • Flashed /dev/mmcblk0 (28.8 G microSD), 8.6 GB written at 37.8 MB/s.
  • Mounted p2 read-only and verified EVERY artefact :
    • /var/lib/secubox/boot-mode = kiosk
    • /etc/systemd/system/default.target/lib/systemd/system/graphical.target
    • /etc/systemd/system/secubox-kiosk.service (434 bytes) ✓
    • /etc/systemd/system/graphical.target.wants/secubox-kiosk.service../secubox-kiosk.service (relative) ✓
    • /usr/bin/chromium
    • /usr/share/secubox/kiosk/secubox-kiosk.sh

What this teaches about debootstrap+qemu chroots

  • /run/systemd/system being bind-mounted from the host makes systemctl believe systemd is running, which breaks every offline operation. The wrapper-with-SYSTEMD_OFFLINE pattern is the standard fix.
  • policy-rc.d returning 101 is the OTHER half — blocks invoke-rc.d from trying to actually start daemons during apt operations.
  • For mountpoints needed by build-time hooks (raspi-firmware checks mountpoint -q /boot/firmware), bind-mount on self is the right primitive — NOT tmpfs. tmpfs throws away the postinst's writes ; self-bind preserves them in the underlying directory.
  • [[ -e symlink ]] follows the symlink target. From the build host's perspective, absolute symlinks created inside ${ROOTFS} point to paths outside ${ROOTFS}. Use relative symlinks (matches systemctl's own convention anyway).

Open follow-ups (non-blocking)

  • build-live-usb x64 amd64 — preexisting failure, separate from this chain.
  • build-mochabin-live-usb — same.
  • build-image espressobin-v7 / -ultra — preexisting hardware-profile issues, separate.
  • Release create-release job will keep failing as long as those four are red. APT publish for v2.13.10 is therefore not present — rpi-arm64 + amd64 .debs are still available from v2.13.4's APT repo, which is the current install path.

2026-05-31 — v2.13.3 → v2.13.4 cross-build finally green + media + kiosk regressions

Major release-pipeline day. The arm64 publish path that's been broken since v2.13.0 is finally green. Three releases shipped in one sitting.

Release pipeline saga (closes the v2.13.x cross-build chain)

  • v2.13.3 tagged → still failed. PR #428 (-a matrix.arch on dpkg-buildpackage) was correct but insufficient: with -a arm64 on the amd64 CI runner, debhelper invokes the cross-toolchain binaries (aarch64-linux-gnu-strip for dh_strip, aarch64-linux-gnu-objdump for dh_makeshlibs) which are not installed on the runner.
    • secubox-sentinelle-gsm arm64dh_strip: aarch64-linux-gnu-strip: No such file or directory
    • secubox-daemon arm64dh_makeshlibs: aarch64-linux-gnu-objdump: No such file or directory
  • #431 filed + PR #432 merged same hour — install binutils-aarch64-linux-gnu (~5 MB) in build-packages.yml when matrix.arch == 'arm64'. Folded into the existing apt step.
  • v2.13.4 tagged → APT publish green for the first time since v2.13.0.
    • 153 release assets, 5 arm64 (secubox-daemon, secubox-sentinelle-gsm, secubox-redroid, secubox-daemon-c3box, plus the SHA256 manifest)
    • apt.secubox.in/dists/bookworm/main/binary-arm64/Packages serves 15 arm64 packages
    • Three-fix chain complete: #425 (dh_shlibdeps -Xsecubox-redsea) → #427 (dpkg-buildpackage -a matrix.arch) → #431 (cross-binutils)

Nextcloud dashboard real data (#429, branch pushed)

  • secubox-nextcloud dashboard was returning hardcoded stubs (localhost:8080 URLs, empty users, host-path storage). Added _public_base_url() reading overwrite.cli.urltrusted_domains[] → fallback; rewrote /connections, removed the lxc_running() early-return from /users, switched /storage to lxc_attach du/df inside the container, added 3-pattern backup detection.
  • Branch feature/429-secubox-nextcloud-dashboard-api-renvoie pushed (commit b715c0e4), PR not yet opened (carry-over).
  • Live deployed to /usr/lib/secubox/nextcloud/api/main.py on gk2. Deploy mistake: my find | head -1 deploy detection took the alphabetically first match (/usr/lib/secubox/mail/api/main.py) and overwrote it with nextcloud's main.py. Caught from a head-mismatch, restored mail/main.py from packages/secubox-mail/api/main.py via scp, restarted secubox-mail (back active). Memo: deploy by exact path, never find | head.

Nextcloud upload-limits debugging (live patch on gk2)

  • Operator reported "erreurs de televersments" on cloud.gk2 / nextcloud.gk2. Traced through the chain:
    • PHP SAPI Apache had upload_max_filesize = 2M / post_max_size = 8M (Debian defaults — never bumped)
    • host nginx had no client_max_body_size directive at all (default 1M)
    • HAProxy/mitmproxy: no limit found
  • Wrote drop-in /etc/php/8.2/apache2/conf.d/99-secubox.ini bumping uploads to 4G + max_execution_time=3600. Initial drop-in didn't load: used # as comment prefix, which is invalid in PHP .ini (PHP wants ;). Switching #; made the drop-in scanned and applied.
  • host nginx: added client_max_body_size 4G + proxy_request_buffering off in nginx.conf http{} block.
  • Smoke test: PUT 5M then 50M to https://nc.gk2.secubox.in/remote.php/dav/... → 401 Sabre auth (expected, full body received). Backend OK.
  • Carry-over: cloud.gk2.secubox.in is NOT in any nginx vhost server_name. It falls through to the default_server which serves wrong-domain.html. Fix: add cloud.gk2.secubox.in to nextcloud.conf's server_name nc.gk2 nextcloud.gk2; line.

Media flash for operator

  • rpi400 microSD (/dev/mmcblk0, 28.8 G): gunzip → dd secubox-rpi-arm64-bookworm.img.gz (v2.13.4). 8.6 GB written at 38 MB/s. SHA256 . Kiosk MISSING despite --kiosk in CI — see #433 below.
  • amd64 live USB (/dev/sda, 28.8 G DataTraveler 3.0):
    • 1st stick: dd I/O errored at 268 MB, USB device disappeared from kernel. Stick is dying — classic NAND failure pattern (cf. feedback_test_with_two_usb_sticks.md).
    • 2nd stick (same batch): flash succeeded, 8.6 GB at 24 MB/s.
  • VBox VM SecuBox-live-amd64-v2134 created from the same image: 2 CPU, 2 GB RAM, BIOS firmware, NAT with port forwards (2222/8080/8443/9080). Boots to the SecuBox kiosk login screen; "Invalid credentials" inline error on wrong password but no lockdown (see #434).

Issues filed (5 new)

  • #430 — Federate two SecuBox Nextcloud instances (OCM trusted-servers workflow, future dashboard tab + endpoints). Documentation + integration test queued.
  • #431 — CI cross-binutils (already fixed via PR #432 same day).
  • #433build-rpi-usb.sh --kiosk silently fails: image v2.13.4 ships without kiosk despite Kiosk mode installed and enabled log line. apt install for chromium/xserver/openbox fails in qemu-arm64 chroot (apt --fix-broken install), || warn swallows it, and the subsequent heredoc/seed steps don't end up in the rsynced rootfs (mechanism still unclear — needs set -e review + build-time assertion).
  • #434 — kiosk login lockdown after N failed attempts (CSPN brute-force hardening, default N=1, unlock via reboot / USB key / timed).

Tracking files

  • WIP.md / HISTORY.md (this entry) / TODO.md updated to reflect v2.13.4 victory, the 4 new issues, and the carry-over PR #429.

Lessons memorialised

  • (already in memory) Source-first ALWAYS — but deploy by exact path, not find | head. Cost me one mail/main.py restoration.
  • PHP .ini drop-ins: comments are ;, not #. # doesn't error, it just silently aborts the parse and you lose every directive after.
  • For arm64 in CI, dpkg-buildpackage -a arm64 requires binutils-aarch64-linux-gnu installed alongside it — debhelper resolves cross-tools by triplet name.

2026-05-30 — v2.13.1 + v2.13.2 release polish + media flash + 4 issues filed

Follow-up day on v2.13.0 to actually get the release pipeline green and put the resulting artefacts in the operator's hands.

  • v2.13.1 tagged — packaging fix: drop duplicate debian/compat in secubox-fmrelay + secubox-zkp-hamiltonian (debhelper refused when compat was declared both in debian/compat AND via Build-Depends: debhelper-compat). CI verdict: fmrelay built clean , but publish still blocked by secubox-sentinelle-gsm arm64 (different root cause).
  • #425 filed + fixed in same day — the sentinelle-gsm arm64 build failed because dh_shlibdeps tried to resolve a prebuilt aarch64 ELF (bin/secubox-redsea) on the amd64 CI runner. Runtime deps were already declared explicitly in debian/control, so dh_shlibdeps -Xsecubox-redsea in debian/rules is safe. Locally reproduced + fixed (PR #426).
  • #423 filed + fixed — build-rpi-usb.sh --kiosk flag was a stub: parsed but never installed chromium/X/openbox and never created secubox-kiosk.service (which the boot menu's apply_mode already references). Added the full install block (apt, kiosk files from image/kiosk/, systemd unit on vt7, default boot mode = kiosk, default target = graphical) AND wired extra_args: "--kiosk" on the rpi400 matrix entry in build-all-live-usb.yml so Pi 400 artefacts ship chromium-fullscreen-by-default (PR #424).
  • #422 filed — vm-x64 image v2.13.0 boots in VirtualBox but with a cascade of [FAILED] (otg-gadget, networkd-wait-online, mitmproxy, crowdsec, net-fallback, openclaw in restart loop) → sshd accepts TCP but no banner. Root cause: appliance-only services (secubox-otg-gadget needs configfs/USB gadget kernel) aren't gated on a VM profile, plus networkd-wait-online is strict. Proposed: mask the hardware-only units on the vm-x64 build profile.
  • #421 filed — /run/secubox/{authelia,cookies,certs}.sock are bound by services running in a private mount namespace (RuntimeDirectory=secubox + the host dedicated /run/secubox tmpfs mount = collision), invisible to nginx (host) → 502 on /api/v1/cookies + /api/v1/certs and 500 on every Authelia auth_request consumer (lyrion was the loudest). Live workaround: commented out the 4 Authelia auth_request lines in the lyrion vhost (lyrion now 200). Real fix needs reconciling tmpfs-mount vs RuntimeDirectory, reboot-tested.
  • v2.13.2 tagged after #425 + #423 merged → re-runs the release pipeline with: fmrelay + zkp + sentinelle-gsm packaging fixes (publish unblocked expected) + rpi400 kiosk-by-default. Watcher running.
  • Media livré à l'opérateur :
    • 🟢 USB live amd64 flashée sur Kingston DataTraveler 28,8 G
    • 🟢 microSD rpi400 flashée sur SanDisk SC32G 29,7 G
    • 🟡 VirtualBox VM SecuBox-amd64 créée (NAT port-forward SSH:2222, HTTP:8080, HTTPS:8443) MAIS bug #422 → kept off-disk pour rejouer une fois l'image corrigée.

2026-05-29 (cont.) — backlog sweep + v2.13.0 release

Closed a batch of fixed-live-but-unmerged issues by finalizing each branch (PR → merge → worktree clean), and cut the minor release.

  • #392 vhost-health: fold error (404/502) into the 🔴 bucket in the pre-computed summary path (PR #413).

  • #394 Nextcloud: drop the Authelia auth_request gate on the public vhost so the mobile client authenticates (PR #414; changelog conflict resolved → 1.4.1 above master's 1.4.0).

  • #152 Roundcube SQLite by default — config.inc.php.local DSN + des_key + schema init + sqlite3 CLI (PR #415).

  • #150 vortex-firewall postinst unmasks + enables nftables.service so LXC NAT survives reboot (PR #416).

  • #168 nginx reload no-op — diagnosed (orphan + stuck old master from a binary-upgrade re-exec; serving master reloads fine), live-cleaned to a single master, closed as operational (no package patch). Brief self-inflicted nginx blip during cleanup, recovered.

  • #395 WAF: skip cred-004 on NC mobile login + WebDAV (PR #417). Caveat: landed in the secubox-waf addon copy; live addon is the secubox-mitmproxy copy — the two-copy drift is a separate cleanup.

  • #389 navbar resort (PR #418); #391 secubox-system-tuning RAM package (PR #419); #377 secubox-fmrelay package — rtl_fm→Icecast+RDS (PR #420).

  • #153 confirmed already squash-merged (PR #160); git cherry had mislead.

  • Worktree tree fully tidied (verified each branch's content was in master via PR merge-commit/squash before removing).

  • Release v2.13.0 (minor bump from v2.12.19): tag push triggers the release pipeline → all packages + system images (mochabin/real-ARM, espressobin-v7/ ultra, vm-x64/amd64+VirtualBox, rpi400) + APT publish.


2026-05-29 — WAF unban + NC 32 upgrade + user provisioning (#410) + Companion personas (#409)

Long live-ops + feature session on gk2. Master commits pushed: 0bf67891 (WAF), 787c6b03 (LXC DNS+FAQ), 3ff008aa (NC32 pin).

  • WAF lockout fixed (fix(waf)): mitmproxy secubox_waf.py whitelist was an exact-match 2-IP set, so a LAN operator whose request matched a rule (e.g. a logout) hit the 3-strikes ban with no bypass. Made it CIDR-aware and trusted loopback + RFC1918 (LAN + internal LXC net). Live in the mitmproxy LXC + backported to packages/secubox-mitmproxy/addons/secubox_waf.py. Operator IP 192.168.1.13 unbanned (CrowdSec had no decision — it was the in-memory 403). The historical "router-goform on gitea" false-positives in the 2026-05-08 threat log are from an older, since-tightened pattern (/goform/.*(\$\(|;|\)` is specific now). "Attacked dead sites" = bot probing counted pre-backend; not real visitors.
  • LXC DNS gotcha fixed (fix(nextcloud)): NC (and mitmproxy) containers had resolv.conf → systemd-resolved stub 127.0.0.53 with no upstream, so DNS died (appstore/addons not shown, update checks fail) while NAT egress worked. Root fix: resolved.conf.d upstream DNS=1.1.1.1 9.9.9.9. Applied live to NC + mitmproxy LXCs, persisted in nextcloudctl base install, documented in docs/FAQ-LXC-DNS.md (closes the WIP "LXC template wiki: DNS" carry-over).
  • Nextcloud 31.0.14 → 32.0.10 major upgrade (PHP 8.2 OK for NC32). DB dump (mysql, not sqlite — the outlier) + config.php backed up to data/_pre32_backup/ + 825M tree backup first; ran updater.phar + repair + db:add-missing-indices. All 3 hostnames 200 (added missing cloud.gk2 mitmproxy route). Source NC_VERSION bumped to 32.0.10.
  • NC config-warning hardening (live): trusted_proxies 10.100.0.0/24+lo, overwriteprotocol=https, memcache.local=APCu (fixes DB-locking too), PHP memory_limit=512M + opcache.interned_strings_buffer=16 (apache SAPI), HSTS, missing indices, maintenance window, default_phone_region=FR, php8.2-gmp + imagick SVG delegate.
  • NC user cleanup: removed the 4 non-SecuBox users (bat, bourdon, lemurien, ragondin) → NC now only admin + gk2 (master-users rule). Data backed up to /data/backups/nc-removed-users-20260529.tar.gz (71M, restorable).
  • #410 user provisioning push (branch feature/410-…, pushed): SecuBox is source of truth. user_store.set_password() (single write path = capture point) + list_users(); host CLI secubox-user-sync (set/seed/sync/list) writes the canonical argon2 hash then pushes the same plaintext (env/stdin, never argv) into each app's new <app>ctl user-provision verb (nextcloudctl, photoprismctl), creating per-user photo folders /data/shared/photos/<user> and user-scoped libraries. Validated live with a throwaway user (caught: NC sudo strips OC_PASS → stdin+--preserve-env; PhotoPrism CE gates non-admin roles → default role + --upload-path). 13/13 unit tests. Seeding gk2/admin is the operator's step (secubox-user-sync seed — sets their passwords).
  • #409 Companion personas (earlier): avatar = persona holding a group of cookie auths; selectable picker (auto-discovers logins), one-click "Become" (group restore on any LAN machine), avatar webui "Personas" tab. Client- encrypted, on-box ciphertext only. .xpi rebuilt; wiki updated.

2026-05-28 — PeerTube LIVE end-to-end (upload confirmed)

PeerTube install completed on the gk2 LXC (10.100.0.120) and verified working at https://peertube.gk2.secubox.in/ including video/avatar upload. Resolution of the 2026-05-27 "in flight" install:

  • pnpm install finished after retry with MSGPACKR_NATIVE_ACCELERATION_DISABLED=1 (arm64 native binding for msgpackr-extract is optional perf-only; JS fallback works).
  • Ownership: chown -R peertube:peertube /var/www/peertube — the config/ and storage/ subdirs were root-owned, blocking storage/logs creation (EACCES) and config template copy.
  • Node 20 → 22.22.2: PeerTube 8.2.0 requires Node ≥22. Installed via the signed NodeSource apt repo (keyring + sources.list added manually; the curl | bash setup script is blocked by policy).
  • production.yaml patched: webserver.hostname=peertube.gk2.secubox.in
    • https=true + port=443; listen.hostname=0.0.0.0 (so nginx on the host reaches the LXC bridge IP); 64-byte hex secrets.peertube; DB password secubox; admin email admin@cybermind.fr.
  • Initial admin (auto-generated first boot, logged once): root / gisatejewumefatibedu — operator must rotate via UI.
  • "No upload button" turned out to be a stale browser bundle (the service bounced ~6× during install + Node swap). Server side was perfect throughout: role Administrator, quota -1, root_channel present, /videos/publish + /videos/upload both 200, client fully built. Hard-refresh resolved it; upload confirmed by operator.

Source drift NOT yet backported (Source-first follow-up): the live install path is native-in-LXC (Node 22 + pnpm + systemd peertube.service inside the container), whereas packages/secubox-peertube (#388 branch) describes a Docker/Podman API-managed model, and #390's vhost conf still lacks the full public vhost (9000 port + streaming timeouts). Both need reconciliation — see TODO P0.


2026-05-27 (afternoon/evening session — peertube + photoprism + mail + WAF)

Continuation of the morning consolidation session. Operator-driven work on three fronts: peertube package + container + URL, photoprism LXC + Nextcloud-shared photos folder, and mail/WAF/SSO unblocking mobile clients. Eight new PRs (#388#395) and substantial live-fix work on gk2.

Container infra (gk2)

  • 2 new LXC containers at /data/lxc/{peertube,photoprism}/, each at 10.100.0.{120,130}. Both started life with the strict common.conf + userns.conf + apparmor.profile=generated template default, which blocked unprivileged operations (postgres-15 postinst chown, podman CNI bridge iptables, even basic apt). Fixed by aligning their config with the working matrix LXC template: single lxc.include = /usr/share/lxc/config/debian.common.conf, drop the apparmor lines, keep the lxc.idmap UID mapping.
  • Bind-mount UID ownership pattern captured the hard way: bind mounts default to host-root ownership which the LXC root (UID 100000 outside) cannot chown. Both peertube postgres + photoprism storage failed with "Operation not permitted" until host-side chown -R 100000:100000 /data/<service>/ opened the dirs for the LXC. Recipe should ship in the LXC template wiki.

IP forwarding / NAT (gk2)

  • All LXC containers were silently cut off from external internet (apt update inside any container → DNS Temporary failure → fail). Root cause: 99-secubox-hardening.conf sets ip_forward = 0; my 90-secubox-lxc-forward.conf from the consolidation session was alphabetically earlier and lost. Renamed to 99-secubox-zz-lxc-forward.conf so it wins. Also added net.ipv4.conf.all.forwarding = 1 explicitly (the legacy ip_forward key alone isn't enough on modern Debian).
  • Masquerade rule targeted eth2 which is DOWN — real WAN is lan0 (192.168.1.200/24, default route via 192.168.1.254). Added oif lan0 masquerade to inet nat postrouting, traffic now egresses correctly. Should backport to secubox-system-tuning.

Swap (gk2)

  • Added /data/swapfile2 (4 GB) bringing total swap to 8 GB (was 4 GB from session #391).

PhotoPrism — LIVE (#388 follow-up)

  • secubox-photoprism LXC running podman → official docker.io/photoprism/photoprism:latest (Ubuntu 26.04 / 2.59 GB image) with --network=host (CNI bridge can't add iptables rules in unprivileged LXC), PHOTOPRISM_HTTP_PORT=2342, sqlite DB.
  • Bind mounts: /var/lib/photoprism/originals/data/shared/photos (shared with NC), /var/lib/photoprism/{storage,import}/data/photoprism/{storage,import}.
  • Public URL https://photoprism.gk2.secubox.in/ wired:
    • nginx vhost /etc/nginx/sites-available/photoprism.conf → 9080 → 10.100.0.130:2342
    • HAProxy ACL host_photoprism_gk2_secubox_in added to both http-in + https-in frontends → nginx_vhosts backend
    • URL returns 307 (PhotoPrism login redirect) ✓
    • login: admin / secubox-CHANGE-ME (must rotate)

Nextcloud — bind mount + URL alias + SSO removal (#394)

  • Photo sync ready: /data/shared/photos bind-mounted into NC at /var/www/nextcloud/data/Photos (NC LXC config edit, requires restart). Smartphone NC client → "Photos" folder → PhotoPrism auto-indexes via the shared dir.
  • URL nextcloud.gk2.secubox.in worked end-to-end by:
    • mitmproxy haproxy-routes.json entry added (nextcloud.gk2.secubox.in → 192.168.1.200:9080) — TWICE, once on host's /srv/mitmproxy/haproxy-routes.json and once on the LXC's separate copy (live config drift — see TODO).
    • nginx vhost server_name aliased: nc.gk2.secubox.in nextcloud.gk2.secubox.in;
    • mitmproxy LXC service restarted to pick up the new routes
  • SSO removed from NC vhost so official Nextcloud mobile client (which uses HTTP Basic + app-password, not browser cookies) can authenticate. Comment-out preserves the four auth_request / error_page 401 / auth_request_set / proxy_set_header X-Forwarded-User lines for revert symmetry. Source backport in #394 → secubox-nextcloud 1.3.5.
  • NC bruteforce protection disabled at runtime + table truncated. The SSO loop had triggered NC's own bf throttle; operator re-enables via occ config:system:set auth.bruteforce.protection.enabled --value=true after confirming mobile reconnect.

WAF cred-004 false-positive (#395)

  • After SSO unblock, NC mobile client hit a NEW 403 wall: WAF rule cred-004 ("JWT/token in URL", severity=critical) matches NC's legitimate /index.php/login/v2/poll?token=… polling, banning the client IP after BAN_THRESHOLD=3 polls (every ~1-2 sec).
  • Fix: path-based early-return skip in packages/secubox-waf/mitmproxy/secubox_waf.py check_request() for /index.php/login/v2/ and /ocs/v2.php/core/login. Source bumped to secubox-waf 1.1.3; live patch applied to gk2's /data/lxc/mitmproxy/rootfs/srv/mitmproxy/secubox_waf.py.
  • Verified: 0 new 403s in 60+ sec post-fix vs 1/sec before.

CrowdSec allowlist (gk2)

  • Created cscli allowlist secubox-trusted with 4 internal nets (192.168.1.0/24, 192.168.255.0/24, 10.100.0.0/24, 10.0.3.0/24). Operator's public IP not added (would need user input).

LXC DNS systemd-resolved gotcha

  • Fresh download-template LXCs default to systemd-resolved stub resolv.conf (127.0.0.53) that has no actual nameservers configured. Both peertube + photoprism failed apt with "Temporary failure resolving 'deb.debian.org'" until I overwrote /etc/resolv.conf with nameserver 1.1.1.1 / 8.8.8.8. The fix doesn't persist across LXC restarts (systemd-resolved rewrites it). Should ship a stable resolv.conf in the LXC template bootstrap.

Peertube — install in flight at session end

  • LXC up at 10.100.0.120, all bind mounts + network OK, podman/ postgres prerequisites unblocked by the template fix + bind-mount chown. Install script at /root/install-peertube.sh running step 6/8 (yarn install --production --pure-lockfile, ~10000 packages on arm64). Monitor armed; service expected active in ~15-25 min.
  • Pre-emptively rewired host nginx peertube.conf proxy_pass → 10.100.0.120:9000 (was 127.0.0.1:9001 from the earlier Podman attempt). HAProxy ACL was already in place from #390.

Live config drift discovered (mitmproxy)

  • /srv/mitmproxy/haproxy-routes.json on the host is NOT bind-mounted into the mitmproxy LXC. Each has its own copy, edits to one don't reach the other. Tripped me up when adding the nextcloud.gk2.secubox.in route. Recipe: also bind-mount the LXC's /srv/mitmproxy/ from host. Same applies to secubox_waf.py. Filed in TODO.

2026-05-27

Consolidation pass — full per-cluster audit + 2 real merges + 5 naming sweeps + gk2 deploy

Phase 1 audit of all 141 secubox-* packages (docs/superpowers/plans/2026-05-27-secubox-consolidation-audit.md, generated by re-runnable scripts/audit-packages.py). Key meta-finding: the audit's projected reduction (~100 packages, 28%) was based on naming-affinity intuition that didn't survive per-package code inspection. Realistic floor revised to ~135-137 (4-5% reduction).

Where consolidation actually paid off:

  • #378 (Tier 0) — secubox-c3box binary-package-name collision: Python dashboard + Go daemon both built a .deb named secubox-c3box. Renamed the Go variant to secubox-daemon-c3box with Conflicts: for clean upgrade. Not yet on gk2 — Arch:any, needs arm64 cross-compile (secubox-daemon not currently installed on the board, so non-acute).
  • #380 (Tier 1) — pruned 2634 LOC of dead source from the three already-transitional mail packages (secubox-mail-lxc, secubox-webmail, secubox-webmail-lxc): api/, nginx/, www/, menu.d/, dead .service files, misleading README. Each now ships only debian/.
  • #381 (Tier 2, real merge) — folded secubox-mmpm into secubox-magicmirror: 334-line FastAPI app → APIRouter mounted at /mmpm. Frontend URL: /mmpm//magicmirror/mmpm/. Old package becomes transitional. -1 effective package.
  • #384 (Tier 3, real merge) — folded secubox-master-link into secubox-p2p: surprising finding that master-link's 851-LOC API was effectively dead on production (no nginx config → unreachable via web; the visible /master-link/ UI was being served by p2p all along). Operator-scoping calls made and documented in the commit. -1 effective package, -1284 LOC dead code.
  • #382 / #383 / #385 / #386 / #387 — five thematic Description-clarity sweeps. After these land, NO secubox-* package on master ships an "X Module" placeholder headline anymore. 24 Descriptions clarified, 9 maintainer placeholders corrected from SecuBox <dev@secubox.local> → Gerald.

Where consolidation did NOT pay off (decisions recorded in audit doc): streamlit/forge (distinct workflows, lxc-dep cost), dpi cluster (4 packages = clean two-layer + two-engine + consumer split), dns cluster (5 distinct DNS subsystems, no config overlap), threats cluster (7 distinct security capabilities with different backends).

Build tooling fix: scripts/build-packages.sh had a hardcoded 30-package allowlist that silently skipped the other 110 of 141 packages. Replaced with dynamic glob over packages/secubox-*/ with debian/control — core first, metapackages last. Surfaced today when 18 of 31 touched packages had to be built manually.

Deploy: 30 of 31 .debs apt-installed on gk2 (root@192.168.1.200). secubox-ndpid dropped from batch (depends on ndpid | ndpi-reader, not in board's apt sources). All transitional Breaks/Replaces fired cleanly (mmpm + master-link old payloads removed). 24/24 expected- active services running post-deploy; secubox-magicmirror.service inactive is correct (ConditionPathExists=/etc/secubox/magicmirror/ enabled opt-in unit). 3 orphan nginx snippets manually removed — pre-#380 leftovers that dpkg's .list tracked but new .debs didn't ship; recommend follow-up to add rm -f in transitional postinsts.


2026-05-26

SOC firewall_summary cache hardened (v2.12.17 + v2.12.18)

v2.12.17 (40b58372) — Moved the nftables cache populator from image/firstboot.sh into the secubox-hub package. Ships /usr/sbin/secubox-nft-cache, secubox-nft-cache.{service,timer} (30 s tick, auto-enabled via timers.target.wants/ symlink), plus a sudoers fragment authorising user secubox to call nft list * and the restart of one specific unit. Co-locating crons with the module that consumes their data is now the canonical SecuBox pattern.

v2.12.18 (a1c60d6b) — /firewall_summary fallback ladder rewritten: fresh cache → realtime via sudo → stale cache → none. Discovered at deploy time that NoNewPrivileges=true on secubox-hub.service blocks setuid traversal, so sudo from inside the hub silently fails. The stale-cache fallback prevents the SOC widget from ever rendering zeros even when realtime is denied. All four code paths verified on gk2.

Also documented a board-side cleanup step needed when upgrading from v2.12.16: remove the two stray cron files (/etc/cron.d/secubox-nft-cache, /etc/cron.d/secubox-nft-stats) and the helper /usr/local/bin/secubox-nft-cache.sh left over from an earlier ad-hoc fix. They were racing the new systemd timer and occasionally producing a 0-byte JSON cache.

secubox-waf 1.1.2 — warm asyncio cache (v2.12.19)

v2.12.19 (30e89193) — Rewrote the data path for the three hot WAF endpoints (/stats, /alerts, /bans). Old design ran a full 200 k-line threat-log scan + GeoIP on every cache miss; the scan took longer than the 30 s TTL on this board, so the cache never warmed, HAProxy 504'd and the dashboard rendered empty while the WAF process burned 50-67 % CPU continuously. New design: a single asyncio background task refreshes a module-level _warm dict every 30 s off the request path; endpoints are O(1) dict reads. _read_log_tail() seeks 512 KB from end of log instead of readlines() so memory does not scale with log size. Cold-start hits compute synchronously once via asyncio.to_thread. Verified on gk2: /stats 200-900 ms (was 6 s+ or timeout), /alerts 360 ms, /bans 210 ms (was 5-15 s).

secubox-soc 1.0.1 — production dashboard captured back into source

0de665c5 — Source-side packages/secubox-soc/www/soc/index.html had silently been rewritten in commit 24296084 (April 15) into a 44 KB "threat-map" frontend that talks to /api/v1/soc/* (a secubox-soc-gateway backend that isn't running on production boards). Production boards have been running an out-of-tree 107 KB "comprehensive dashboard" aggregating /api/v1/hub, /api/v1/waf, /api/v1/crowdsec and /api/v1/hub/public/firewall_summary — never captured into source. First deploy of 1.0.1 from raw source clobbered the working dashboard and locked the operator out (login.html redirect, no gateway backend). Recovery: pulled the 107 KB file from /srv/www/soc/index.html (May 10 snapshot still on board) back into source, stripped the .logo* CSS overrides that re-styled the sidebar.js-injected SecuBox brand as a 40×40 gradient box, rebuilt, redeployed. Source is now the canonical comprehensive dashboard.

Memory feedback_source_first_always saved as a hard rule: never bump+deploy a package without first diffing source against the running board's deployed copy. Board frontends are authoritative.

RuntimeDirectoryPreserve mass-redeploy on gk2

Crowdsec endpoint 502s traced to the unix socket missing from /run/secubox/. Another secubox service had restarted with a unit file lacking RuntimeDirectoryPreserve=yes and wiped the directory for everyone. Source-side commit 24000d67 (v2.12.10 era) had already added the keyword to all 96 source units, but only 16/127 service files on gk2 had it — the other 111 packages had never been redeployed since the commit.

Rebuilt 100 packages (3 needed -d to skip python3-all build-dep not installed locally; scripts/build-packages.sh hardcoded list only covers 30/100 so the remaining 70 went through a one-off shell loop) and shipped them all to gk2 with dpkg -i --force-confold. The cascade of 100 unordered postinst restarts overwhelmed the Marvell ARM board; SSH and HTTPS timed out, an emergency reboot was needed and triggered an fsck on /data.

Post-reboot state: NTP auto-synced (TOTP login OK), 84/127 services with Preserve, 41 services without RuntimeDirectory= at all (so Preserve is sans objet), 2 real stragglers remaining (secubox-torrent, secubox-voip) tracked in TODO P0.

wazuh postinst tolerates masked unit (63284497)

One of the 100 packages (secubox-wazuh) initially failed the mass-install: postinst called systemctl enable secubox-wazuh.service unconditionally, the operator had masked that unit at firstboot, systemctl enable returns exit 1 against a masked unit, set -e aborted the script, dpkg left the package half-configured. Recovery on board was unmask → dpkg --configure → re-mask to preserve operator intent. Source-side fix wraps enable+start in a systemctl is-enabled ... != "masked" check. Same pattern likely belongs in every other secubox-* postinst — audit tracked in TODO P0.

Memory entries saved

  • feedback_source_first_always — diff source vs board before any bump/deploy; board frontend is authoritative.
  • feedback_global_refactor_acceptable — cross-package mass refactors are correct when source-side fixes have stalled un-deployed; performance-first design (warm asyncio cache, off-request-path compute) preferred over correctness-only.

2026-05-24

Module dual-vhost split — MUST pattern + lyrion/zigbee/authelia alignment

Codified in docs/MODULE-GUIDELINES.md §4 (REQUIRED) + §5 (nginx template) + .claude/PATTERNS.md Pattern 12. Any module with a real upstream web UI MUST split:

  • admin.gk2.secubox.in/<module>/ = SecuBox static admin page (calls /api/v1/<module>/*)
  • <module>.gk2.secubox.in/ = real app at vhost root (so absolute asset URLs resolve)

The admin page's Open <App> UI → button MUST read its href from /api/v1/<module>/access at runtime — never hardcode the public hostname. Reverse-proxying the app under /<module>/ is now a forbidden anti-pattern (breaks LMS Material, Nextcloud, Grafana, …).

Three modules aligned (master commits b1718788, d4adc1a3, 54da8a7c):

  • secubox-lyrion 1.1.0/lyrion/ rewritten as static admin; lyrionctl access URLs corrected (LAN http://IP:9000/, public https://lyrion.gk2.secubox.in/); config_get strips TOML inline comments; admin "Open Music UI" button now reads from /access.
  • secubox-zigbee — admin "Open Zigbee Manager" button reads from /access.
  • secubox-autheliaautheliactl same config_get + emit_access_json fixes; public hostname default auth.maegia.tvsso.gk2.secubox.in; "Open SSO Portal" button reads from /access.

Live on gk2 — all three return the same shape, all three admin pages behave identically. Next: nextcloud + audit grafana/yacy/rustdesk for the same anti-pattern.


2026-05-22

MOCHAbin mPCIe slot J5 — EP06 GPIO investigation runbook (Issue #345)

Context: PR #255's DTS patch wired the UTMI PHY to cp0_usb3_1, so the mPCIe slot's USB pipe is alive (4 USB buses on gk2 vs 2 before). But plugging an EP06-E still produces no enumeration: lsusb blank for the slot, lspci -vvv reports DLActive- on the PCIe side. Suspect: the slot's W_DISABLE# / PWR_EN / WAKE# control lines are not declared in the DTS, so they come up in whatever default state the SoC pad config leaves them — likely holding the modem powered-down. No MOCHAbin schematic in the repo to pin down which CP0 GPIO is wired to J5.

Done:

  • #345 — added scripts/probe-mpcie-gpios.sh: empirical sweep that drives each unrequested CP0 GPIO line HIGH for a few seconds and watches dmesg / lsusb for a new USB device. Skips any line marked [used] by gpioinfo, uses libgpiod (gpioset --mode=time so the line auto-restores to input on release). Three modes: --baseline (snapshot only, no GPIO writes — safe to run anytime), --line gpiochipN K (single line, useful once a candidate is found), or no arg (full sweep across gpiochip1 + gpiochip2 = the two CP0 banks). Output to /var/log/secubox/mpcie-probe-<ts>.log.
  • Companion docs/hardware/mochabin-mpcie-ep06-runbook.md documenting the hypothesis, the procedure step-by-step, and the DTS template (rfkill-gpio block patterned after cn9132-clearfog.dts lines 69122) to apply once a candidate GPIO is identified.

Validation: --baseline smoke-tested on gk2 — produces correct gpioinfo dump, identifies the [used] lines (cp0_gpio0 line 0 "reset", line 12 "PHY reset", line 30 "shutdown", plus the SFP+ pca9554 expander chip 3). No spare modem expected; if probing finds no candidate the next escalation is multimeter scoping of J5 pins 2/20/24/39/41/52.

Incident — v0.1.0 blanket sweep crashed gk2: running the "all unused inputs" sweep with no modem in J5 took the board hard-down within seconds (host became unreachable; ARP stopped responding). gpioinfo's [used] tag only reflects lines the kernel requested, not lines that are physically wired — several unrequested CP0 GPIOs drive critical board functions (eth switch reset, PCIe2 PERST#, pca9554 IRQ). v0.2.0 of the script removes the blanket-sweep mode entirely: defaults to dry-run candidate listing, requires explicit --commit --line CHIP N to drive any single line, hardcodes a DANGER_LINES skip-list, and holds gpiochip2 off behind --allow-chip2. Memorialised in memory feedback_gpio_blanket_sweep_crashed_board.md.


2026-05-21

secubox-nextcloud v1.3.0 — reverse-proxy + SSO gating + move to 10.100.0.21 (Issue #280)

Context: Package predated the SSO chain. On gk2 the /nextcloud/ URL served a static stub (alias to /usr/share/secubox/www/nextcloud/), the LXC was hand-patched from source's lxc.net.0.type=none to veth+10.100.0.20/24 — colliding with secubox-authelia at the same IP. No Authelia gating.

Done:

  • #280 / PR #281 (b86dee78): secubox-nextcloud v1.3.0nginx/nextcloud.conf now reverse-proxies /nextcloud/ to 10.100.0.21:80 with Apache-friendly headers and SSO-gates via auth_request /__sbx_auth_verify + error_page 401 = @sbx_auth_login (handler from secubox-authelia v1.0.8). sbin/nextcloudctl LXC template moved from type=none (host-mode) to veth+br-lxc+10.100.0.21/24, LXC_PATH=/data/lxc, env-override knobs per MODULE-GUIDELINES §3. Frees 10.100.0.20 for authelia. Operator rebind recipe in the PR.

Validation: Hot-rebind on gk2 (sed -i s/10.100.0.20/10.100.0.21/ /data/lxc/nextcloud/config + restart), deployed nginx conf, both LXCs now reachable at distinct IPs. /nextcloud/ no-cookie → 302 to /auth/?rd=…/nextcloud/. Apache returns 400 for plain / because of Nextcloud trusted_domains — operator must add admin.gk2.secubox.in to config.php for the post-SSO page to render.

Context: After v1.0.5 the browser flow on https://admin.gk2.secubox.in/zigbee/ exhibited an infinite redirect loop: login succeeded, returned to /zigbee/, then nginx auth_request returned 401/403 → 302 back to /auth/?rd=… → loop. Three independent bugs stacked.

Done:

  • #272 / PR #275 (f6439aeb): secubox-authelia v1.0.6install-lxc.sh now renders TWO session.cookies[] entries (maegia.tv + ${SECUBOX_HUB_DOMAIN}, default gk2.secubox.in) plus matching access_control rules. Authelia returned 403 on /auth/api/state because no cookies[].domain matched admin.gk2.secubox.in. New env knob SECUBOX_HUB_DOMAIN.
  • #273 / PR #276 (a907b5ba): secubox-zigbee v2.4.4 + secubox-lyrion v1.0.7@sbx_auth_login redirect changed from $http_host to $host to strip the internal :9080 nginx port that was leaking into public redirects. Lyrion vhost also switches $scheme:// → hardcoded https://.
  • #274 / PR #277 (fe37e2c7): secubox-authelia v1.0.7 — nginx /__sbx_auth_verify and FastAPI /verify now forward X-Original-URL + X-Forwarded-{Method,Proto,Host,Uri,For} to Authelia's /api/verify. Without these, Authelia defaulted to the first cookies[] entry (maegia.tv), never found the gk2.secubox.in session, returned 401 → infinite loop after successful login.

Validation: Hot-deployed nginx confs + api/main.py onto gk2 + sed-patched the live LXC config to add gk2.secubox.in + *.gk2.secubox.in to access_control (source-side install-lxc.sh changes don't touch the running config — caveat memorialised in memory). Also restarted z2m which had silently hung 14h with no listener on :8080 — root cause was z2m needing ~20s post-restart to bind. Board load dropped 359 → 7 once the auth loop stopped. Final state: /zigbee/ returns clean 302 to /auth/?rd=https://admin.gk2.secubox.in/zigbee/ (no :9080), /auth/api/state → 200, z2m UI loads after login. User confirmed.


2026-05-19

v2.10.0 + v2.10.1 — fresh-image login chain end-to-end, VBox amd64 + bare-metal x86_64 (Issues #218, #220, #222, #224)

Context: v2.10.0 closed the admin / secubox login chain from firstboot through image build (apt-get -f), web URL refactor (sed across 67 files), and Debian packaging (compat dup). v2.10.1 patched the kiosk for Debian 12 AppArmor on bare-metal. End state: fresh USB-booted x86_64 hardware reaches the SecuBox login form, accepts the default credentials, and presents the forced password-change UX — same path as VBox.

Done:

  • #218 / PR #219 (081d058d): image/build-image.shdpkg -i --force-depends left ~15 Debian-only Python deps uninstalled (python3-argon2, python3-jose, python3-cryptography, python3-jsonschema, python3-maxminddb, python3-websockets, python3-evdev, python3-pyroute2, python3-zmq, python3-serial, python3-rich, python3-pyotp, python3-qrcode). Added apt-get install -f -y --no-install-recommends after the slipstream step. Three CI iterations to land the minimal diff: v1 (all 15 in INCLUDE_PKGS) → python3-zmq broke debootstrap second-stage; v2 (just argon2) → same C-ext postinst issue; v3 (zero new in INCLUDE_PKGS, only the apt-get -f step) → green. Comment line "skip apt-get -f as pip provides Python deps" was a myth — those deps don't exist on PyPI under matching names.
  • #220 / PR #221 (95db06e7): deleted packages/secubox-health-doctor/debian/compat. dh refused to clean because compat 13 was declared in both debian/compat and Build-Depends: debhelper-compat (= 13). 132 other packages use the modern single-source pattern.
  • #222 / PR #223 (99fc829b): sed /portal/login.html/login.html across 82 occurrences in 67 source files. PR #169 had moved login.html to the secubox-portal root (/usr/share/secubox/www/login.html) but missed updating the inverse links. nginx try_files was falling back to hub's index.html, whose checkAuth() redirected to /portal/login.html → infinite loop. Diagnosed via offline image dump (debugfs on dd-extracted ROOT, no sudo) when the access log showed identical 200-served bytes for two different URLs.
  • v2.10.0 tag — bundles #218 + #220 + #222.
  • #224 / PR #225 (e0e3a335): image/sbin/secubox-kiosk-launcher — always pass --no-sandbox to chromium, not just in the VM branch. Debian 12 ships AppArmor with unprivileged_userns restriction, chromium's zygote needs unprivileged user namespaces, restart loop until MAX_FAILURES=3 disabled the kiosk on bare-metal. The kiosk runs as a dedicated unprivileged secubox-kiosk user on an isolated VT in a closed-network appliance with AppArmor per-process chromium profile still applied — internal renderer sandbox provides marginal value vs. AppArmor while breaking the kiosk entirely. Diagnosed from /tmp/kiosk-session.log showing the literal you can try using --no-sandbox workaround text from chromium's own error message.
  • v2.10.1 tag — bundles #224.

Validation:

  • VBox amd64 boot (CI run 26088128896 SHA 0da47bad) — kiosk reaches login form, admin / secubox accepted, forced password-change UX appears.
  • Bare-metal USB boot (Kingston DataTraveler 28.8 GB, CI run 26091295653 SHA e8784388) — same path, plus working secubox-kiosk-launcher v3.3 with chromium fullscreen.

Non-obvious learning (saved to memory as reference_ci_artifact_source.md): build-image.yml pulls secubox-debs-all from the latest SUCCESSFUL build-packages.yml run on ANY branch (via dawidd6/action-download-artifact@v3, default workflow_conclusion: success). Cost one CI cycle today when PR #223's sed fix was on the test branch but the .debs came from master's last successful build-packages run. Mitigation: always run build-packages.yml on the test branch first and ensure 0 failures; otherwise action-download-artifact silently falls back to master's previous artifact.

Follow-up filed:

  • #226 / PR #227 — bare-metal x86_64 polish: ConditionArchitecture=arm64 on three Pi-only services (healthbump, led-trigger, picobrew) that were [FAILED]-looping on x64; expand X11 driver coverage from fbdev-only to the standard xorg set (intel/amdgpu/radeon/nouveau/qxl/vmware/vesa/fbdev) for faster bare-metal cold-init.

dropletctl static-publisher CLI — port from OpenWrt, supersedes PR #185 (Issue #196, PR #199 opened pending review)

Context: The Droplet FastAPI consumer (packages/secubox-droplet/api/main.py, 839 lines) has long shelled out to subprocess.run(["dropletctl", "publish", file, name, domain]) and parsed [OK]-bearing output — but the binary it expected (the OpenWrt-style static-content publisher) had never been ported to Debian. PR #185 / issue #181 had shipped a different dropletctl — a noun-verb wrapper over the running Droplet API socket (file upload, file list, file rename, etc.) — which solved a different problem. The user explicitly asked for the OpenWrt-style publisher, accepting that PR #185's wrapper would be superseded.

Workflow: Full brainstorm → spec → plan → subagent-driven execution loop in worktree feature/196-implement-secubox-droplet-cli-dropletctl. 11-task TDD plan; two-stage review (spec compliance then code quality) per task; final whole-branch code review caught 2 latent bugs not seen in per-task reviews, both fixed before merge with red-on-old test coverage.

Done:

  • Spec (commit 5e6e1d83): static-only v1.1.0, delegates HTTP-facing work to metablogizerctl site publish, per-package bats convention with PATH-shimmed stubs, design rationale recorded in docs/superpowers/specs/2026-05-18-dropletctl-design.md.
  • Plan (commit 133c2c0e): 11 tasks, 1230 lines, each with verbatim heredocs the subagents could follow without interpretation.
  • T1 scaffold (b9c781ab): bats harness, helpers.bash, 2 PATH-shimmed delegate stubs, dropletctl skeleton. Surfaced the PR #185 wrapper that this branch replaces; user-approved supersession.
  • T2T9 (87b9623b13ddc0a8): TDD increments — publish arg validation → name sanitization + HTML staging → ZIP extraction with single-nested-dir unwrap → tarball + plain-directory input → idempotency pin → remove → rename → list. Each task: 13 new bats cases, minimum-diff implementation, full review loop.
  • T8 hardening (0714d941): code-reviewer caught mv data-corruption (nests source inside existing target dir); added collision guard + old == new short-circuit + 2 new bats cases.
  • T10 packaging (5d490ea6): debian/changelog bumped 1.0.2 → 1.1.0-1~bookworm1 (Closes #196); debian/control Depends extended with secubox-metablogizer (>= 1.1), rsync, unzip, python3 (T5 reviewer caught the runtime-dep gap beyond the plan's metablogizer-only ask); debian/rules comment updated (install line was already present from PR #185, so no duplicate).
  • T11 lint (no commit needed): bash -n clean, bats 15/0, shellcheck not installed locally (deferred to CI).
  • Final review fix (550403df): cmd_rename switched from prefixed-FQDN to bare-base domain in TOML (Fix A — list was double-prefixing post-rename); cmd_publish now rejects domain values not matching ^[a-zA-Z0-9.-]+$ (Fix B — closes TOML injection via the API boundary).
  • Test strengthening (0c08ae06): test 16 rewritten to seed the legacy-FQDN scenario explicitly so it's genuinely red on the old buggy code (red-on-old verified by swapping the parent binary in).

Accepted plan deviations (each is a fix to a plan bug, all surfaced + reviewed):

  • T3: dropped local from local staging in cmd_publish so the EXIT trap can reference $staging under set -u (function-scoped local would unwind before the trap fires at shell exit).
  • T8: added 3 minor hardening items not in plan (collision guard + same-name short-circuit + asymmetry comments) per code reviewer's CHANGES REQUESTED.
  • T9: cmd_list reconstructs vhost from f"{name}.{domain}"; plan's literal https://{domain}/ would have rendered the bare base domain for every site, failing the test's first.mydomain.test assertion.

Final state:

  • 17 bats tests, 0 failures
  • HEAD 0c08ae06 on feature/196-implement-secubox-droplet-cli-dropletctl
  • PR #199 opened with full commit series + supersession note for PR #185 + follow-up backlog
  • Hardware deploy on gk2 (192.168.1.200) deferred per plan §"Hardware deploy" until PR merges

Followup (queued in PR body, not blocking):

  • flock around TOML writers for concurrency safety
  • Dedupe the 3 hand-rolled TOML regexes (upsert_toml_site, remove_toml_site, cmd_list) into a shared python helper
  • cmd_rename rollback on metablogizerctl publish failure (currently exits 5 with half-renamed state)
  • Replace [ON] literal in cmd_list with real metablogizerctl state query
  • packages/secubox-droplet/README.md + debian/dropletctl.8 man page

2026-05-17

amd64 VirtualBox test bundle + apt.secubox.in keyring fix

Context: User asked for a fresh amd64 VirtualBox image suitable for non-expert testers. The build host has no qemu-img, VBoxManage, or passwordless sudo; the CI pipeline (build-image.yml --board vm-x64) is the canonical path.

Done:

  • Downloaded the May-11 CI artifact (25661033196, tag v2.2.1-eye-remote) → decompressed → bundled with a pure-Python raw→VDI converter (raw_to_vdi.py, dynamic VDI, 1 MiB blocks, 33.3% sparse, signature 0xbeda107f validated) so the bundle is self-contained without VBox/qemu tooling on the build host.
  • Wrote verify.sh — 6-check self-test (file presence, CI SHA, local hash table, VDI header, GPT layout, ESP boot files). Locale-safe (LC_ALL=C sha256sum --quiet), runs on any bash + python3.
  • Rewrote README.md for first-time testers (TL;DR 4-command launch, credentials table root/secubox + admin/secubox, network defaults, troubleshooting table, "how to get fresher build" recipe).
  • Wrote FIX-PXE.md documenting the VBox 7 EFI quirk (\EFI\BOOT\BOOTX64.EFI skipped on fresh VMs with empty NVRAM) — three workarounds, with the NIC-disconnect one-liner as the cleanest.
  • Triggered a fresh CI build on master (25983400130) → failed in 6 min at chroot apt-get update with NO_PUBKEY F42E679EE3730EA1.

Root cause: https://apt.secubox.in/secubox-keyring.gpg was published ASCII-armored. apt's signed-by= directive accepts only binary OpenPGP keyrings. The subkey F42E679EE3730EA1 (rsa4096, signs the Release file dated 2026-05-12) was present in the armored file but unreadable to apt.

Fix on the server (192.168.1.200, hosts apt.secubox.in):

  • Backed up the armored keyring: /var/www/apt.secubox.in/secubox-keyring.gpg.armored.bak.<epoch>
  • gpg --dearmor in place: 3947 B armored → 2855 B binary
  • file reports OpenPGP Public Key Version 4, Created Tue May 12 07:25:56 2026, RSA (Encrypt or Sign, 4096 bits); User ID; Signature; OpenPGP Certificate
  • Standalone apt-get update against apt.secubox.in: green, fetches InRelease + Packages with no GPG error
  • Re-triggered CI (25983593168) → green in 14 min → secubox-vm-x64-bookworm.img.gz 968 MB

Bundle in output/ci-vm-x64-25983593168/: verified 6/6 (CI hash + local hashes + VDI header + GPT + ESP /EFI/BOOT/BOOTX64.EFI), 2.76 GiB VDI (34.5% sparse), commit 2eff4045 on master, built 2026-05-17.

Followup:

  • publish-packages.yml doesn't (re-)export the keyring — it's managed manually on the board, so the server-side fix should persist. If a future operator re-uploads as armored, the same dearmor step will be needed. Worth adding a gpg --dearmor-on-publish step to whichever script ships the key to /var/www/apt.secubox.in/ (not in-repo today).

2026-05-16

eye-remote: Phase 1 multi-gadget DHCP — N Pi gadgets coexist at L3 on eye-br0 (Issue #158, branch feature/158-eye-remote-multi-gadget-l3-dhcp-server-o)

Context: The L2 bridge (eye-br0) landed via #155 / PR #157 solved the hardware-level collision for N Pi RNDIS gadgets but left every Round image with the same static IP (10.55.0.2/30). With two gadgets attached the host ARP cache flapped between them; exactly one Pi was reachable at a time. Phase 1 adds L3 addressing: a dnsmasq instance scoped exclusively to eye-br0 issues per-MAC leases from 10.55.0.10.250, and Round images switch from static peer to DHCP client.

Done:

  • Parser library (Tasks 14): reservations.conf reader (per-MAC TOML-style), dnsmasq.leases reader (epoch + MAC + IP + hostname), IP assignment logic (pool 10.55.0.10.250, first-fit, MAC stable), Pydantic models GadgetLease / GadgetReservation / LeaseState — full pytest suite.
  • FastAPI router (Tasks 56): GET /api/v1/eye-remote/leases joins active lease file with reservation table, JWT-gated; wired into api/main.py lifespan.
  • dnsmasq config + systemd unit (Tasks 78): eye-remote-dnsmasq.conf bound to eye-br0 only, 24 h lease, dhcp-script=leasewatch.sh; dedicated secubox-eye-dnsmasq.service (masks Debian's system-wide dnsmasq.service, PIDFile=/run/secubox/eye-dnsmasq.pid).
  • Host helpers (Tasks 910): find-usb-serial maps USB gadget interface → Pi CPU serial via udev + sysfs, writes to /etc/secubox/eye-remote/serials/; leasewatch.sh dhcp-script hook auto-appends new MACs to reservations.conf and writes JSONL audit entries.
  • nftables (Task 11): narrow DHCP allow on eye-br0 only (udp dport 67); default DROP preserved.
  • Debian packaging (Tasks 1213): secubox-eye-remote ships dnsmasq config, systemd unit, nftables snippet, leasewatch.sh; postinst enables + starts secubox-eye-dnsmasq.service. secubox-system ships find-usb-serial + leasewatch.sh to /usr/lib/secubox/; version bumped.
  • Round image (Tasks 1415): usb0eye0 rename via .link udev predictable-name file; all references in build script + gadget composer updated. eye0 switched to DHCP client via systemd-networkd .network file; firstboot derives hostname from CPU serial (sbx-rnd-<serial_suffix>); build script enables systemd-networkd, disables competing dhcpcd + usb-network.service.
  • Integration test (Task 16): tests/scripts/test-eye-remote-multi-gadget-netns.sh — netns + two veth pairs simulate two Pi gadgets; verifies distinct leases, lease renewal, and find-usb-serial mapping. Skipped without root (EUID != 0), ready for privileged CI runners.
  • Docs + tracking (Task 17): remote-ui/round/MULTI-GADGET.md created with "Resolved by #158" banner + historic limitation documentation; WIP.md + HISTORY.md updated.

State:

  • Branch feature/158-eye-remote-multi-gadget-l3-dhcp-server-o pushed to origin; head after Task 17 commit is the tracking commit.
  • Live-board deploy deferred to user: scripts/deploy.sh only rsyncs Python API; full install requires .deb transfer + dpkg -i on the board (installs dnsmasq-base, masks dnsmasq.service, loads nftables rules into kernel).
  • Pi reflash deferred: Round image changes (Tasks 1415) need a fresh flash of a Pi Zero W to validate.
  • PR not yet opened — user decides when to open the review surface.
  • Phase 2 (explicit pairing approval before lease grant, NIZK handshake option) deferred to a separate follow-up issue.

Context: Test board 192.168.1.200 had both a Pi Zero W (rpiz, MAC 02:fb:00:00:11:03) and a Pi 4B (MAC 02:fb:00:00:d2:7f) plugged into its USB hub. Both enumerated cleanly as Linux Foundation Multifunction Composite Gadget (1d6b:0104) but every usb* net iface stayed in state DOWNsecubox-eye-remote.service was running but uvicorn's 10.55.0.1:8000 had no interface to bind to.

Root cause: Hand-installed /etc/systemd/network/10-eye-remote.link (not dpkg-owned) tried to rename every 02:fb:00:00:* + rndis_host interface to the fixed name eye-remote. With two matching gadgets the rename collided and systemd-networkd silently left them as usb0/usb1/usb2. The companion 50-eye-remote.network (and netplan's eye-remote ethernet stanza in 00-secubox.yaml) then never matched anything live, so 10.55.0.1/30 was never applied. The /etc/network/interfaces.d/usb0 ifupdown stanza was also dead (networkd is the active manager).

Fix (host-side, no Pi-side changes):

Verification on 192.168.1.200:

  • eye-br0 is UP with 10.55.0.1/24
  • Three L2 slaves: enx02fb00001103 (rpiz), enx02fb0000d27f (Pi 4B), plus a stale eye-remote (the rpiz's pre-fix rename; harmless leftover, will disappear on next hot-unplug)
  • secubox-eye-remote.service active, uvicorn listening on 10.55.0.1:8000, curl http://10.55.0.1:8000/health → HTTP 200
  • udev journal shows the bridge enslavement firing on the udevadm trigger retrigger; journalctl -t secubox-eye-remote clean

Followups:

  • Branch pushed to origin/fix/155-eye-remote-link-rename-collision-when-mu; PR opening deferred per user preference.
  • Round-image change for per-Pi peer IP (DHCP or MAC-derived static) — file as a separate issue when the Round image work resumes.

Context: Operator wants a compliance audit on their own infrastructure: confirm that every cookie effectively present in visitors' browsers is either Set-Cookie-traceable (server-emitted) or strictly_necessary. JS-set non-essential cookies (GA, Hotjar, Matomo, FB pixel) must be flagged because LCEN art. 82 / ePrivacy requires prior consent. The WAF already injects health-banner.js into every HTML response in transit through mitmproxy — that injection point is the only place that can see both server cookies (via the addon hook) AND browser-effective cookies (via a sibling script that snapshots document.cookie).

Done:

  • packages/secubox-mitmproxy/addons/cookie_audit.py — mitmproxy response hook that parses every Set-Cookie (full attrs: Domain/Path/Max-Age/Secure/HttpOnly/SameSite), sha256-hashes the value, and appends one JSONL record per cookie to /var/log/secubox/cookie-audit/server.jsonl. 7 unit tests.
  • packages/secubox-hub/www/shared/cookie-inventory.js — vanilla JS snapshotter that hashes document.cookie via SubtleCrypto sha256 and POSTs to /api/v1/cookie-audit/ingest at DOMContentLoaded, +2s, and on visibilitychange. Hard-capped at 8 snapshots/page.
  • packages/secubox-mitmproxy/addons/secubox_waf.py — extended the existing banner injection to load both scripts; two new CDN-config keys (cookie_inventory_url, cookie_audit_ingest_url).
  • packages/secubox-metrics/api/cookie_audit.pyCookieAuditAggregator + Classifier mirrors the cert_status pattern; joins server ledger ⨝ browser snapshots per (vhost, name), emits source ∈ {http, js, both}, flags rgpd_violation for source == "js" AND category != "strictly_necessary". 9 unit tests.
  • packages/secubox-metrics/api/main.py — wires the aggregator into the lifespan loop, opens CORS to POST, adds three routes: POST /ingest, GET /report?host=…, GET /summary. Hard caps (200 cookies/snap, 128B names, 512B UA). Refuses ingest when enabled = false.
  • common/secubox_core/config.pyget_cookie_audit_config() with built-in RGPD baseline patterns (8 strictly_necessary, 5 functional, 9 analytics, 8 marketing). Operator patterns MERGED on top by default; classifier_override = true to replace.
  • secubox.conf.example[cookie_audit] + [cookie_audit.classifier] documented.
  • READMEs of secubox-metrics + secubox-mitmproxy updated.
  • All 34 metrics tests still green + 9 new = 43 passing; 7 new mitmproxy tests passing.

State:

  • PR #159 squash-merged to master as a19486c9. Issue #156 auto-closed via Closes #156.

Follow-ups:

  • AppArmor profile for usr.bin.mitmdump needs /var/log/secubox/cookie-audit/** rw and /var/lib/secubox/cookie-audit/** rw (deferred until deployed).
  • Logrotate snippet for the JSONL ledger (deferred — first iteration aims at validating shape before hardening).

Mail stack Phase 2 (Rspamd) — live-deployed on admin.gk2.secubox.in, 13/13 gates green (Issue #153, branch feature/153-mail-stack-phase-2-rspamd-migration-roun)

Context: Continuation of the Phase 2 deploy paused yesterday at DKIM keygen. Bind-mount entries were missing in /var/lib/lxc/mail/config, rspamadm only exists inside the LXC (not on host PATH), and the hardcoded chown 100110:100110 in configure_rspamd_controller didn't match the actual _rspamd uid in the LXC's Debian image (107, not 110).

Done:

  • Appended 4 bind-mount entries (/etc/rspamd-keys, /var/lib/rspamd/{bayes,history,settings}) to /var/lib/lxc/mail/config, restarted the mail LXC.
  • Generated DKIM keypair for secubox.in/default (2048-bit) via lxc-attach -n mail -- rspamadm dkim_keygen. DNS TXT recorded in /data/volumes/mail/rspamd/dkim/secubox.in/default.txt — awaiting publication.
  • Re-ran configure_rspamd_milter to deploy the 8 local.d/*.{conf,inc} templates into the LXC's /etc/rspamd/local.d/. Re-ran configure_rspamd_controller for worker-controller.inc + secrets.inc.
  • Fixed secrets.inc ownership via lxc-attach -- chown _rspamd:_rspamd (kernel idmap maps inside-LXC uid 107 to the right outside-LXC subuid regardless of image). Rspamd then started cleanly.
  • Added HAProxy vhost rspamd.gk2.secubox.in → mitmproxy_inspector via haproxyctl vhost add. Added matching entry to mitmproxy LXC's /srv/mitmproxy/haproxy-routes.json and restarted mitmproxy. End-to-end curl returned HTTP 200, x-secubox-waf: inspected, body pong from rspamd/3.4.
  • Ran mailctl rspamd purge-legacy on the board — D9 health gate verified rspamc-controller reachable, then purged opendkim opendkim-tools spamassassin spamc spamd cleanly. Postfix/Dovecot stayed active throughout.
  • All 13 acceptance gates green: ports, milter, DKIM, modules, WAF path, SA/OpenDKIM absent, Phase 1 regression (5 production mailboxes + webmail WAF path).

Live-deploy fixes pushed as 637b2221:

  • packages/secubox-mail/lib/mail/rspamd.shconfigure_rspamd_controller chowns via lxc-attach instead of hardcoded host uid.
  • tests/scripts/test-mail-phase2-acceptance.sh — gate 7 grep widened (Messages scanned|Pools allocated); gate 12 dpkg call wrapped with || true so set -e doesn't silently abort the smoke when SA/OpenDKIM are correctly absent.

Pre-existing brokenness noted (unrelated to Phase 2):

  • /srv/haproxy/certs/ is missing on the board; the live haproxy still serves from in-memory config, but any reload would crash. Resolved this session — see "HAProxy cert recovery" below.

HAProxy cert recovery (2026-05-16 13:41 board-local)

Root cause: the running HAProxy was started 2026-05-15 17:37 from a config that pointed bind *:443 ssl crt at /data/haproxy/certs/ (95 .pem files). Later in the day, a haproxyctl config regen (triggered when adding the rspamd.gk2 vhost) emitted a new /etc/haproxy/haproxy.cfg that:

  1. Changed the cert directory to /srv/haproxy/certs/ (which didn't exist).
  2. Stripped every backend {…} block (9 backends dropped, including mitmproxy_inspector, webui_direct, nginx_vhosts, fallback, the metablog_* group, and gitea_ssh).
  3. Replaced the issue-#44 is_webui_admin regex routing of admin.gk2.secubox.in to webui_direct with a plain hdr -i ACL routing to mitmproxy_inspector.

Result: any systemctl reload haproxy would crash. The live process held the old (correct) config in memory and kept serving 26k+ SSL connections, hiding the breakage.

Recovery applied:

  • Symlinked /srv/haproxy/certs -> /data/haproxy/certs (so the haproxyctl-style path resolves to the real cert store, future regens stay valid).
  • Rebuilt /etc/haproxy/haproxy.cfg from bak.20260515-1531-pre-503-fix (latest known-good with 9 backends), grafted in the two intentional changes the regen had introduced:
    • acl is_webui_admin hdr(host) -m reg ^admin\.gk2\.secubox\.in$ + use_backend webui_direct if is_webui_admin (in both http-in + https-in).
    • acl host_rspamd_gk2_secubox_in hdr(host) -i rspamd.gk2.secubox.in + use_backend mitmproxy_inspector if host_rspamd_gk2_secubox_in (in both frontends).
  • haproxy -c validated cleanly. systemctl reload haproxy forked a new worker; master process stayed up (20h06m etime preserved). New worker took over without dropping the SSL listener.
  • Verified: rspamd.gk2.secubox.in (WAF inspected), admin.gk2.secubox.in (direct webui_direct), webmail.gk2.secubox.in (WAF inspected) — all return 200/expected status. Full Phase 2 smoke re-ran 13/13 green post-reload.

Defensive backup retained: /etc/haproxy/haproxy.cfg.bak.20260516-broken-by-vhost-add (the broken-by-haproxyctl-regen cfg, for forensic reference).

Followups before PR merge:

  • Investigate why mailctl rspamd install's first configure_rspamd_milter call didn't actually populate the LXC's /etc/rspamd/local.d/ (manual re-invocation worked) — possibly an early-bail before LXC_BASE/TEMPLATES_DIR reached the function.
  • Move the rspamadm dkim_keygen call into lxc-attach inside rspamd_keygen so DKIM keygen is automated on next deploy (currently errors rspamadm not on PATH).
  • Re-run rspamd-route-sync-patch.sh ordering so the mitmproxy LXC copy of haproxy-routes.json is updated reliably (host copy was, LXC copy wasn't on this deploy).

State:

  • Branch pushed, head 637b2221. PR not opened (per memory: no unprompted PRs).
  • Phase 2.5 (ClamAV) and Phase 3 (multi-domain DKIM) deferred per the spec.

2026-05-14

remote-ui Phases 1 + 3 merged to master (Issue #127, PRs #130 + #132)

Context: Closing out the remote-ui/square/ variant for Pi 4B/400. Phase 1 (extract remote-ui/common/) and Phase 3 (Pillow + /dev/fb0 kiosk) had both been implementation-complete with green tests/reviews but the PRs were still open pending hardware bench. Decision: squash-merge both now, drive Pi 400 manual bench (Task 24) live this session.

Done:

  • Triaged the build-eye-remote CI failure on PR #130: traced to pre-existing version drift — workflow env VERSION: '2.2.0' vs. build-eye-remote-image.sh:16 VERSION="2.2.1". One-line fix 0ba80c31 on the branch (.github/workflows/build-eye-remote.yml 2.2.0 → 2.2.1). CI re-ran green end-to-end including Compress + Upload artifact.
  • Squash-merged PR #130 (Phase 1) → 7c37415f on master.
  • Squash-merged PR #132 (Phase 3) → dee8bf8b on master.
  • Posted merge-complete comment on issue #127 enumerating remaining hardware gates (Tasks 18/19/23/24).
  • Rebased local-only docs commit a4de42c8 onto merged master.

State:

  • Issue #127 stays open per CLAUDE.md "Jamais de fermeture automatique" — Tasks 18 (round/ diffoscope), 19 (Zero W bench), 23 (Pi 4B bench), 24 (Pi 400 bench) all need user-driven hardware validation before close.
  • Phase 2 PR #131 remains closed (superseded by Phase 3 — kept on record for design rationale only).

Followups:

  • Build + flash secubox-eye-square_0.2.0_arm64.img.xz for Pi 400 hardware bench (Task 24, in progress this session). Build script remote-ui/square/build-eye-square-image.sh; CI workflow for square/ does NOT exist yet — add as Phase 4 followup so future contributors don't have to local-build.
  • Same image will satisfy Task 23 (Pi 4B sanity).
  • Worktrees 127-add-remote-ui-square-variant-for-pi-4b-7, 127-phase2-square-variant, 127-phase3-python-kiosk are now ready for scripts/agent-worktree.sh clean <#> — branches merged.

2026-05-13

Session 167 — Auth rework: secubox-users as identity source + TOTP 2FA (Issue #120, 19 tasks)

Goal: Replace plaintext auth.toml admin login with secubox-users-backed argon2id auth + RFC 6238 TOTP 2FA, kill-on-disable session revocation, CLI↔API parity through a single engine module, feature-flagged cutover.

Done:

  • Spec: docs/superpowers/specs/2026-05-13-secubox-users-auth-design.md
  • Plan: docs/superpowers/plans/2026-05-13-secubox-users-auth.md (19 tasks)
  • secubox_core.user_store + tests (canonical reader, auth.toml fallback path)
  • secubox_core.feature_flags + tests
  • secubox_core.auth rewired (jti, scope, session validator, disabled-reject)
  • secubox_users.engine (single mutation entry) — lifecycle / passwords / TOTP / sessions methods + tests (21 tests)
  • secubox_users.password_policy + tests (argon2id, 6-rule policy)
  • secubox_users.totp (pyotp + argon2id-hashed backup codes) + tests
  • secubox_users.migrate_v1_to_v2 (idempotent) + tests
  • secubox-auth branching /auth/login + /auth/login/mfa + /auth/totp/{enroll,confirm,disable} + /auth/set-password + integration tests
  • secubox-users API handlers refactored to delegate to engine; new endpoints with RBAC: /user/<u>/disable, /enable, /password, /totp/disable, /totp/backup-codes, /sessions, /sessions/revoke
  • /import now routes through engine.create_user (rejects password_hash injection)
  • usersctl rewritten in Python (13 subcommands, all delegate to engine) + CLI tests
  • CLI↔API parity test (3 mutation sequences diff-tested)
  • debian/control deps + debian/postinst: migration + seed admin + seed <hostname>, secubox user/group, env-var seed (no shell injection)
  • changelog: secubox-users 1.4.0-1~bookworm1
  • Live smoke script tests/scripts/test-users-auth-live.sh (callable post-deploy)
  • 70+ cumulative tests passing per package (run per-directory due to api/ namespace collision documented in pytest.ini)

Followups:

  • Build + publish the .deb (via APT pipeline) so the board can apt upgrade.
  • After deploy: run bash tests/scripts/test-users-auth-live.sh to validate.
  • Phase 2 cutover: flip [auth] enforce_v2 = true in /etc/secubox/users.feature_flags.toml on the board.
  • Encryption-at-rest for TOTP secrets — revisit under PARAMETERS double-buffer hardening.
  • "Sign in with Google" OIDC stays deferred (schema reserves google: null).
  • Top-10k SecLists wordlist refresh (currently ~100-entry starter in share/common-passwords.txt).
  • Frontend banner when /auth/health reports fallback or NTP unsync.
  • Cross-package pytest collection: rename api/ packages to break the namespace collision in a separate cleanup PR.

MetaBlogizer deploy webhook (Issue #113, sub-E of #49)

Goal: Close the umbrella #49. Auto-deploy on git push to metablog-* repos. HMAC-verified webhook + per-site lock + ring buffer + Gitea API installer.

Done:

  • Spec: docs/superpowers/specs/2026-05-13-metablog-deploy-webhook-design.md
  • Plan: docs/superpowers/plans/2026-05-13-metablog-deploy-webhook.md (8 tasks)
  • api/webhook.py: verify_signature, load_secret (cached), classify_payload, site_lock pool, git_pull, ring buffer (50 entries)
  • main.py: POST /webhook (public, HMAC), GET /deploys (JWT)
  • scripts/metablog-webhook-{install,uninstall}.sh — Gitea API, idempotent, --dry-run
  • Bash smoke tests/scripts/test-metablog-webhook.sh (3 gates)
  • 21 pytest cases (36 total in the api/tests/ suite, all green)

Followups:

  • Streamlit auto-deploy webhook (separate scope)

Fix — Hub sidebar mobile-mode auto-detect (Issue #114, PR #115)

Symptom: [Sidebar] Mobile mode: ON (touch: true, narrow: false) on a wide-window desktop with a touchscreen.

Root cause: packages/secubox-hub/www/shared/sidebar.js isTouchDevice() ORed 'ontouchstart' in window, navigator.maxTouchPoints > 0, and matchMedia('(pointer: coarse)'). The first two are true on hybrid touchscreen laptops where the mouse is the primary input, forcing mobile mode incorrectly.

Fix: Use only matchMedia('(hover: none) and (pointer: coarse)') — a primary-input check. False on a laptop with a mouse, true on phones and tablets. The ≤768px narrow-viewport fallback is unchanged.

Commit: ce1f270c · merged via PR #115 (9fbb784f).


2026-05-12

Session 165 — MetaBlogizer version dashboard UI (Issue #103, sub-D of #49)

Goal: Extend the existing /metablogizer/ list view with version-aware columns, filter, sort, and 60s polling — plus a per-site drill-in page. Consume sub-C's enriched API.

Done:

  • Spec: docs/superpowers/specs/2026-05-12-metablog-version-dashboard-design.md
  • Plan: docs/superpowers/plans/2026-05-12-metablog-version-dashboard.md (5 tasks)
  • index.html extended in place: 3 new columns (Version → Gitea releases, Streamlit 🎨 link, Updated relative time + ISO tooltip), filter input, sortable headers with ▲/▼, 60s auto-refresh paused via Page Visibility API, row-name drill-in link
  • New site.html drill-in (~220 lines): every site.json field + 3 external links (live, Gitea, Streamlit hidden when null), same CRT P31 phosphor theme as index
  • Smoke tests/scripts/test-metablogizer-ui.sh — 4 gates all green (file shape, drill-in anchors, HTML well-formedness, live reachability 200)
  • Fixed localStorage key bug discovered in code review: drill-in now uses canonical sbx_token (matches index.html and the rest of the Hub)
  • README updated with the dashboard + drill-in URLs

Followups:

  • Sub-E (deploy webhook) is the last remaining sub-project of #49.

Session 164 — MetaBlogizer site.json schema + version metadata (Issue #101, sub-C of #49)

Goal: Formal JSON Schema for site.json + Python validator/enricher (version/last_updated derived from git when absent) + backfill script + API extension. Unblocks sub-D (Dashboard).

Done:

  • Spec: docs/superpowers/specs/2026-05-12-metablog-site-schema-design.md
  • Plan: docs/superpowers/plans/2026-05-12-metablog-site-schema.md (8 tasks)
  • JSON Schema draft-07: packages/secubox-metablogizer/schema/site.json.schema.json
  • Python module: packages/secubox-metablogizer/api/site_schema.py (load_schema/validate/enrich) + 8 pytest cases, all passing
  • API wiring: _load_site_json() helper in api/main.py validates warn-only, enriches version/last_updated from git
  • python3-jsonschema added to debian/control
  • Backfill script: scripts/metablog-site-backfill.sh (creates missing, --force merges, auto-detects streamlit_app via Gitea probe)
  • 3-gate smoke: tests/scripts/test-metablog-site-schema.sh
  • Live run: 165 total, 104 created, 61 skipped (incl. 2 force-fixed), 0 failed

Discovered + fixed mid-execution:

  1. 2 pre-existing site.json files (money, evolution) were missing the required published field — invalid under the new schema. Fixed via bash scripts/metablog-site-backfill.sh --force --site <name> which merged in the missing field while preserving the existing title/description values.
  2. python3-jsonschema not on MOCHAbin initially — installed via apt-get install -y python3-jsonschema to enable smoke gate 2 (which validates all 61+ live site.json against the schema).
  3. Total is 165 not 166 — a site was removed since the audit in PR #97; orchestrator picks up whatever find returns.

Commits:

  • 77f8feef — feat: JSON Schema draft-07
  • 345e59be — feat: site_schema module + 8 pytest cases
  • 58a80b8f — feat: load_sites() schema-enriched output
  • 7ca8b468 — feat: python3-jsonschema in debian/control
  • 426b0b1f — feat: backfill orchestrator
  • d4b4160a — test: 3-gate smoke
  • a4ada6c2 — docs: backfill run summary

Followup: Sub-D (Dashboard) can now consume current_tag/streamlit_app/etc.


Session 163 — Streamlit Gitea version pinning (Issue #95, sub-F of #49)

Goal: Mirror the 28 directory-form Streamlit apps from /srv/streamlit/apps/ into Gitea as gandalf/streamlit-<app> with v1.0.0 tag, then extend streamlitctl with tag-pinned deploy + rollback, and surface the active tag in the FastAPI.

Done:

  • Spec: docs/superpowers/specs/2026-05-12-streamlit-gitea-version-pinning-design.md
  • Plan: docs/superpowers/plans/2026-05-12-streamlit-gitea-version-pinning.md (9 tasks)
  • Per-app ingest function (scripts/lib/streamlit-ingest-app.sh) — sibling of B's metablog version
  • Cherry-picked scripts/lib/gitea-ssh-preflight.sh from PR #97 (not yet merged to master)
  • Orchestrator (scripts/streamlit-ingest.sh) with preflights + JSON report + dot-dir/__pycache__ filter
  • 3-app smoke + idempotent re-run
  • Full run: 28/28 apps in Gitea (20 ingested-fresh + 8 skip-already-current, 0 failed). First pass had 20 broken-stub failures; bulk-deleted via API and second pass cleaned. Summary at docs/superpowers/runs/2026-05-12-streamlit-ingest-summary.md.
  • streamlitctl deploy <app> --from-gitea --tag <vX.Y.Z> — clone+replace in-place, backup to <app>.bak.<ts>, max 3 backups, .deploy.json record
  • streamlitctl rollback <app> — promote latest backup, sentinel <app>.bak.rolledback.<ts>
  • FastAPI _get_apps() enrichment (current_tag + deployed_at) via .deploy.json or git describe --tags --exact-match
  • Deploy/rollback cycle smoke (canary: yijing) all gates pass

Discovered + fixed mid-execution:

  1. set -- "${var[@]:-}" empty-array expansion produced a single empty positional arg, hitting the case default Unknown flag: . Fixed in both source-args and saved-args occurrences (commits 34a4760e, 7d522e9a).
  2. Auto-restart removed from deploy/rollback. cmd_start/cmd_stop in streamlitctl operate on the whole LXC, so restart-after-deploy would have killed all running Streamlit apps. Streamlit auto-reloads on file changes; operator can manually streamlitctl restart if needed (commit 15b2a737).
  3. 20 pre-existing broken Gitea stubs (DB entry without on-disk objects, same as B/#97) — bulk-deleted via one-shot admin token.
  4. .claude/ and __pycache__/ directories in /srv/streamlit/apps/ got picked up as app candidates by the initial find -type d — filter added (! -name '.*' ! -name '__pycache__').

Commits:

  • 66693d89 — feat: per-app ingest function
  • 7007d40c — feat: cherry-pick gitea-ssh-preflight helper from #97
  • 1a12c63f, 76379cf5, c8747067, 34a4760e, 7d522e9a — orchestrator + filters + empty-array fixes
  • e3ef78b9 — test: 3-app smoke
  • fc39bf2d — docs: full-run summary (28/28, 0 fail)
  • f975de18, 15b2a737 — streamlitctl deploy --from-gitea --tag + rollback
  • 6e7f698a — feat: FastAPI _get_apps() enrichment
  • a23b2f45 — test: deploy/rollback cycle smoke

Verification (5 random apps via git ls-remote): All return valid 40-char SHAs on main + v1.0.0.

Followup: API service on MOCHAbin needs restart after package upgrade for current_tag to surface (existing _get_apps() cached in-process). Not blocking the PR.


Session 160 — Health Banner Live Panel (Issue #92)

Goal: Add three public sections to the health banner — VisitorOrigin, LiveHosts, CertStatus — sharing one polling pipeline.

Spec: docs/superpowers/specs/2026-05-12-visitor-origin-feed-design.md Plan: docs/superpowers/plans/2026-05-12-visitor-origin-feed.md Branch: feature/92-health-banner-visitor-origin-feed-anonym Issue: #92

Highlights

  • nftables inet secubox_metrics table (priority -300) — independent of secubox-firewall.
  • VisitorOrigin: kernel-dedupe via timeout'd set, mmdb resolution, threshold gate before persistence (raw IPs never leave the function).
  • LiveHosts: HAProxy admin socket + 60x1-min ring buffer; counter-reset detection.
  • CertStatus: cryptography parse of /etc/letsencrypt/live/*/cert.pem.
  • Banner v1.3.0: three fail-isolated fetch loops on a shared 30 s cadence.

Session 160 — secubox apt + clone: validate against live repo (Issue #89)

Goal: Audit the 2026-05-11 plan vs the implemented Go CLI; close any genuine gap; end-to-end against https://apt.secubox.in/ (commissioned in Session 152 / Issue #80).

Done:

  • All 48 plan steps in docs/superpowers/plans/2026-05-11-secubox-apt-clone.md ticked after walking each against cmd/secubox/{cmd,internal/apt}/*.go. go test ./internal/apt/... ./cmd/... passes; binary builds; subcommand --help strings correct.
  • One real bug found during E2E: apt.Client.DefaultGPGKeyURL was https://apt.secubox.in/secubox.gpg, which doesn't exist on the public repo. nginx try_files falls through to index.html, so the Go code wrote a 2.5 kB HTML landing page as the keyring → apt update exit 100. Fixed by pointing at /secubox-keyring.gpg.bin (binary OpenPGP). The unit tests didn't catch this because they use httptest servers that don't hit the real URL.

Commits:

  • 4e8eadf4 — docs(apt): Tick plan checkboxes per code audit (ref #89)
  • 6e3276bb — fix(apt): Use binary keyring URL so apt accepts the signed-by= source (ref #89)

E2E proof (fresh bookworm chroot):

secubox apt setup     # OK: SecuBox repository configured successfully!
apt-get update        # Hit:1 https://apt.secubox.in bookworm InRelease
apt-cache search secubox # 15 packages

secubox clone --minimal -y reaches apt install; dpkg postinst then fails inside minbase (no systemd) — not a CLI bug, expected limitation of --variant=minbase. Validates on real targets with systemd (MOCHAbin, VM).

Follow-up (separate work):

  • Add an E2E smoke job to CI that exercises secubox apt setup against a real or recorded apt.secubox.in (would have caught the URL bug). Out of scope here.

Session 159 — WebUI Obfuscation (#44)

Goal: Lock the SecuBox WebUI to https://admin.<HOSTNAME>.<DOMAIN_SUFFIX>/ only, enforced at HAProxy and nginx layers, driven by /etc/default/secubox.

Spec: docs/superpowers/specs/2026-05-12-webui-obfuscation-design.md Plan: docs/superpowers/plans/2026-05-12-webui-obfuscation.md

Changes:

  • New package secubox-defaults ships /etc/default/secubox (SECUBOX_HOSTNAME + SECUBOX_DOMAIN_SUFFIX) as the single source of truth. Postinst autodetects from hostname -s if unset, then fires dpkg-trigger secubox-defaults-changed.
  • secubox-haproxy API extended with three endpoints in api/main.py:
    • GET /webui/admin-domain — canonical identity + escaped regex (info, no auth)
    • GET /webui/nginx-config — JWT-protected, rendered nginx vhost as text/plain
    • POST /webui/refresh — JWT-protected LRU cache invalidation (204)
  • Helper module api/webui_identity.py parses /etc/default/secubox with shlex, caches via lru_cache(maxsize=1), exposes get_identity()/invalidate_cache(). Defensive OSError handling so file-permission failures surface as ValueError (caught by endpoints → 503).
  • sbin/secubox-render-nginx-webui script: snapshot → fetch from API → atomic stage → nginx -t → reload, with rollback on validation failure. Includes early binary-availability check.
  • sbin/haproxyctl (bash generator) injects acl is_webui_admin hdr(host) -m reg ^admin\.<HOST>\.<SUFFIX>$ + use_backend webui_direct if is_webui_admin at the top of http-in and https-in frontends. Emits the matching backend webui_direct (→ 127.0.0.1:9080) only when the strict ACL is in use. Sources /etc/default/secubox once per frontend loop (hoisted from per-iteration).
  • Python generate_config() in api/main.py mirrors the bash generator (symmetric ACL + backend emission + per-vhost skip).
  • debian/postinst declares interest-noawait secubox-defaults-changed and on trigger runs (best-effort): POST /webui/refresh, secubox-render-nginx-webui, secubox-haproxy-regen-safe.
  • secubox-haproxy-regen-safe script now packaged into secubox-haproxy (installed to /usr/local/bin/).
  • secubox-haproxy debian/control declares Depends: ..., secubox-defaults.
  • Integration test tests/integration/test_44_webui_obfuscation.sh covers positive probe (admin.gk2 = WebUI), negative probes (gk2.secubox.in and admin.fake.secubox.in NOT WebUI), LAN-direct preserved, and regression spot-checks on cpf, arm, lldh, pub, werdl, 3d.

Tests: 12 pytest passing (6 in tests/test_webui_identity.py, 6 in tests/test_webui_endpoints.py). Includes a permission-denied test that confirms OSError is caught and surfaces as ValueError.

Implementation workflow: Brainstorming → spec → plan → subagent-driven execution (15 tasks, each TDD with implementer + spec reviewer + code quality reviewer). 6 review cycles triggered fix subagents (OSError handling in _parse_defaults, missing conftest.py commit, debian/install conflict, missing binary check, missing webui_direct backend def, missing regen-safe packaging).

Closes: #44 (pending PR merge)


Session 158 — SSL Health Banner Fixes & HAProxy Recovery

Goal: Fix SSL display not working in banner + recover broken HAProxy config.

Changes:

  • packages/secubox-metrics/api/main.py:
    • Added domain query parameter for SSL cert check (fixes cross-origin domain detection)
    • Added /data/haproxy/certs/ to cert search paths
    • Added PermissionError handling for restricted cert files
  • packages/secubox-hub/www/shared/health-banner.js (v1.2.1):
    • Pass window.location.hostname as query param to API
    • Added version footer display for debugging
  • HAProxy config: Fixed broken mitmproxy_inspector_DISABLED references
  • Permissions: Granted secubox group read access to /etc/letsencrypt/live/

Commits: 443e375f (merge), 90e8b8fc (fixes)

Reference: CM-SSL-BANNER-FIX-2026-05-12


Session 157 — SSL Certificate Health in Health Banner

Goal: Display SSL certificate expiration status in the Health Banner sidebar.

Changes:

  • packages/secubox-metrics/api/main.py: Added get_ssl_status() function + endpoint enrichment
  • packages/secubox-hub/www/shared/health-banner.js: Added SSL display (v1.2.0)
  • tests/test_ssl_status.py: 5 unit tests

Features:

  • Reads cert from /etc/letsencrypt/live/{domain}/cert.pem (with fallbacks)
  • Displays after score section: 🔒 45j
  • Color-coded thresholds:
    • 🔒 >7j (green)
    • 🔐 3-7j (yellow)
    • 🔓 <3j (red)
    • 🔓 EXPIRÉ (red blink)

Commits: 6bd4fb3e, 9c07eda6, 593b991f

Reference: CM-SSL-BANNER-2026-05-12


Session 156 — HAProxy Routing Catastrophe & Generator Fix

Trigger: User flagged https://cpf.gk2.secubox.in/ returning "Wrong Domain". Investigation revealed a much wider regression that surfaced minutes after Session 154 — the entire HAProxy https-in routing for metablog/streamlit sites had silently broken.

Symptom (when investigation started, ~10:05):

  • arm.gk2, cpf.gk2, lldh.ganimed.fr, etc. all returning HTTP 200 with a 6202 b <title>Wrong Domain - SecuBox</title> page (nginx:9080 default_server fallback).
  • /etc/haproxy/haproxy.cfg last modified at 10:03:48 — minutes after my Session 154 nginx + mitmproxy fixes.
  • HAProxy reloaded at 10:03:51 with the broken config.
  • 4 metablog_* backends DOWN, all metablog vhosts returning 503 or Wrong Domain.

Cause chain (multi-layered):

  1. Stale generator on board. /usr/sbin/haproxyctl (the bash script that actually writes haproxy.cfg from /etc/secubox/haproxy.toml) was an older version that emitted use_backend waf_inspector if host_X while waf_enabled=1. The repo's current packages/secubox-haproxy/sbin/haproxyctl already emits use_backend mitmproxy_inspector — but the board had a 33115 b old copy still using waf_inspector.
  2. waf_inspector backend was a dead reference. The script also generated backend waf_inspector { server srv0 127.0.0.1:8890 check } but port 8890 was not listening — so even if the regen worked, those vhosts would have been DOWN.
  3. waf_enabled=0 fallback was equally broken. When waf_enabled evaluated to 0, the script fell back to each vhost's TOML backend = "nginx_vhosts" (server 127.0.0.1:9080 check) — but nginx:9080 has no server_name for individual gk2 sites (only for admin.gk2.secubox.in), so every other host hit the default_server "Wrong Domain" page.
  4. TOML coverage is incomplete. /etc/secubox/haproxy.toml declares only 93 vhosts, while /srv/mitmproxy/haproxy-routes.json has 245 active routes. The 150 missing domains had no HAProxy ACL → default_backend fallback (deny 503).
  5. Race with the Python service. secubox-haproxy.service (FastAPI on /usr/lib/secubox/haproxy/api/main.py) was polling/validating the config and logging "Failed to load HAProxy config: Invalid value (at line 854, column 7)" every ~10 s for >10 min before haproxyctl finally succeeded a regen that produced the broken-but-syntactically-valid output.
  6. Container routes had drifted too. lxc-attach -n mitmproxy -- jq .[cpf.gk2.secubox.in] routes.json showed [10.100.0.1, 9080] while the host file had [10.100.0.50, 8523]. A previous unsuccessful regen had pushed a corrupted JSON into the container.

Restore + harden plan (user-approved option: patch direct + freeze regen):

  1. systemctl stop secubox-haproxy.service — freeze any further regen attempts.
  2. Backup current haproxy.cfg.
  3. Read /srv/mitmproxy/haproxy-routes.json → for each of 245 domains, replace use_backend nginx_vhosts if host_X with use_backend mitmproxy_inspector if host_X in haproxy.cfg (180 lines patched: 90 unique × 2 frontends).
  4. Replace default_backend fallback with default_backend mitmproxy_inspector in both http-in and https-in frontends — so the 150 routes that exist in mitmproxy JSON but not in haproxy.toml are still dispatched correctly (mitmproxy reads the Host header against its routes table).
  5. haproxy -c -f haproxy.cfg → validate; systemctl reload haproxy.
  6. Push host routes JSON → mitmproxy LXC; systemctl restart mitmproxy inside container.
  7. Verify with live HTTPS probes.

Verification (live, fresh, cache-busted):

Domain HTTP Title
cpf.gk2.secubox.in 200 Streamlit (cineposter_fixed @ port 8523)
arm.gk2.secubox.in 200 SITREP ARM/ARMADA — CLASSIFIED // GANDALF-7
lldh.ganimed.fr 200 La Livrée d'Hermès — Anibal Edelberto Amiot
admin.gk2.secubox.in 200 SecuBox Control Center
pub.gk2.secubox.in 200 GK² · NET — Opérateur Internet
werdl.gk2.secubox.in 200 Retrouver son téléphone — Pour toute la famille
3d.gk2.secubox.in 200 SecuBox Dice 3D
42.gk2.secubox.in 200 CyberMind QWIZZ — Détecteur d'Injonction Paradoxale
zkp.gk2.secubox.in 200 🔐 OPORD CYBER-ZKP // SECUBOX CLASSIFIED

Persistent fixes committed to repo:

  • packages/secubox-haproxy/api/main.py — Python generator: use_backend waf_inspectoruse_backend mitmproxy_inspector (2 occurrences, lines 1147 + 1170). Survives next package rebuild.
  • packages/secubox-haproxy/sbin/haproxyctl — Bash generator: default_backend fallbackdefault_backend mitmproxy_inspector (2 occurrences). Repo version already used mitmproxy_inspector for the use_backend lines; the board copy was stale. Future dpkg -i secubox-haproxy_*.deb will replace the board's stale /usr/sbin/haproxyctl.
  • scripts/secubox-haproxy-regen-safe — new wrapper: snapshot → regen → validate → atomic swap → reload, with rollback on validation failure. Prevents future broken-config-deployed-anyway incidents.

Still on board (not in repo, infra-side only):

  • /usr/sbin/haproxyctl patched in place on the board to mirror repo state.
  • /usr/lib/secubox/haproxy/api/main.py patched on board.
  • secubox-haproxy.service left stopped until user confirms safe to re-enable.

Open questions:

  • /etc/secubox/haproxy.toml only declares 93 vhosts — should the 150 metablog/streamlit domains be added so HAProxy has explicit ACLs and stats? Currently they work via default_backend mitmproxy_inspector (catch-all), which is functional but loses per-vhost stats granularity.
  • Re-enabling secubox-haproxy.service is safe now (generators patched), but verify no other code path writes haproxy.cfg with the broken pattern (e.g., on-demand /generate API endpoint).

Session 155 — Multi-Agent Worktree Workflow

Goal: Enable parallel multi-agent work via one-branch-per-issue isolated worktrees.

Delivered:

  • scripts/agent-worktree.sh with sub-commands start, list, sync, finish, clean
  • scripts/lib/agent-worktree-lib.sh (slug + label→prefix helpers)
  • Test suite scripts/tests/test-agent-worktree.sh (26 cases, no bats dep)
  • gh CLI mock at scripts/tests/fixtures/gh-mock.sh
  • New section in CLAUDE.md: ## 🌿 Multi-Agent Worktree Workflow — Obligatoire
  • scripts/README.md updated with usage

Issue: #83. Spec: docs/superpowers/specs/2026-05-12-multi-agent-worktree-workflow-design.md. Plan: docs/superpowers/plans/2026-05-12-multi-agent-worktree-workflow.md.


Session 154 — Metablogizer Vhosts Audit & Regeneration

Goal: User flagged https://lldh.ganimed.fr/ returning 404. Apply the same diagnostic + fix workflow to all metablogizer vhosts, find every site with broken routing / missing content / port mismatch.

Trigger case — lldh.ganimed.fr:

  • nginx had server_name lldh.gk2.secubox.in only (no .ganimed.fr alias) → 404 from default_server.
  • mitmproxy route pointed to port 8000 instead of 8900 (where metablog nginx listens).
  • /srv/metablogizer/sites/lldh/ contained only La_Livree_dHermes_Galerie-1.zip (31 MB) + .git — no index.html to serve.

Trigger fix (3 steps):

  1. Extracted zip via python3 -m zipfile (84 files: index.html + planches/*.jpg).
  2. Added lldh.ganimed.fr to existing server_name lldh.gk2.secubox.in block in /etc/nginx/sites-enabled/metablogizer, nginx -t && systemctl reload nginx.
  3. Patched /srv/mitmproxy/haproxy-routes.json: lldh.ganimed.fr[192.168.1.200, 8900], pushed to LXC container, systemctl restart mitmproxy (SIGHUP alone wasn't enough — mitmproxy serving cached/wrong content until full restart).

Systematic audit of all metablog vhosts (Python script in /tmp on board):

Scanned /etc/nginx/sites-enabled/metablogizer for every server_name → root pair, cross-checked each domain against /srv/mitmproxy/haproxy-routes.json and the on-disk index.html.

Metric Count
Sites scanned 159
Server_names total 162
OK after fixes 162 (100%)
wrong_port (route ≠ 8900) 0
missing_route (in nginx, absent from routes) 1 → pub.gk2.secubox.in
extract_zip (zip not extracted) 0 (after lldh)
no_index_no_zip real cases 1 → werdl

Additional fixes applied:

  • pub.gk2.secubox.in → added to mitmproxy routes [10.100.0.1, 8900], mitmproxy restarted. Now HTTP 200 ("GK² · NET — Opérateur Internet & Services Numériques").
  • werdl/index.html → symlink to famille_index.html (3 HTML files existed: famille_index, famille_bebe-enfant, famille_papy-mamy; preserved originals, no destructive rename). Now HTTP 200 ("Retrouver son téléphone — Pour toute la famille").

False positive identified:

  • Initial audit flagged public as a "site without index", but my regex over-captured: it was matching the trailing public in Laravel-style paths /srv/metablogizer/sites/{money,live,evolution}/public/. Those parent sites (money, live, evolution) serve fine via their normal server_name blocks. No action needed.

Backups on board:

  • /srv/mitmproxy/haproxy-routes.json.bak.<epoch> (pre-patch)
  • /etc/nginx/sites-enabled/metablogizer.bak.<epoch> (pre-server_name-add)
  • Symlink for werdl is non-destructive (famille_index.html untouched).

Verification (live, fresh HTTPS, no cache):

  • lldh.ganimed.fr → 200, 43959 b, "La Livrée d'Hermès"
  • lldh.gk2.secubox.in → 200, 43959 b, same content
  • pub.gk2.secubox.in → 200, 47584 b, "GK² · NET"
  • werdl.gk2.secubox.in → 200, 6017 b, "Retrouver son téléphone"
  • Spot-check admin.gk2, arm.gk2, zkp.gk2, 3d.gk2 from Session 153 still 200 (no regression).

Note: all fixes were applied to live board state (routes JSON, nginx vhost config, site content). The metablogizer vhost config (/etc/nginx/sites-enabled/metablogizer) is auto-generated per site by the metablogizer service and is not tracked in the repo, so no repo file changed from this session beyond this HISTORY entry.


Session 153 — Mitmproxy Route Sync Stability Fix

Goal: All *.gk2.secubox.in metablogizer sites (arm, zkp, 3d, …) returned "Wrong Domain" page on HTTPS. Investigate, restore service, and harden the auto-sync infrastructure.

Symptom chain:

  1. User report: https://arm.gk2.secubox.in/ → "Wrong Domain - SecuBox" landing page.
  2. ~160 metablogizer sites affected (all routed via mitmproxy WAF).
  3. Two systemd timers (sync-mitmproxy-routes.timer + secubox-route-sync.timer) firing at the same second on the same routes file.
  4. sync-mitmproxy-routes.service had been failing with exit code 30/4 for >30 minutes (chronic).

Root cause (deepest):

log() function in /usr/local/bin/sync-mitmproxy-routes.sh wrote to stdout. fix_dead_container_routes() calls log "Fixing dead route: …" and is itself captured via routes_json=$(fix_dead_container_routes "$routes_json") — every log line was concatenated into the JSON variable, producing [date] Fixing dead route: …{"255.gk2...". jq then complained Invalid numeric literal at line 1, column 12, set -e killed the script, and the corrupted JSON got pushed to the mitmproxy container (lxc-attach … tee /srv/mitmproxy/haproxy-routes.json). mitmproxy restart-looped on Failed to load routes: Expecting ',' delimiter, HAProxy backend mitmproxy_inspector went DOWN, every public domain returned HTTP 503.

Additional contributing bugs:

  • fix_dead_container_routes returned $fixed (a count) instead of 0 — non-zero return tripped set -e in the caller's command substitution.
  • sync-all-routes.sh step 2 wrote metablogizer routes to port 9080 (nginx default_server → "Wrong Domain") instead of 8900 (where nginx actually listens with server_name per metablog site).
  • jq read calls had no error tolerance — any malformed JSON fed in via $routes_json aborted the whole script via set -euo pipefail.
  • Two systemd services bound to the same script (sync-mitmproxy-routes.service + secubox-route-sync.service) racing on the same file.

Fixes applied:

# Fix Cible Mechanism
1 Routes patchées 9080→8900 (165 sites metablog) /srv/mitmproxy/haproxy-routes.json (host + container) Python script (extract server_name from nginx, rewrite port)
2 return $fixedreturn 0 sync-mitmproxy-routes.sh:fix_dead_container_routes sed
3 Port metablog 9080 → 8900 in step 2 sync-all-routes.sh:62 sed
4 log() writes to stderr (root-cause fix) sync-mitmproxy-routes.sh:log adds >&2 so $() capture is clean
5 Defensive 2>/dev/null || true on jq read sync-mitmproxy-routes.sh (2 occurrences) tolerate corrupted input
6 Fallback preserving old routes_json on jq write failure sync-mitmproxy-routes.sh (3 occurrences) new_rj=$(... || true); [[ -n "$new_rj" ]] && routes_json="$new_rj"
7 flock -n guard prevents concurrent runs sync-mitmproxy-routes.sh (head) /run/sync-mitmproxy-routes.lock
8 Disable duplicate timer systemctl disable --now secubox-route-sync.timer systemd

Verification (post-fix):

  • sync-mitmproxy-routes.service via systemd → exit 0/SUCCESS, "Sync complete".
  • Container routes JSON valid: 244 keys, all metablogizer domains → [10.100.0.1, 8900].
  • mitmproxy: active, listening on 0.0.0.0:8080.
  • HAProxy backend mitmproxy_inspector srv0 10.100.0.60:8080 op_state=UP.
  • Live tests: admin.gk2, arm.gk2, zkp.gk2, 3d.gk2 all HTTP 200 with correct titles.

Files modified (versioned in repo):

  • scripts/sync-mitmproxy-routes.sh (synced from board /usr/local/bin/)
  • scripts/sync-all-routes.sh (new in repo, synced from board)

Backups created on board (timestamped, kept for rollback):

  • /srv/mitmproxy/haproxy-routes.json.bak.<epoch> (pre-patch JSON)
  • /usr/local/bin/sync-mitmproxy-routes.sh.bak.<epoch> and .bak.<epoch>-preflock
  • /usr/local/bin/sync-all-routes.sh.bak.<epoch>

Topology note discovered:

  • Canonical Hub vhosts (nginx sites-available/secubox-local): admin.gk2.secubox.in, gk2.secubox.in, secubox.maegia.tv, c3box.maegia.tv + LAN aliases.
  • ~165 metablogizer sites listed in /etc/nginx/sites-enabled/metablogizer, each listen 0.0.0.0:8900 with per-site server_name, root /srv/metablogizer/sites/<name>/.
  • Public flow: HAProxy https-in (443) → ACL host match → backend mitmproxy_inspector (LXC 10.100.0.60:8080) → mitmproxy looks up host in haproxy-routes.json → upstream [10.100.0.1, 8900] → nginx vhost matches server_name → serves static site.

Open follow-up: the sync-streamlit-routes.timer last fired 2026-05-10 (1d 15h ago) and didn't fire since — needs separate investigation. Not blocking metablog/Hub stability.


Session 152 — APT Public Repo Staging Pipeline (Issue #80)

Goal: Stage a complete signed APT repo at output/repo/ for bookworm × {arm64, amd64}, validated end-to-end. User pushes to apt.secubox.in out-of-band.

Spec & plan:

  • Design: docs/superpowers/specs/2026-05-12-apt-public-repo-staging-design.md
  • Plan: docs/superpowers/plans/2026-05-12-apt-public-repo-staging.md

Delivered (10 tasks, 9 commits — merged via PR #82, plus 0f1907df chroot fix):

Component Commit Purpose
scripts/build-packages.sh --filter + --dry-run ce82e13d Tier-driven build filtering via JSON manifest
scripts/lib/tier-manifest.sh + hardening 6f59de25, 52463db1 Resolve base/tier-lite/tier-standard/tier-pro → JSON package list
scripts/stage-gpg-bootstrap.sh 3b99bcf4 Persistent GPG key at ~/.gnupg/secubox/, writes FINGERPRINT.txt
scripts/stage-apt-repo.sh 5f7b8474 Main orchestrator (GPG → reprepro init → tier loop → check gate)
scripts/render-deploy-artifacts.sh d6fe14d5 nginx vhost + DEPLOY.md + install.sh + CMSD-1.0 license copies
scripts/validate-staged-repo.sh + chroot fix bb58789b, 0f1907df reprepro check + gpg verify + license cmp + chroot apt-update smoke
.gitignore for staging artifacts 197eba63 Ignore output/repo/{db,pool,dists,conf,gpg} and build logs

Tooling used:

  • secubox gen --tier <tier> --board mochabin --out <dir> (existing Go CLI; emits manifest.yaml)
  • reprepro with persistent ~/.gnupg/secubox/ keyring (SignWith fingerprint)
  • dpkg-buildpackage + crossbuild-essential-arm64 (already installed)
  • python3-yaml (for parsing secubox gen output)

End-to-end validation (base + tier-lite × arm64+amd64):

  • 9 packages published, all Architecture: all
  • reprepro check clean
  • gpg --verify InRelease → Good signature, fingerprint 31848880ED89C1722677D75A25C9E32645166DB9
  • License files byte-match project root
  • chroot apt-get update against file://output/repo/ succeeds (sees SecuBox repo)

Important finding (arch:all dominance):

Architecture field Count
all 130
any 2 (secubox-daemon, zkp-hamiltonian)

130/132 SecuBox packages are Architecture: all — the cross-arch (arm64 vs amd64) split is mostly cosmetic. The two Architecture: any packages aren't in build-packages.sh's PACKAGES= list, so the arm64 pool count is currently 0. Adding them is separate work.

GPG signing key:

  • UID: SecuBox Package Signing Key (apt.secubox.in) <packages@secubox.in>
  • Fingerprint: 31848880ED89C1722677D75A25C9E32645166DB9
  • Home: ~/.gnupg/secubox/ (persistent across rebuilds)
  • Public key: output/repo/secubox-keyring.gpg (ASCII-armored) + .bin

Open item (not blocking):

  • TLS cert for apt.secubox.in shows ERR_TLS_CERT_ALTNAME_INVALID — must be re-issued by certbot for the exact SAN. Recipe is in output/repo/DEPLOY.md.

Next steps (user):

  1. Optional: full pipeline run with bash scripts/stage-apt-repo.sh (no flags = all four tiers, 30-90 min).
  2. rsync to apt.secubox.in per output/repo/DEPLOY.md (excludes db/, gpg/, conf/).
  3. certbot --nginx for cert; verify SAN includes apt.secubox.in.
  4. Smoke-test from clean client: curl -fsSL https://apt.secubox.in/install.sh | sudo bash && sudo apt-get update.
  5. Close issue #80 on success.

Session 151 — Fix Sidebar Mobile Mode False-Positive on Touch Desktops

Goal: Sidebar of secubox-hub forced mobile mode (hamburger + hidden sidebar) on Firefox PC because the detection logic used isTouchDevice() || isNarrowViewport() — any touch signal (touchscreen laptop, Firefox pointer: coarse, maxTouchPoints > 0) triggered mobile UX at desktop widths.

Root cause: packages/secubox-hub/www/shared/sidebar.js:2113-2116OR was too permissive. User console showed Mobile mode: ON (touch: true, narrow: false) on a 1080p+ Firefox.

Fix: Changed to strict AND — both touch AND narrow viewport required.

function shouldUseMobileMode() {
    // Mobile = touch device AND narrow viewport.
    // OR was too permissive: PCs with touchscreens (or Firefox advertising
    // pointer:coarse) triggered mobile mode at desktop widths.
    return isTouchDevice() && isNarrowViewport();
}

Files modified:

  • packages/secubox-hub/www/shared/sidebar.js:2113-2118

Deploy:

  • bash scripts/deploy.sh secubox-hub root@192.168.1.200 (rsync www//usr/share/secubox/www/, no service restart needed for static JS)
  • Verified on canonical Hub vhost https://admin.gk2.secubox.in/shared/sidebar.js (line 2117 contains the new && logic)

Topology note (discovered during validation):

  • Canonical Hub vhosts (nginx sites-available/secubox-local): admin.gk2.secubox.in, gk2.secubox.in, secubox.maegia.tv, c3box.maegia.tv + LAN secubox.local / 192.168.1.200 / 192.168.255.1
  • HAProxy webui-lan frontend → default_backend webui_direct (127.0.0.1:9080) — bypasses mitmproxy, ideal for LAN/test
  • All public *.gk2.secubox.in vhosts route via mitmproxy → same nginx:9080 → same /usr/share/secubox/www/ (single source of truth for the Hub UI)

Build artefact note: packages/secubox-hub/debian/secubox-hub/usr/share/secubox/www/shared/sidebar.js was not modified — it will be regenerated at next dpkg-buildpackage.


Session 150 — OPAD Doctrine Documents v2.4.0

Goal: Create the 5 foundational OPAD (Off-Path Active Defense) doctrinal documents for SecuBox-Deb migration to passive observation + packet injection architecture.

Reference: CM-WALL-OPAD-2026-05

Architecture Overview:

  • OPAD Principle: SecuBox observes traffic passively (port mirroring) and injects packets to neutralize threats, never sits in the data path
  • 4 Injection Primitives: DNS-R (99%), DHCP-R (95%), RST-I (90%), ARP-R (98%)
  • 8 Invariants (INV-01 to INV-08): Fail-silent, no forwarding, zero WAN surface, etc.
  • 3-Prong Profile: Observation / Injection / Policy configuration structure

Files Created:

File Lines Description
doctrine/opad/OPAD.md 671 Core doctrine, principles, invariants, injection specs
doctrine/opad/CSPN.matrix.md 569 ANSSI threat × capability matrix (36 threats, 72% coverage)
doctrine/opad/OPAD-OPERATIONS.md 948 Operational guide, troubleshooting, 4R rollback
schemas/opad-profile.schema.json 365 JSON Schema draft-07 for profile validation
common/secubox_core/opad/models.py 400 Pydantic v2 models (OPADProfile, configs)
common/secubox_core/opad/__init__.py 85 Package exports
tests/test_opad_schema.py 374 18 tests (JSON Schema + Pydantic equivalence)
Total 3412

Technical Notes:

  • Pydantic v2 syntax: @field_validator, ConfigDict, model_json_schema()
  • JSON Schema draft-07 with $defs for reusable definitions
  • MAC address validation: ^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$
  • Success rate constraints: 0.90 ≤ rate ≤ 1.0 for injection primitives

Tests: 18 passed (0.13s)

  • JSON Schema Draft7 validation
  • Pydantic model equivalence
  • MAC address format validation
  • Success rate bounds checking
  • Policy rule priority range (0-9999)

Commits: Cherry-picked to feature/eye-remote-auto-mode


2026-05-11

Session 147 — Fix Eye Agent Import Errors (#78)

Goal: Fix import errors in eye-agent main.py that prevented the service from starting.

Problem Analysis:

  • main.py imported nonexistent classes: DashboardRenderer, LocalRenderer, FlashRenderer, GatewayRenderer, RenderContext
  • Missing modules: agent.system, agent.secubox, agent.web
  • Import chain failures due to missing aiohttp dependency

Solution:

  1. Created stub modules with working implementations:

    • system.py: WifiManager, BluetoothManager, DisplayController
    • secubox.py: DeviceManager, FleetAggregator
    • web.py: WebServer (FastAPI-based)
  2. Created display/renderers.py with mode-specific renderers:

    • DashboardRenderer: 3D cube + metric rings (PIL-based)
    • LocalRenderer: Disconnected mode display
    • FlashRenderer: Alert/splash messages
    • GatewayRenderer: Fleet overview for multi-device
    • RenderContext: Dataclass for render state
  3. Refactored imports in main.py:

    • Replaced single try/except with _try_import() helper
    • Each module imported individually with fallbacks
    • Missing modules set to None instead of crashing
    • Warnings logged for missing optional components
  4. Updated display/__init__.py:

    • Added graceful fallbacks for all exports
    • Missing classes set to None
    • Logged warnings instead of crashing

Branch: fix/78-eye-agent-imports Commit: e71209f5

Files Created/Modified:

File Status
agent/main.py Modified
agent/display/__init__.py Modified
agent/display/renderers.py New
agent/system.py New
agent/secubox.py New
agent/web.py New

Session 146 — Eye Remote v2.2.1 Build & Validation

Goal: Update build script with fallback display fix, build & test new image.

Changes:

  1. Build script updated (build-eye-remote-image.sh v2.2.1)

    • Added secubox-fallback-display.service installation
    • Enabled fallback-display instead of broken eye-agent
    • Added PIL dependencies: libopenjp2-7, libtiff6
    • All agent subdirectories: display, secubox, system, web, api, recovery, sync
  2. Image built and tested

    • /tmp/secubox-eye-remote-2.2.1.img (5.3GB uncompressed)
    • Flashed to SD card, tested on MOCHAbin
    • Dashboard working: 3D cube + rainbow rings + real metrics
  3. GitHub Issues created

    • #78: Fix eye-agent import errors (bug)
    • #79: Investigate Buildroot/Busybox minimal image (enhancement)

Artifacts:

  • Commit: b2f046c1
  • Image: secubox-eye-remote-2.2.1.img.xz

Session 145 — Eye Remote Dashboard Fix

Problem: Eye Remote Pi Zero W showing wrong dashboard (plain fb_dashboard instead of nice fallback_manager with 3D cube and rainbow rings).

Root Causes:

  1. Agent code incomplete - DashboardRenderer class doesn't exist
  2. Build script missing agent/api/ directory copy
  3. Relative imports failing (from ..api.setup import)
  4. PIL dependencies missing (libopenjp2-7)

Fixes:

  1. Deployed fallback_manager.py as main dashboard

    • 3D rotating cube animation
    • Rainbow concentric rings for modules
    • Connection state: OFFLINE/CONNECTING/ONLINE/COMMUNICATING
    • Real-time metrics from MOCHAbin API
  2. Created secubox-fallback-display.service

    • Replaced broken secubox-eye-agent.service
    • Proper PYTHONPATH and WorkingDirectory
  3. NAT routing through MOCHAbin

    • IP forwarding enabled
    • iptables MASQUERADE for 10.55.0.0/30
    • Pi can reach internet via USB OTG
  4. Missing directories copied

    • agent/api/ (metrics_fetcher, setup, gadget)
    • agent/recovery/
    • agent/sync/

Working Configuration:

Service: secubox-fallback-display.service
Display: /usr/lib/secubox-eye/agent/display/fallback/fallback_manager.py
API: http://10.55.0.1:8000/api/v1/system/metrics

2026-05-09

Session 141 — WAF Optimization, Route Fixes, Export Package, UI Enhancements

Problem: Mitmproxy CPU at 90%+ constant, many sites returning 502/503.

Root Causes:

  1. WAF regex checks running on every request (including static assets)
  2. Dead container routes (10.100.0.10-50) causing connection timeouts
  3. MultiDictView.to_dict() AttributeError on every request

Fixes:

  1. WAF Optimizations (secubox_waf.py)

    • Skip WAF checks for static assets (.js, .css, .png, etc.)
    • Skip WAF checks for /health, /status, /system_health endpoints
    • Skip WAF checks for trusted hosts (git, admin, internal API)
    • Fixed to_dict()dict() for MultiDictView
    • Result: CPU dropped from 90%+ to 0% idle, 25-90% under load
  2. Route Sync Script (sync-mitmproxy-routes.sh)

    • Added dead container detection and auto-fix
    • Routes to dead IPs (10.100.0.10-50) → webui (9080)
    • Fixed bash arithmetic for set -e compatibility
  3. Metablogizer Export Package Enhanced

    • Full export ZIP with: content/, config/, certs/, README.md
    • nginx.conf, haproxy.cfg generated configs
    • Complete republishing instructions
  4. Users Module

    • REVOKE ALL sessions panic button
    • Emergency session revocation endpoint
  5. WAF UI Enhancements

    • Eyemote Visualization - Concentric donut rings for attack origins by continent
      • 5 Olympic-colored rings (Americas, Europe, Asia, Oceania, Africa)
      • Pulsing center pupil with total attack count
      • Legend with circular flag badges per country
    • Quick Action Buttons - Circular buttons in status bar:
      • 🔄 Refresh (with spinner animation)
      • 🛡️ WAF Toggle (active/paused states)
      • 🗑️ Clear All Bans (with confirmation)
      • 📤 Export Logs (JSON download)
      • ⚙️ Settings link
  6. SOC Dashboard Migration

    • Replaced WarGames geomap with Triple Eyemote Sensors
    • 3 mini eyemotes in grid: Attack Origin, Visitors, Target Vhosts
    • Double-cache pattern for async API response handling
    • Same Olympic-colored visualization as WAF page

Files Updated:

  • scripts/sync-mitmproxy-routes.sh - Dead container auto-fix
  • packages/secubox-waf/mitmproxy/secubox_waf.py - Optimizations
  • packages/secubox-mitmproxy/addons/secubox_waf.py - Optimizations
  • packages/secubox-metablogizer/api/main.py - Enhanced export
  • packages/secubox-users/api/main.py - Revoke all endpoint
  • packages/secubox-users/www/users/index.html - Panic button
  • packages/secubox-waf/www/waf/index.html - Eyemote rings, quick action buttons
  • packages/secubox-hub/www/soc/index.html - Triple Eyemotes with double-cache

Systemd:

  • sync-mitmproxy-routes.timer - Every 5 minutes

Session 140 — Domain Filtering, Error Pages, Mitmproxy Sync

New Features:

  1. Domain Filtering - Only admin.gk2.secubox.in allowed for admin access

    • Default server catches all other domains → wrong-domain.html
    • Styled landing page with redirect to correct admin URL
  2. Error Pages

    • wrong-domain.html - Funky styled page for unauthorized domains
    • unknown-module.html - Styled 404 for unknown modules on admin
    • /login URL without .html extension for cleaner aesthetics
  3. Metablogizer Enhancements (v1.2.0)

    • Auto-publish all sites on service startup
    • "Republish All" button in UI
    • empty-site.html - Sketch-style error for sites with no content
    • Default domain suffix changed from .local to .gk2.secubox.in
    • Mitmproxy sync - Routes updated when publishing sites
  4. Users Sessions

    • Active sessions display in Users module
    • Session revocation endpoint

Infrastructure:

  • Cleaned up nginx backup files (webui.conf.bak*)
  • Added sudoers for secubox user: nginx test/reload, mitmproxy reload
  • Fixed mitmproxy routes permissions for secubox user

Files:

  • packages/secubox-hub/www/shared/wrong-domain.html (NEW)
  • packages/secubox-hub/www/shared/unknown-module.html (NEW)
  • packages/secubox-metablogizer/api/main.py - Mitmproxy sync, v1.2.0
  • packages/secubox-metablogizer/www/metablogizer/empty-site.html (NEW)
  • packages/secubox-metablogizer/www/metablogizer/index.html - Republish button
  • packages/secubox-users/api/main.py - Sessions endpoint
  • packages/secubox-users/www/users/index.html - Sessions display

Session 139 — Nginx Health Routes Fix

Problem: Sidebar health checks returning 404 for most modules.

Root Cause:

  1. Only 6 modules had nginx routes configured in /etc/nginx/secubox-routes.d/
  2. The include /etc/nginx/secubox-routes.d/*.conf; was missing from webui.conf

Solution:

  • Generated nginx routes for all 82 modules with API sockets
  • Removed 19 duplicates already hardcoded in webui.conf
  • Added missing include statement to webui.conf
  • All /api/v1/<module>/health endpoints now functional

Impact:

  • Sidebar status indicators now work for all 100+ modules
  • Health-batch endpoint functional for efficient status polling

Session 134 — CrowdSec Bans Display Fix, Service Stability (v2.7.2)

CrowdSec Dashboard:

  • Fixed bans limit showing max 100 (actual: 143 bans on live server)
  • API now returns total count separate from paginated decisions array
  • Increased default limit from 100 to 1000, max to 10000
  • Frontend fetches 500 decisions, displays up to 200 with "X more" indicator
  • GeoIP enrichment limited to first 100 for performance

Service Debugging:

  • Investigated vhost.sock intermittent creation issue
  • All 8 core API services verified healthy (hub, auth, crowdsec, system, vhost, certs, publish, waf)
  • MOCHAbin CPU stabilized at ~43% (down from 135%)

Live Server (192.168.1.200):

  • 143 active bans in CrowdSec
  • 6 collections installed (base, linux, nginx, http-cve, whitelist, vpatch)
  • Custom SecuBox auth parser/scenario deployed

Files Updated:

  • packages/secubox-crowdsec/api/routers/decisions.py — Total count, higher limits
  • packages/secubox-crowdsec/www/crowdsec/index.html — Display total, show more indicator
  • .claude/WIP.md — Session 134 tracking

Session 133 — WAF Category Toggles, API Error Handling (v2.7.1)

WAF Dashboard:

  • Added Categories tab with toggle on/off buttons for each WAF filter
  • Fixed API error handling (return null instead of empty object)
  • Added loadCategories() function to populate categories list
  • Handle null responses gracefully in all data loaders (loadAlerts, loadBans, loadStats)
  • Category toggles call /category/{id}/toggle API endpoint

SOC Dashboard:

  • Fixed API calls to handle null responses properly
  • Show meaningful states (OFFLINE, UNKNOWN) instead of stuck "LOADING"
  • Use requireAuth=false for public endpoints (firewall_summary, health)
  • Initialize empty states for all metrics when API fails

nftables Verification:

  • IPv4: 3.1M packets processed, 2.7K blocked (CAPI + CrowdSec + manual bans)
  • IPv6: 22K packets processed
  • Cache files updated for SOC dashboard consumption

WAF Threats Log:

  • 11,463 threats logged in /var/log/secubox/waf-threats.log
  • Latest entries: scanner probes, robots.txt enumeration, favicon fingerprinting

Tag: v2.7.1

Files Updated:

  • packages/secubox-waf/www/waf/index.html — Category toggles, null handling
  • packages/secubox-hub/www/soc/index.html — API error states

2026-05-08

Session 132 — Eyemote Icons, Concentric Donuts, Categories Fix

Sidebar — Eyemote Icons (replacing dice):

  • Replaced dice icons with level-based eyemote icons (🟢🟡🟠🔴🔥💀)
  • Per-metric icon sets (CPU, MEM, DISK, LOAD, TEMP, NET)
  • Doubled icon size (1.4rem) for better visibility
  • New getMetricIcon(pct, type) function

SOC Dashboard — Categories Donut Fix:

  • Fixed duplicate waf-categories assignments causing "Loading..." display
  • Removed orphan assignment in CrowdSec section (undefined cats variable)
  • Categories now properly render concentric ring donut
  • Colorized stat cards with accent colors

WAF Dashboard — Severity/Category Donuts:

  • Added concentric severity donut with emojis (💀🔴🟠🟡🔵)
  • Replaced severity badges with emoji spans
  • Category donut with eyemote-style visualization

System Dashboard — Resource Gauge:

  • Concentric ring gauge for CPU/RAM/Disk/Load
  • 4-ring visualization with health center indicator
  • Legend with per-metric values

Files Updated:

  • packages/secubox-hub/www/shared/sidebar.js — Eyemote icons
  • packages/secubox-hub/www/soc/index.html — Category fix, colorized cards
  • packages/secubox-waf/www/index.html — Severity/category donuts
  • packages/secubox-system/www/system/index.html — Resource gauge

Session 131 — Dice Icons, Scribe Trace, LED Pulse Improvements

Sidebar — Dice Icons for Smart Strip:

  • Replaced LED dot indicators with dice icons (⚀⚁⚂⚃⚄⚅)
  • Dynamic update based on metric value (0-100% → dice 1-6)
  • New getDiceForPercent() helper function
  • Icons colored per metric type (CPU=red, MEM=yellow, etc.)

SOC Dashboard — Scribe Trace Histogram:

  • Replaced bubble stats with vertical histogram visualization
  • EEG/encephalogram style for firewall packets (DROP/ACCEPT/REJECT)
  • Rolling history of 20 samples
  • Vertical bars stacked by packet type

SOC Dashboard — API Path Fixes:

  • Fixed API_HUB path from /api/v1/hub to /api
  • Updated firewall_summary to /api/public/firewall_summary
  • Removed category tags (keeping donut metrics only)

WAF Dashboard — Syntax Fixes:

  • Removed orphan return statements from getSiteEmoji function
  • Fixed extra closing brace causing "return not in function" error
  • Verified brace balance across all script blocks

HealthBump LED Service — Pulse Improvements:

  • LED1 (HW): 1 flash per cycle (slowest)
  • LED2 (SVC): 2 flashes per cycle (medium)
  • LED3 (SEC): 4 flashes per cycle (fastest)
  • Pulse direction: bright → dim → bright (not dark to color)
  • Variable pulse timing for visual differentiation

Files Updated:

  • packages/secubox-hub/www/shared/sidebar.js — Dice icons, getDiceForPercent
  • packages/secubox-hub/www/soc.html — Scribe trace, API fixes, no category tags
  • packages/secubox-waf/www/index.html — Syntax fixes
  • scripts/secubox-healthbump — 1/2/4 flash patterns

Session 130 — Eyeremote Style Visualizations + SOC/WAF Improvements

Hub Dashboard — Concentric System Health Gauge:

  • New createConcentricGauge(cpu, mem, disk) function
  • Eyeremote-style visualization with 3 concentric rings
  • Icons positioned at 12 o'clock (🔥CPU, 🧠RAM, 💾Disk)
  • Center displays average load percentage
  • Color-coded legend with live values
  • Replaces simple ring gauges for richer visualization

SOC Dashboard Improvements:

  • Added firewall packet stats bubbles (🛑DROP, ACCEPT, REJECT)
  • CrowdSec category multi-layer donut with createMultiLayerDonut()
  • Fixed sidebar.js injection (was missing)
  • Fixed CATEGORY_EMOJI syntax error

WAF Statistics Page Enhancements:

  • Restored separate Severity/Category donuts (user preference)
  • New createCountryDonut() with country flags as icons
  • New createSiteDonut() with site-specific emojis
  • Fixed parseDuration() to handle compound formats like "22m47s"
  • Dual visualization: donut + bar graph for Top Countries/Sites

Backend API — Firewall Summary:

  • New endpoint: /api/v1/hub/public/firewall_summary
  • Reads from nftables cache files (permission workaround)
  • Returns: tables, chains, rules, processed, dropped, accepted, counters
  • Cron job updates /var/cache/secubox/nft-counters.txt every 30s

IPv6 Support Confirmed:

  • CrowdSec has both crowdsec (IPv4) and crowdsec6 (IPv6) tables
  • Counters include both address families

Files Updated:

  • packages/secubox-hub/www/index.html — Concentric gauge, ring gauge functions
  • packages/secubox-hub/www/soc.html — Firewall stats, category donut, sidebar
  • packages/secubox-waf/www/index.html — Country/Site donuts, duration fix
  • packages/secubox-hub/api/main.py — firewall_summary endpoint
  • /etc/cron.d/secubox-nft-stats — Cron for nftables cache

Session 129 — Eye Remote Radar Animation + SOC Fixes

Radar Dashboard Created (remote-ui/round/radar.html):

  • New radar-style visualization for Eye Remote HyperPixel 2.1 Round
  • Classic radar sweep animation with green phosphor aesthetic
  • 6 module blips (AUTH/WALL/BOOT/MIND/ROOT/MESH) at 60° intervals
  • Blips light up when radar sweep passes them
  • Color-coded status: green=ok, yellow=warn, red=critical
  • Concentric grid rings (60, 100, 140, 180, 210px)
  • 12 radial grid lines
  • Center displays: time, date, hostname, system status
  • Top mini-metrics: CPU%, MEM%, DSK%
  • Transport badge: OTG/WiFi/SIM auto-detection
  • Ticker bar for alerts
  • Simulation mode for offline testing
  • Module thresholds: AUTH(CPU 70/85), WALL(MEM 75/90), BOOT(DISK 80/95), MIND(LOAD 2/4), ROOT(TEMP 65/75), MESH(WiFi -70/-80dBm)

SOC Dashboard Fixes (earlier this session):

  • Added shared sidebar injection to SOC page
  • Fixed CrowdSec health detection (direct LAPI HTTP + sudo for cscli)
  • Applied SecuBox Six-Stack colorimetry to stat cards
  • Implemented tiered auto-ban in CrowdSec profiles.yaml

Files Created:

  • remote-ui/round/radar.html — v1.0 Radar dashboard

Session 128 — LED Tooltips + Kernel nftables Fix

Sidebar v2.32.0:

  • Per-LED tooltips: each LED row (Hardware/Services/Security) now has its own tooltip
  • Hardware tooltip: CPU/MEM/DISK/LOAD histograms with current/average/max values
  • Services tooltip: OK/WARN/ERROR/UNKNOWN service counts
  • Security tooltip: Active bans and recent alerts from CrowdSec
  • Fixed tooltip positioning for sidebar elements (was offset by navbar width)

SOC Page Fix:

  • Fixed nginx /soc/ route that was incorrectly proxying to API instead of serving static files
  • SOC dashboard HTML now loads correctly

Kernel nftables Issue (GitHub #64):

  • Discovered kernel 6.6.137 missing critical nftables options:
    • CONFIG_NF_TABLES_INET - inet family support
    • CONFIG_NF_TABLES_IPV4 - IPv4 rules
    • CONFIG_NFT_CT, CONFIG_NFT_LOG, CONFIG_NFT_REJECT, etc.
  • CrowdSec bouncer in restart loop due to Operation not supported errors
  • Updated board/mochabin/kernel/config-6.12-openwrt-merged.fragment with complete nftables config
  • Created issue #64 with bouncer health alert requirements (CSPN critical)

Files Updated:

  • www/shared/sidebar.js — v2.32.0 with per-LED tooltips
  • www/shared/hybrid-skin.css — Service/Security tooltip CSS styles
  • board/mochabin/kernel/config-6.12-openwrt-merged.fragment — Full nftables support

Session 127 — Smart Strip + LED Pulsing + Round UI Virtual

(earlier today - see v2.25.0 to v2.31.0 changes)


Session 126 — Hybrid Skin License & Centralized Injection

License Headers Added:

  • Added proper CyberMind/Gérald Kerma license and attribution to all theming files
  • All files now include: author, license (Proprietary/ANSSI CSPN), location, project info
  • Design references documented in code comments

Centralized Hybrid Skin Injection (sidebar.js v2.19.0):

  • injectHybridSkin() function auto-loads CSS on any page with sidebar
  • Injects: design-tokens.css, sidebar.css, hybrid-skin.css
  • Adds hybrid-skin class to body, hybrid-main to main content
  • All modules automatically get hybrid skin via navbar loading
  • No need to patch individual module index.html files

Files Updated:

  • www/shared/sidebar.js — v2.19.0 with hybrid skin injector
  • www/shared/hybrid-skin.css — Full license header with design references
  • www/shared/design-tokens.css — Six-Module Color System documentation
  • www/shared/sidebar.css — Glass Morphism sidebar license header
  • www/soc/index.html — SOC Dashboard with architecture documentation

License Format (Standard):

SecuBox-Deb :: <Module Name>
CyberMind — https://cybermind.fr
Author: Gérald Kerma <gandalf@gk2.net>
License: Proprietary / ANSSI CSPN candidate
Location: Notre-Dame-du-Cruet · Savoie · France
COPYRIGHT (C) 2024-2025 CyberMind / Gérald Kerma

Deployed to Production:

  • All updated files synced to 192.168.1.200:/usr/share/secubox/www/
  • Local/live sync verified (all files match)

Session 125 — Socket Repair & Health API Planning

Socket Repair Complete:

  • Fixed 91 running services with 87 Unix sockets
  • ai-gateway: Fixed permission denied for /tmp/secubox/ai-gateway cache dir
  • mcp-server: Socket now active after service configuration fix
  • Restarted and verified: crowdsec, vhost, wireguard, system
  • Hub uses TCP:8001 by design (VM compatibility)

Health API Standardization Plan:

  • Designed navbar-compliant health response schema
  • Fields: status, module, version, enabled, dev_stage
  • Batch health endpoint for efficient polling
  • Sidebar.js updates for version/dev_stage display
  • Retrofit strategy for 116 modules

Services Verified:

  • hub (v1.7.0), waf (v1.2.0), crowdsec (v2.0.0), haproxy, vhost
  • wireguard (v2.0.0), system (v1.2.0), ai-gateway, mcp-server
  • ⚠️ dns degraded (unbound not running), metrics needs /health

Session 122 — WAF GeoIP Country Lookup & Stats Enhancement

WAF API Enhancements:

  • Added GeoIP country lookup using MaxMind GeoLite2-Country database
  • Added top_countries to WAF threat stats (identifies attacking countries)
  • Added top_vhosts to WAF threat stats (most targeted vhosts)
  • Fixed IP field name: ipclient_ip for log compatibility
  • Added _lookup_country() with caching and LAN detection

Files Updated:

  • packages/secubox-waf/api/main.py — GeoIP integration
  • Source files synced to debian package directories

Session 123 — Health-Aware Sidebar, WAF Alerting & ACME Certs

Sidebar v2.0.0 (Emoji LED + Pre-cache):

  • Emoji LED status: 🟢ok 🟡warn 🔴error unknown 🔵checking
  • Auto-sort: healthy first (ok → warn → unknown → error)
  • Pre-cache in localStorage for instant display on load
  • Quick error toast (no buttons, auto-dismiss 2.5s)
  • Pre-flight health checks on navigation
  • 30-second periodic health refresh

WAF WebUI Alerting:

  • Live threat ticker with pulsing red indicator
  • 7680 total threats, 2813+ today detected
  • Alerting tab with filterable list (severity, category, IP)
  • Export alerts to CSV
  • Quick ban buttons on each alert
  • Compact category listing with emoji + toggle

WAF Threat Log Fix:

  • Symlinked /var/log/secubox/waf-threats.log/srv/mitmproxy/logs/waf-threats.log
  • Mitmproxy logs now accessible to WAF API

LXC Network Fix:

  • Updated lxc-network-fix.service to run continuously (30s loop)
  • Fixed all container symlinks in /var/lib/lxc/
  • Disabled cgroup2.cpu.max in all container configs

Certificate Status (CRITICAL):

  • 15+ certificates EXPIRED
  • 40+ expiring within 30 days
  • Certificates at /data/haproxy/certs/

ACME Certificate Manager WebUI:

  • Created /certs/ dashboard with emoji TTD indicators
  • 💀expired 🔴critical 🟡warning 🟢healthy
  • Pre-flight wizard with DNS/HTTP/ACME checks
  • Origin emoji icons per backend type

Files Deployed:

  • /usr/share/secubox/www/shared/sidebar.js — v2.0.0 pre-cache
  • /usr/share/secubox/www/waf/index.html — alerting system
  • /usr/share/secubox/www/certs/index.html — ACME manager
  • /etc/systemd/system/lxc-network-fix.service — continuous veth fix

Session 122 — WAF Architecture Fix & Eye Remote Integration

mitmproxy LXC Container Fix:

  • Container was STOPPED causing 503 on all vhosts
  • Fixed cgroup2.cpu.max config preventing startup
  • Created lxc-network-fix.service to auto-start veth interfaces
  • mitmproxy now running with 145 routes and 150 WAF rules

HAProxy Routing Refactor:

  • Changed all vhosts from nginx_vhostsmitmproxy_inspector
  • All HTTP traffic now flows through WAF inspection
  • Fallback backend changed from 503 deny to mitmproxy pass-through
  • Traffic flow: HAProxy → mitmproxy (WAF) → nginx/backends

Eye Remote Dashboard:

  • Fixed /eye-remote/ page (nginx location was missing)
  • Simplified JS to work with pizero-metrics API
  • Added Quick Commands mini card
  • API proxy: /api/v1/eye-remote/*10.55.0.2:8000/*

USB Gadget Network:

  • usb0 interface at 10.55.0.1/24
  • Pi Zero W responding at 10.55.0.2:8000
  • Live metrics: CPU, RAM, Temp, Uptime, Load

CrowdSec Status:

  • 100+ active bans (SSH brute-force from DE/NL/RO/SE)
  • WAF threats log ready at /srv/mitmproxy/logs/waf-threats.log

Dependencies Added:

  • netcat-openbsd for diagnostics

Files Modified:

  • /etc/haproxy/haproxy.cfg — All vhosts through mitmproxy
  • /etc/nginx/sites-enabled/webui.conf — Eye Remote locations
  • /var/lib/lxc/mitmproxy/config — Fixed network config
  • /etc/systemd/system/lxc-network-fix.service — Auto veth startup
  • /usr/share/secubox/www/eye-remote/index.html — Simplified dashboard
  • /usr/share/secubox/www/eye-remote/js/eye-remote.js — pizero-metrics API

Session 121 — HealthBump v2.1 with Activity Detection & K2000

Features Added:

  • Activity-based brightness: ACTIVE (100) when metrics change, SLEEP (20) when stable
  • K2000 (Knight Rider) sweep effect for boot/success announcements
  • Alert mode (red K2000) for warnings
  • Rainbow party mode for testing

I2C Timing Alignment:

  • Rewritten to use same values as secubox-led-safe:
    • WRITE_DELAY=0.3 (300ms)
    • ERROR_BACKOFF=3 (3s)
    • MAX_ERRORS=5
    • RESET_THRESHOLD=3
  • Simplified to /bin/sh (POSIX compliant like led-safe)

Commands:

secubox-healthbump              # Health check (default)
secubox-healthbump k2000 2 cyan # K2000 sweep
secubox-healthbump success      # Boot announcement
secubox-healthbump alert 2      # Red alert sweep
secubox-healthbump rainbow      # All colors party
secubox-healthbump off          # Turn off LEDs

I2C Bus Recovery:

  • Full rebind: echo '80018000.i2c' > /sys/bus/platform/drivers/mv64xxx_i2c/unbind
  • Then bind + modprobe to recover from lockups

Files Updated:

  • packages/secubox-led-heartbeat/usr/sbin/secubox-healthbump
  • packages/secubox-led-heartbeat/systemd/secubox-healthbump.service
  • docs/LED-HEALTHBUMP.md

Session 120 — LED System Complete & Kernel 6.6.137 Validation

Root Cause Analysis (Systematic Debugging):

  1. Evidence Gathered

    • Kernel 6.12.85: mv64xxx_i2c_fsm: Ctlr Error -- state: 0x2, status: 0x0
    • I2C bus completely locked after rapid LED writes
    • Only recoverable via reboot
  2. Data Flow Trace

    sysfs → leds-is31fl319x → regmap → i2c-mv64xxx → IS31FL3199
                                         ↑ FAILURE (controller stuck)
    
  3. Hypothesis Tested

    • Kernel 6.6.137 LTS (same generation as OpenWrt) should work
    • Result: CONFIRMED - LEDs working perfectly on 6.6.137

Validation via TTY:

  • Red/Green/Blue: Bright and perfect
  • Manual control (secubox-led): Working
  • HealthBump stats: Working
  • I2C bus: Stable, no errors

Package Fixes Committed:

  • debian/rules — Installs all HealthBump scripts
  • debian/postinst — Enables healthbump.timer + led-pulse.service
  • debian/prerm — Stops all services, turns off LEDs
  • debian/changelog — Version 2.0.0-1~bookworm1

Boot Configuration:

  • Default kernel: kernel66 (6.6.137 LTS)
  • LED support: Native, no patches needed

Commits:

  • 853f9a6d fix(led-pkg): Update packaging for HealthBump 3-tier system
  • 5c3afdd5 feat(kernel): Add I2C timing patches for LED driver reliability

Session 119 — I2C Timing Investigation and Kernel Patches

Investigation:

  1. Root Cause Analysis — Errata FE-8471889

    • mv64xxx I2C controller has timing issues on Armada platforms
    • Mainline kernel has 5µs delay fix, but insufficient for LED controllers
    • OpenWrt kernel works due to different build/timing characteristics
  2. OpenWrt Patches Review

    • Checked OpenWrt mvebu patches-6.12 for I2C fixes
    • Found i2c-pxa patches for Armada 3700 (not applicable to 7040)
    • 304-revert_i2c_delay.patch only affects Armada XP (32-bit)
  3. Kernel Configuration Comparison

    • Both OpenWrt and custom kernel use CONFIG_I2C_MV64XXX=y
    • I2C clock at 100kHz (standard mode) - correct for errata fix
    • DTB uses marvell,mv78230-i2c compatible - errata should trigger

Solution — Kernel Patches Created:

  1. 001-leds-is31fl319x-add-i2c-delays.patch

    • Adds 1ms usleep_range() between regmap writes
    • Prevents rapid I2C transactions that cause bus errors
  2. 002-i2c-mv64xxx-increase-errata-delay.patch

    • Increases errata delay from 5µs to 50µs
    • Provides more margin for I2C bus settling

Files Created:

  • kernel-build/patches/001-leds-is31fl319x-add-i2c-delays.patch
  • kernel-build/patches/002-i2c-mv64xxx-increase-errata-delay.patch
  • Updated kernel-build/README.md with patch instructions

References:


Session 118 — LED HealthBump 3-Tier System

Completed:

  1. Kernel with IS31FL3199 LED Driver — Built-in LED support

    • Kernel 6.12.85 with CONFIG_LEDS_IS31FL319X=y
    • LEDs appear at /sys/class/leds/{red,green,blue}:led{1,2,3}/
    • Discovered brightness 10 optimal (255 causes I2C EIO errors on Debian)
  2. 3-Tier LED HealthBump System — Visual health indicator

    • LED1 (bottom): Hardware layer - CPU, memory, WAN connectivity
    • LED2 (middle): Services layer - HAProxy, Nginx, certificate expiry
    • LED3 (top): Security layer - CrowdSec bans, attack rate detection
    • Variable pulse speeds: slow (HW), medium (SVC), fast (SEC)
  3. SPUNK ALERT — Critical failure rapid flash

    • All LEDs flash rapid red when HAProxy/CrowdSec down
    • Ported from OpenWrt secubox-led-pulse script
    • Overrides normal health status until services recover
  4. Manual LED Controlsecubox-led command

    • Layer aliases: hw/1/bottom, svc/2/middle, sec/3/top
    • Status colors: ok/green, warn/yellow, error/red, msg/blue, off
  5. OpenWrt LED Script Analysis — Serial console retrieval

    • Found /overlay/upper/usr/sbin/secubox-led-pulse via ttyUSB0
    • OpenWrt uses brightness 255 without I2C errors (different driver timing)
    • Saved reference in docs/reference/secubox-led-pulse-openwrt.sh

Files Created/Updated:

  • packages/secubox-led-heartbeat/usr/sbin/secubox-healthbump — 3-tier health check
  • packages/secubox-led-heartbeat/usr/sbin/secubox-led — Manual LED control
  • packages/secubox-led-heartbeat/systemd/secubox-healthbump.{service,timer} — Systemd units
  • docs/LED-HEALTHBUMP.md — Documentation with pulse speeds and SPUNK ALERT
  • docs/reference/secubox-led-pulse-openwrt.sh — OpenWrt reference script

Current Status:

  • HealthBump running on 30s timer
  • LED1: Yellow (medium load), LED2: Green (services ok), LED3: Blue (mitigating 100 bans)

Session 117 — OpenWrt-style Kernel with DSA Built-in

Completed:

  1. Debian Kernel Boot Fix — Switched to Debian kernel for DSA support

    • Custom kernel 6.12.85 had DSA module chain incomplete
    • Copied Debian kernel vmlinuz-6.12.85+deb12-arm64 to FAT boot partition
    • DSA modules (mv88e6xxx, dsa_core) loaded properly
    • lan0/1/2/3 interfaces restored
  2. OpenWrt Kernel Config Analysis — Studied OpenWrt MVEBU config

    • Downloaded OpenWrt mvebu + cortexa72 configs
    • Key insight: OpenWrt uses =y (built-in), Debian uses =m (modules)
    • OpenWrt approach: faster boot, no initrd dependency for network
  3. Created OpenWrt-style Config Fragment — 365 lines merged config

    • board/mochabin/kernel/config-6.12-openwrt-merged.fragment
    • Merges OpenWrt MVEBU DSA config + Debian systemd compatibility
    • All boot-critical and DSA drivers built-in (=y)
    • Includes: IKCONFIG, WireGuard, nftables, LED triggers, crypto
  4. Built & Deployed OpenWrt-style Kernel — DSA built-in, no modules

    • Kernel 6.12.85 with OpenWrt-merged config
    • CONFIG_NET_DSA=y (built-in)
    • CONFIG_NET_DSA_MV88E6XXX=y (built-in)
    • CONFIG_MVPP2=y (built-in)
    • CONFIG_IKCONFIG=y (/proc/config.gz available)
    • Kernel boot time: 6.6s (fast, no module loading)
  5. Verified Working

    • DSA interfaces: lan0/1/2/3 created at boot
    • No DSA modules loaded (all built-in)
    • /proc/config.gz available (IKCONFIG)
    • HAProxy running
    • WAN uplink: 192.168.1.254 gateway, internet OK

Files Created:

  • board/mochabin/kernel/config-6.12-openwrt-merged.fragment — OpenWrt+Debian merged config

Boot Menu (extlinux.conf):

  1. OpenWrt-style Kernel (default) — DSA built-in, no initrd
  2. Debian Kernel — modules, needs initrd
  3. SecuBox Custom — previous build

2026-05-07

Session 116 — Kernel 6.12.85 Boot Fix (Built-in Drivers)

Completed:

  1. Kernel Boot Crisis Resolution — Fixed MOCHAbin unable to boot

    • Root cause: Critical drivers compiled as modules (=m) instead of built-in (=y)
    • Multiple rebuild cycles to identify all missing built-in drivers
    • Created USB rescue boot system for recovery
  2. Built-in Driver Fixes — All boot-critical drivers now =y

    • CONFIG_MMC_SDHCI_XENON=y — eMMC controller (was causing mmcblk0 not found)
    • CONFIG_EXT4_FS=y — Root filesystem (was causing VFS panic)
    • CONFIG_VFAT_FS=y — Boot partition mount
    • CONFIG_NLS_ASCII=y, CONFIG_NLS_UTF8=y — VFAT charset (was "IO charset ascii not found")
    • CONFIG_PHY_MVEBU_CP110_UTMI=y — USB PHY (was "deferred probe pending: wait for supplier ut0")
    • CONFIG_PHY_MVEBU_CP110_COMPHY=y — PCIe/SATA PHY
    • CONFIG_BLK_DEV_SD=y — SCSI disk driver (was no /dev/sda creation)
    • CONFIG_AHCI_MVEBU=y — SATA controller
    • CONFIG_MVPP2=y — Network driver
  3. Hardware Verified Working

    • eMMC: 14.7 GiB detected (mmcblk0p1/p2)
    • SATA: WD Blue SA510 1TB @ 6Gbps
    • USB: Both xHCI controllers (f2500000, f2510000)
    • Network: eth0/eth1/eth2 with MAC addresses
    • PCIe: Root port initialized (no device connected)
  4. Kernel Fragment Saved — For future builds

    • board/mochabin/kernel/config-6.12.85-secubox-boot.fragment
    • Contains all MOCHAbin boot-critical options
    • Usage: merge_config.sh + olddefconfig + make Image

Session 115 — Kernel Documentation & DISK I/O Metric

Completed:

  1. DISK I/O Metric for Eye Remote — Replaced MESH WiFi metric

    • New metric: io_read_mb, io_write_mb, io_read_peak_mb, io_write_peak_mb
    • Reads from /proc/diskstats (mmcblk0/sda/nvme0n1)
    • Peak tracking persisted across restarts
    • Legend updated: 💾 DISK / I/O MB/s
    • Deployed to MOCHAbin production
  2. MOCHAbin Kernel Documentation — Wiki page created

    • docs/wiki/MOCHAbin-Kernel.md: Full LED kernel build guide
    • Debian base config + SecuBox fragment approach documented
    • GPIO polarity fix for IS31FL3199 documented
    • Added to wiki sidebar
  3. Kernel Config Files — Committed to git

    • board/mochabin/kernel/config-6.12.85-debian-base (13K options)
    • board/mochabin/kernel/config-6.12.85-secubox-led-v2.fragment
    • Build script with CyberMind branding: LOCALVERSION=-secubox-cybermind
  4. CyberMind Branding — Applied to kernel

    • LOCALVERSION set to -secubox-cybermind
    • Produces: 6.12.85-secubox-cybermind
    • Build script header updated with CyberMind URL

Session 114 — Round Eye Connections Metric

Completed:

  1. Connections Metric for MIND — Added to Eye Remote API
    • connections: Current established TCP connections count
    • peak_connections: Maximum observed (persisted to /var/cache/secubox/eye-remote/peak_connections)
    • connections_percent: Pre-calculated ratio for Round Eye MIND ring
    • Reads from /proc/net/tcp and /proc/net/tcp6 (no subprocess overhead)
    • Documentation updated in README.md and WIKI.md

Session 113 — WAF False Positive Fixes

Completed:

  1. WAF Rules v1.3.0 — Fixed 7 false positive patterns
    • lfi-001: Require 3+ levels of directory traversal
    • rce-006: Target Python SSTI patterns only (not all Jinja2/Vue.js)
    • scan-004: Only block xmlrpc abuse, not legitimate WordPress
    • waf-fp-005: Target MySQL version comments, not CSS
    • recon_crawler: Disabled category (was blocking robots.txt, .well-known)
    • cred-002/006: Detect URL-leaked secrets only, not auth headers
    • Commit: fix(waf): Fix 7 false positive patterns in WAF rules v1.3.0

Session 111 — LED Kernel + CrowdSec GeoIP + Boot Fixes

Completed:

  1. LED Kernel Configuration (Issue #60)

    • Fixed network drivers: MARVELL_PHY, MDIO, SFP, MDIO_I2C → built-in (=y)
    • Fixed USB drivers: XHCI, EHCI, OHCI, USB_STORAGE → built-in
    • Kernel build initiated with updated config
    • LED chip detected at I2C-1 address 0x64 (IS31FL319X)
  2. CrowdSec WebUI GeoIP Enhancement

    • Added country flags to bans/decisions list
    • Implemented GeoIP cache with 24h TTL (localStorage)
    • Uses ipapi.co (HTTPS) with backend fallback
    • Commit: feat(crowdsec): Add GeoIP cache with country flags
  3. WAF Client IP Fix

    • Fixed mitmproxy to read X-Forwarded-For header
    • WAF now logs real attacker IP (not HAProxy internal IP)
  4. GitHub Issues Created

    • #59: 503 errors at boot (service startup delay)
    • #60: LED kernel with IS31FL319X and built-in drivers
    • #61: Eye Remote gadget metrics endpoint

In Progress:

  • Kernel build (~25% complete)
  • 503 error permanent fix (HAProxy/mitmproxy chain)

2026-05-06

Session 109 — VHost Matrix Sync + Eye Remote Fixes

Completed:

  1. VHost Matrix Sync Tool (scripts/vhost-matrix-sync.sh)

    • Python-based HAProxy parsing (reliable regex extraction)
    • Fixed stderr logging for clean JSON capture
    • Syncs HAProxy vhosts → mitmproxy routes + health prober
    • Uses 10.100.0.1 (LXC bridge IP) for proper routing
    • Successfully synced 94 vhosts on production server
  2. Eye Remote Dashboard Fixes

    • API calls now use public endpoints (no JWT required)
    • Added /api/v1/system/metrics alias for Pi Zero compatibility
    • Pi Zero round UI displays correct MOCHAbin host metrics
  3. HAProxy VHost Additions

    • Added sdlc.gk2.secubox.in and facb.gk2.secubox.in backends
    • Both routed through mitmproxy WAF inspector
  4. GitHub Issue #49: MetaBlogizer + Streamlit version management via Gitea


Session 102 — v2.5.0 WAF Integration Complete

Goal: Complete WAF mitmproxy LXC integration (all 5 phases)

Completed:

  1. CMSD-1.0 License Integration

    • Created LICENCE-CMSD-1.0.md (French authoritative version)
    • Created LICENSE-CMSD-1.0.en.md (English informative translation)
    • Created LICENSING.md (license documentation, SPDX guidance)
    • Updated README.md with prominent license notice (CAN/CANNOT table)
    • Wiki pages: License.md, License-FR.md with QR codes
    • PDF booklet uploaded to GitHub release v2.4.0
  2. WAF Phase 1-4: Mitmproxy LXC Container

    • LXC container at /data/lxc/mitmproxy (10.100.0.60:8080)
    • 330 HAProxy backends routing through mitmproxy_inspector
    • HAProxy http-request set-uri for proxy-style requests
    • All traffic tagged with X-SecuBox-WAF: inspected header
    • All 6 LXC containers verified running
  3. WAF Phase 5: Package Updates

    • secubox-waf v1.1.0: Added LXC mitmproxy support, wafctl, systemd service
    • secubox-haproxy v1.2.0: Added waf subcommand (status/enable/disable)
    • WebUI dashboard: Added mitmproxy container status card
  4. WebUI Access Fixed

    • Added 192.168.1.200:9443 HAProxy bind
    • Added nginx server_name for 192.168.1.200
    • WebUI accessible at https://192.168.1.200:9443/
    • Created webui_direct backend (bypasses WAF)

Session 101 — C3BOX Network Recovery + HAProxy LXC Routing

Goal: Establish network connectivity between C3BOX and MOCHAbin for migration

Completed:

  1. C3BOX Network Recovery

    • Fixed eth2 NO-CARRIER issue (was on wrong interface)
    • C3BOX lan0@eth1 connected to MOCHAbin lan0 (DSA switch)
    • IP assigned on br-lan: 192.168.255.201 (original) + .10 (secondary)
    • Connectivity established: C3BOX ↔ MOCHAbin via 192.168.255.x
  2. Migration Archive Imported

    • 93 SSL certificates copied to /data/haproxy/certs/
    • 99 nginx secubox.d configs available
    • LXC container configs imported
  3. HAProxy LXC Routing Added

    • Created backends: lxc_gitea, lxc_nextcloud, lxc_matrix
    • ACL routing for gitea.gk2.secubox.in → 10.100.0.40:3000
    • ACL routing for nextcloud.gk2.secubox.in → 10.100.0.20:80
    • ACL routing for matrix.gk2.secubox.in → 10.100.0.30:8008
  4. Routing Verified

    • gk2.secubox.in → 200 (WebUI)
    • gitea.gk2.secubox.in → 200 (LXC)
    • nextcloud.gk2.secubox.in → 302 (LXC redirect)
    • blog.cybermind.fr → 200 (nginx_vhosts)
    • Unknown domains → 503 (correct fallback)
  5. Metablogizer Migration COMPLETE

    • 166 sites synced from C3BOX (/srv/metablogizer/sites/)
    • 60 sites emancipated (published) with nginx + HAProxy routing
    • UCI config converted to nginx server blocks (per-port)
    • Fixed HAProxy ACL order (metablog backends vs nginx_vhosts)
    • All sites accessible from internet with correct content

TODO (noted for later):

  • Implement mitmproxy WAF container (like C3BOX architecture)
  • HAProxy cacert + vhost SSL verification
  • Metablogizer TOML config conversion

Session 101 continued — Source Package Sync

Goal: Sync source packages with deployed working configurations

Completed:

  1. secubox-streamlit package updated:

    • API main.py: Added sudo -n for LXC commands (NoNewPrivileges workaround)
    • Added systemd drop-in: debian/secubox-streamlit.service.d/allow-lxc.conf
    • Added sudoers config: sudoers.d/secubox-streamlit
    • Added example config: config/streamlit.toml.example
    • Updated postinst: Creates config dir, example config, LXC symlink
    • Updated debian/rules to install new files
  2. secubox-metablogizer package updated:

    • Added example config: config/metablogizer.toml.example
    • Updated debian/rules to install example config
  3. TOML configs saved:

    • .claude/configs/streamlit.toml (35 apps, 29 instances)
    • .claude/configs/metablogizer.toml (151 sites)

Session 100 — MOCHAbin Migration SUCCESS

Goal: Complete C3BOX → SecuBox-DEB migration with proper WAF and routing

Completed:

  1. Network Configuration

    • WAN (eth2): 192.168.1.200/24 → Freebox DMZ
    • LAN (br-lan): 192.168.255.1/24 via systemd-networkd (DSA bridge)
    • LXC (br-lxc): 10.100.0.1/24
    • Default route via 192.168.1.254 (Freebox)
  2. LXC Containers Running

    • gitea: 10.100.0.40
    • mail: 10.100.0.10
    • matrix: 10.100.0.30
    • nextcloud: 10.100.0.20
  3. HAProxy WAF (ACL-based)

    • SQL Injection detection → 403
    • XSS detection → 403
    • Path Traversal detection → 403
    • Scanner detection (nikto, sqlmap, nuclei) → 403
  4. Routing Verified

    • Unknown domains/IP → 503 (correct fallback)
    • gk2.secubox.in → 200 (WebUI)
    • gitea.gk2.secubox.in → LXC gitea
    • nextcloud.gk2.secubox.in → LXC nextcloud
  5. Network Persistence

    • /etc/netplan/01-secubox-gateway.yaml — WAN/LXC config
    • /etc/systemd/network/10-br-lan.network — LAN (DSA bridge)
  6. CTL Tools Installed

    • 17 tools in /usr/sbin/ (haproxyctl, vhostctl, streamlitctl, etc.)

Key Fix: br-lan IP (192.168.255.1) was missing — added via systemd-networkd since DSA bridge not managed by netplan.

Mitmproxy Status: Disabled due to pyOpenSSL ARM64 incompatibility. HAProxy ACL-based WAF provides equivalent protection.

Custom Error Page Added:

  • /etc/haproxy/errors/503.http — "FATAL ERROR / END OF INTERNET" page
  • Unknown domains return custom 503 (cyberpunk skull design)
  • WebUI ACL added for: gk2.secubox.in, admin.gk2.secubox.in, secubox.local, secubox.maegia.tv, c3box.maegia.tv
  • Fallback backend changed from nginx_vhosts to fallback_503

2026-05-05

Session 99 — MOCHAbin Migration Recovery Plan

Goal: Document lessons learned from failed migration and create proper procedure

Analysis of Session 97 Failure:

  1. HAProxy manually configured with only 5 ACLs instead of all 93 domains
  2. Default backend incorrectly set to nginx_vhosts (WebUI) instead of 503 error page
  3. WAF (mitmproxy) not installed due to OpenSSL compatibility issue
  4. Websites not accessible from internet despite HAProxy showing 200 locally
  5. User reverted to old C3BOX

Root Causes Identified:

  • Did not use haproxyctl migrate command
  • Did not use scripts/migration-export.sh for full export
  • Manual HAProxy config used wrong fallback backend pattern

Documentation Created:

  • Updated .claude/WIP.md with comprehensive migration checklist
  • Documented proper 8-step migration procedure
  • Added verification checklist for next attempt
  • Documented key files and error page requirement

Proper Migration Tools:

  • scripts/migration-export.sh — Full export from OpenWrt
  • scripts/migration-import.sh — Import to SecuBox-DEB with transformation
  • haproxyctl migrate <host> — HAProxy-specific migration with UCI→TOML conversion

Key Requirement:

# CORRECT fallback backend
backend fallback
    mode http
    http-request deny deny_status 503

# WRONG - never use WebUI as fallback for unmatched domains
# default_backend nginx_vhosts

Session 97 — MOCHAbin Migration Attempt (FAILED)

Goal: Full data migration from OpenWrt C3BOX to SecuBox-DEB MOCHAbin

Issues Encountered:

  • DSA (Distributed Switch Architecture) — lan0-lan3 can't be added to Linux bridges
  • SFP28-25G module incompatible with 10G SFP+ port (used eth2 copper instead)
  • nftables DNAT syntax in inet tables requires ip dnat to not just dnat to
  • mitmproxy crashed due to OpenSSL AttributeError (X509_V_FLAG_NOTIFY_POLICY)

Network Setup (Partial Success):

  • WAN on eth2 (copper) with DMZ IP 192.168.1.200/24
  • LAN on lan0 (DSA) with 192.168.255.1/24
  • br-lxc for containers with 10.100.0.1/24
  • LXC containers running (mail, nextcloud, gitea)

Critical Failure Points:

  1. HAProxy configured manually — only 5 ACLs/backends instead of 93
  2. WebUI set as fallback backend — domains without ACL showed admin panel
  3. Websites not actually accessible from internet
  4. WAF not functional

User Feedback: "you have missed a lot of works", "websites are not up", "worst, you make the webui admin on frontend fallback", "you make all badly"

Result: User reverted to old C3BOX. Migration needs complete redo with proper tools.


Session 98 — SecuBox Modem Module

Goal: Create comprehensive LTE/5G modem management module

Completed:

  1. Package Structure — Full package at packages/secubox-modem/

    • api/main.py — FastAPI application with background signal collector
    • api/routers/ — status, connection, sms, terminal routers
    • core/ — modem_detect, mm_client, qmi_client, at_interface, signal_history
    • www/modem/ — WebUI with tabs for Status, Signal, SMS, Terminal, Settings
  2. Modem Detection — Auto-detect Quectel modems

    • USB scanning via lsusb
    • ModemManager integration via mmcli
    • Known Quectel PIDs: EC25, EC21, EP06, EM12, RM500Q, RM520N, RG500Q
  3. Connection Management — ModemManager-based

    • Connect/disconnect with APN configuration
    • Config persistence in /var/lib/secubox/modem/
    • Known APN database (FR, US, generic)
  4. SMS Functionality — Full send/receive via mmcli

    • List messages, send SMS, delete
    • WebUI compose modal and message list
  5. AT Terminal — Interactive command console

    • WebSocket endpoint at /api/v1/modem/at/console
    • REST fallback at /api/v1/modem/at/command
    • Security: blocks dangerous commands (AT+CFUN=0, AT+QPOWD, etc.)
  6. Signal Monitoring — Real-time with history

    • Background collector every 30 seconds
    • Signal history stored in /var/cache/secubox/modem/
    • Chart.js graph in WebUI Signal tab
  7. QMI Client — Detailed signal queries

    • qmicli wrapper for RSRP, RSRQ, SINR, cell location
    • RF band information, serving system details
  8. Debian Packaging

    • debian/control — Dependencies: modemmanager, libqmi-utils, libmbim-utils, picocom
    • debian/postinst — Creates data dirs, adds secubox to dialout group
    • systemd/secubox-modem.service — With memory limits
  9. WebUI Features

    • P31 phosphor CRT theme (light mode)
    • Signal bars visualization
    • xterm.js AT terminal
    • Chart.js signal history graph
    • APN database quick-select

Files Created:

  • packages/secubox-modem/api/main.py — FastAPI app (~200 lines)
  • packages/secubox-modem/api/routers/status.py — Status/info/signal endpoints
  • packages/secubox-modem/api/routers/connection.py — Connect/disconnect/config
  • packages/secubox-modem/api/routers/sms.py — SMS CRUD
  • packages/secubox-modem/api/routers/terminal.py — WebSocket AT console
  • packages/secubox-modem/core/modem_detect.py — USB/mmcli detection
  • packages/secubox-modem/core/mm_client.py — ModemManager wrapper
  • packages/secubox-modem/core/qmi_client.py — qmicli wrapper
  • packages/secubox-modem/core/at_interface.py — Serial AT handler
  • packages/secubox-modem/core/signal_history.py — Signal cache
  • packages/secubox-modem/www/modem/index.html — Dashboard (~700 lines)
  • packages/secubox-modem/www/modem/js/modem.js — UI logic (~500 lines)
  • packages/secubox-modem/debian/* — Full Debian packaging
  • packages/secubox-modem/nginx/modem.conf — WebSocket-enabled proxy
  • packages/secubox-modem/menu.d/37-modem.json — Navbar entry
  • packages/secubox-modem/README.md — Comprehensive documentation

Migration Map Updated:

  • Added secubox-modem to module list
  • Total modules: 125

Deployed to MOCHAbin (192.168.255.10):

  • Fixed import paths (...corecore for absolute imports)
  • nginx config moved to /etc/nginx/secubox.d/modem.conf
  • Socket created at /run/secubox/modem.sock
  • Menu entry at /etc/secubox/menus.d/37-modem.json
  • Health endpoint verified: /api/v1/modem/health
  • WebUI accessible at /modem/

Session 95 — Eye Remote USB Gadget & Tow-Boot

Goal: Get Eye Remote (Pi Zero W USB gadget) working with MOCHAbin

Completed:

  1. Tow-Boot Flashed — Replaced old U-Boot 2018.03 with Tow-Boot for proper USB PHY init

    • Used bubt command for Marvell bootloader flash
    • Pre-built binary from tools/Tow-Boot/output/Tow-Boot.spi.bin
  2. Kernel 6.12 Boot — Working with CONFIG_PHY_MVEBU_CP110_UTMI

    • Fixed MAC address issue with setenv ethaddr
    • Fixed console: ttyMV0 → ttyS0 in extlinux.conf
    • Created /boot/extlinux/extlinux.conf with both kernels (default + 6.12)
  3. Eye Remote USB Detection — Pi Zero gadget detected on Bus 01

    • ECM Network + ACM Serial + Mass Storage interfaces
    • udev rules auto-configure 10.55.0.1/30 interface
  4. SSD Storage — 1TB mSATA mounted as /data

    • eMMC freed for system only
    • /data contains: secubox-backups, overlay upper/work dirs
  5. secubox-eye-remote Package Deployed

    • Service running: secubox-eye-remote.service (active)
    • Socket: /run/secubox/eye-remote.sock
    • Health endpoint working: /health
  6. udev Rules Deployed — Auto-configure USB network on connect

    • /etc/udev/rules.d/90-secubox-eye-remote.rules
    • /usr/local/sbin/secubox-eye-network.sh
    • Matches Pi Zero gadget by vendor/product ID (1d6b:0104)

API Status:

  • /api/v1/eye-remote/status — Working (connected=true)
  • /api/v1/eye-remote/serial/status — Working (/dev/ttyACM0 detected)
  • /health — Working
  1. Kernel 6.12 Default — Set as default boot in extlinux.conf

    • DEFAULT secubox-612 in /boot/extlinux/extlinux.conf
    • Running: 6.12.85+deb12-arm64
  2. Socket Creation Fix — RuntimeDirectoryPreserve for all services

    • Root cause: Multiple services with RuntimeDirectory=secubox caused socket cleanup conflicts
    • Fix: Added /etc/systemd/system/secubox-*.service.d/preserve.conf with RuntimeDirectoryPreserve=yes
    • All services now preserve their sockets when other services restart
  3. Nginx Proxy Path Fix — Eye Remote API routing

    • Issue: nginx proxy_pass http://unix:/run/secubox/eye-remote.sock:/; stripped path prefix
    • Fix: Changed to proxy_pass http://unix:/run/secubox/eye-remote.sock:/api/v1/eye-remote/;
    • FastAPI expects full path /api/v1/eye-remote/status
  4. Remote UI Emancipation — Unified WebUI path

    • /remote-ui/ now serves the Remote UI Management page
    • /eye-remote/ redirects to /remote-ui/
    • API remains at /api/v1/eye-remote/
  5. Socket Conflict Resolution — Definitive fix

    • Root cause: All services had RuntimeDirectory=secubox, causing conflicts when any service restarted
    • Fix: Created /etc/systemd/system/secubox-*.service.d/no-runtime-dir.conf with RuntimeDirectory=
    • secubox-core.service is now the ONLY service managing /run/secubox/
    • Result: 85+ sockets stable, no more conflicts
  6. JSON Error Fixes — Navbar component errors

    • Issue: Disabled services returned HTML 502 instead of JSON
    • Fix: Added /etc/nginx/snippets/api-error.conf returning JSON for 502/503/504
    • Services using include /etc/nginx/snippets/secubox-proxy.conf; now return proper JSON errors
  7. Service Emancipation — Full WebUI + API exposure

    • Emancipated 13 services with unified nginx configs:
      • crowdsec, waf, dpi, system, wireguard, netdata, haproxy
      • hub, admin, auth, metrics, glances, backup
    • Each service has: WebUI at /<service>/, API at /api/v1/<service>/
    • All services verified working (UI=200, API=200 or 401 for auth-required)
    • Created /srv/backups directory for backup service

Files Modified:

  • board/mochabin/flash-tow-boot.cmd — bubt flash script
  • board/mochabin/flash-tow-boot.txt — manual instructions
  • packages/secubox-eye-remote/api/main.py — Fixed interface check (usb0, ARP)
  • packages/secubox-eye-remote/udev/90-secubox-eye-remote.rules — Removed NAME rename
  • packages/secubox-eye-remote/scripts/secubox-eye-network.sh — Use usb0, notify API
  • packages/secubox-eye-remote/nginx/eye-remote.conf — WebUI + API + redirect

MOCHAbin Files Created:

  • /etc/nginx/snippets/api-error.conf — JSON error responses
  • /etc/nginx/secubox.d/*.conf — 13 service nginx configs
  • /etc/systemd/system/secubox-*.service.d/no-runtime-dir.conf — Socket conflict fix
  • /srv/backups/ — Backup storage directory
  1. CrowdSec Console Enrollment — Fixed key typo

    • Enrollment key had 1 (one) instead of l (lowercase L)
    • Corrected key: cmoleja50000802le9t1f7o0d
  2. CrowdSec Dashboard Cleanup — Removed obsolete UI elements

    • Removed Migration section (OpenWrt migration not needed)
    • Removed Components tab
    • Removed Access tab
    • Removed "Import from OpenWrt" button
  3. CrowdSec LAPI/CAPI Status Fix — Sudo privilege issue

    • Issue: NoNewPrivileges=true in systemd blocked sudo
    • Fix: Created /etc/systemd/system/secubox-crowdsec.service.d/allow-sudo.conf
    • Added sudoers entry for cscli: /etc/sudoers.d/secubox-crowdsec
    • Rewrote status.py to use cscli lapi status subprocess instead of HTTP
  4. CrowdSec Collections Status Fix — Parsing issue

    • Issue: Collections showing 0 when 7 installed
    • Root cause: Code checked status == "enabled" but CrowdSec uses status = "enabled,update-available"
    • Fix: Changed to "enabled" in (item.get("status") or "")
  5. CrowdSec Bouncers API Fix — LAPI auth issue

    • Issue: HTTP calls to LAPI failed (missing X-Api-Key)
    • Fix: Rewrote bouncers.py to use cscli bouncers list -o json subprocess
  6. CrowdSec Hub Functions — Added missing functions

    • Added refreshHub() and reloadEngine() JavaScript functions
    • Added /hub/update and /service/reload API endpoints
  7. Duplicate Remote UI Entry Removed — Menu cleanup

    • Removed duplicate remote-ui menu entry from secubox-system
    • Eye Remote (eye-remote) remains functional at /eye-remote/
    • Files removed: packages/secubox-system/menu.d/15-remote-ui.json
    • Files removed: packages/secubox-system/www/remote-ui/index.html

CrowdSec Files Modified:

  • packages/secubox-crowdsec/api/routers/status.py — cscli subprocess with shell=True
  • packages/secubox-crowdsec/api/routers/bouncers.py — cscli subprocess for bouncers
  • packages/secubox-crowdsec/api/main.py — Added hub update and reload endpoints
  • packages/secubox-crowdsec/www/index.html — Removed Migration, Components, Access tabs
  • packages/secubox-crowdsec/systemd/allow-sudo.conf — NoNewPrivileges=false override
  • packages/secubox-crowdsec/sudoers.d/secubox-crowdsec — cscli sudo permission

Session 96 — Eye Remote Auto-Pairing & Pi Zero Builder

Goal: Add auto-pairing and metrics API to Eye Remote

Completed:

  1. Auto-Pair Endpoint — Added /api/v1/eye-remote/auto-pair POST

    • Creates pairing record for currently connected device
    • Gets hostname from Pi Zero via metrics API
    • Stores devices in /var/lib/secubox/eye-remote/auto-paired.json
  2. Paired Devices Endpoint — Added /api/v1/eye-remote/paired-devices GET

    • Lists all paired Eye Remote devices
    • Masks tokens for security (shows only first 8 chars)
  3. Pi Zero Metrics API in Builder — Updated install_zerow.sh v1.9.0

    • Integrated pizero-metrics-api.py into SD card builder
    • Added pizero-metrics.service systemd unit
    • New Pi Zero SD cards now auto-include metrics API
  4. PiZero Metrics Public Endpoint — Added /api/v1/eye-remote/pizero/metrics

    • Relays metrics from Pi Zero without requiring auth
    • Dashboard can display CPU, Mem, Temp without complex auth setup
  5. Fixed _eye_state Missing — MOCHAbin hotfix

    • Added _eye_state = {"connected": False, "last_seen": None} to deployed main.py

Files Modified:

  • packages/secubox-eye-remote/api/main.py — Added auto-pair, paired-devices, pizero/metrics endpoints
  • remote-ui/round/install_zerow.sh — v1.9.0, integrated pizero-metrics-api

Commits:

  • 30b8773 feat(eye-remote): Add auto-pair and paired-devices endpoints

Session 97 — Eye Remote Routing Fixes & Navbar Emoji Cleanup

Goal: Fix Eye Remote metrics not reaching MOCHAbin, fix navbar emoji icons

Completed:

  1. rp_filter Martian Source Fix — Packets from Pi Zero (10.55.0.2) were being dropped

    • Root cause: Kernel reverse path filter rejecting packets on USB gadget interface
    • Fix: Added /etc/sysctl.d/99-secubox-usb.conf with net.ipv4.conf.all.rp_filter = 0
    • Also apply per-interface in udev rules: sysctl -w net.ipv4.conf.%k.rp_filter=0
  2. Dual Interface Routing Conflict — Pi Zero had both usb0 and usb1 with same IP

    • Root cause: Pi Zero gadget created RNDIS (usb0) + CDC-ECM (usb1), both configured
    • Fix: Added usb1-disable config to install_zerow.sh to bring down usb1
  3. USB Re-plug Detection — udev rules weren't triggering on reconnect

    • Fix: Added ACTION=="bind" event to udev rules for re-plug detection
    • Added sleep 2 delay for gadget initialization
  4. Network Script Status Command — Added secubox-eye-network.sh status

    • Shows interface state, rp_filter status, and peer reachability
  5. Navbar Emoji Icons — Replaced text-based icons with proper emoji

    • Updated CATEGORY_META in hub API with missing categories
    • Fixed 7 menu.d JSON files with text icons (catalog, shield, camera, etc.)

Files Modified:

  • packages/secubox-eye-remote/sysctl.d/99-secubox-usb.conf — rp_filter disable
  • packages/secubox-eye-remote/udev/90-secubox-eye-remote.rules — bind event, rp_filter
  • packages/secubox-eye-remote/scripts/secubox-eye-network.sh — status command
  • packages/secubox-eye-remote/debian/install — Added sysctl.d to package
  • remote-ui/round/install_zerow.sh — usb1-disable config
  • packages/secubox-hub/api/main.py — CATEGORY_META additions
  • packages/secubox-*/menu.d/*.json — 7 files with emoji icon fixes

Commits:

  • 01f2bf1 fix(eye-remote): Resolve rp_filter and dual-interface routing issues
  • a47d290 fix(menu): Replace text icons with emoji in navbar

2026-05-04

Session 93 — MOCHAbin Full Image Build

Goal: Build MOCHAbin image with slipstream packages (full profile like ESPRESSObin)

Problem:

  • Previous build attempts failed with "Erreur: La localisation 5890MiB est en dehors du périphérique"
  • Root cause: board/mochabin/config.mk had IMG_SIZE="4G" which was insufficient for ~5.5GB rootfs

Fix Applied:

# board/mochabin/config.mk
# Before:
IMG_SIZE="4G"

# After:
IMG_SIZE="8G"

Build Results:

  • Image: output/secubox-mochabin-bookworm.img.gz (1.2G compressed, 8G uncompressed)
  • SHA256: f1db869b5e82c2d851fa16d38faad4db91f4e76982da8d013c3cceef36b7164c
  • Slipstream packages: All SecuBox .deb packages pre-installed

Known Issues (Non-blocking):

  • 4 packages failed during slipstream (missing systemd service files):
    • secubox-mitmproxy
    • secubox-smtp-relay
    • secubox-soc-agent
    • secubox-soc-gateway

Commits:

  • de2f365 fix(mochabin): Increase image size to 8G for full install

Deployment:

  • Flashed to USB thumb drive (28.8G DataTraveler 3.0)
  • Ready for boot testing on MOCHAbin hardware

Session 93b — MOCHAbin eMMC Flash & Boot Success

Goal: Flash SecuBox image to eMMC and boot from it

USB Boot Issues:

  • USB storage not detected in Linux (f2500000.usb deferred probe)
  • USB thumb drive only accessible from U-Boot, not from running Linux

eMMC Flash from U-Boot:

usb reset
ext4load usb 0:3 0x10000000 secubox-mochabin-bookworm.img.gz
gzwrite mmc 0 0x10000000 ${filesize}

Boot Script Issue:

  • Initial boot.scr used uInitrd (U-Boot wrapped initrd)
  • Image only contains raw initrd.img from Debian
  • Error: "Wrong Ramdisk Image Format"

Solution - Use raw initrd with filesize:

setenv bootcmd_emmc 'fatload mmc 0:1 0x7000000 Image; fatload mmc 0:1 0x6f00000 dtbs/marvell/armada-7040-mochabin.dtb; fatload mmc 0:1 0x9000000 initrd.img; setenv bootargs root=/dev/mmcblk0p2 rootfstype=ext4 rootwait console=ttyS0,115200 earlycon=uart8250,mmio32,0xf0512000 net.ifnames=0; booti 0x7000000 0x9000000:${filesize} 0x6f00000'
setenv bootcmd 'run bootcmd_emmc'
saveenv

Key insight: U-Boot can boot raw initrd.img by passing filesize after colon: 0x9000000:${filesize}

Boot Success:

  • Kernel: 6.1.0-42-arm64
  • Memory: 8GB detected (7.8Gi available)
  • eMMC: 14.7 GiB DF4016
  • Network: br-lan @ 192.168.1.1, eth0 @ 10.55.255.177
  • Dashboard: https://192.168.1.1:9443
  • All SecuBox services started

Minor Issues (Non-blocking):

  • crowdsec.service failed to start (needs investigation)
  • lxc-net.service failed (bridge setup conflict)
  • secubox-metablogiz keeps restarting (service loop)

Hardware Notes:

  • SFP module: OEM SFP28-25G-SR-S detected on eth0 (incompatible mode)
  • SATA: 1TB WD Blue SA510 detected on ata2 (user's personal drive)
  • USB: Quectel EP06-E LTE modem on USB1

Session 93c — Nginx .dpkg-new Config Fix

Problem:

  • Dashboard /system/ returning HTML instead of JSON
  • secubox-system.service was disabled/not running
  • Nginx configs in /etc/nginx/secubox.d/ had .dpkg-new suffix (not activated)

Root Cause:

  • dpkg leaves .dpkg-new files when installing new conffiles over existing ones
  • Build scripts didn't rename these after package installation

Fix Applied: Added .dpkg-new activation step to all build scripts:

  • image/build-image.sh (line ~760)
  • image/build-live-usb.sh (line ~1824)
  • image/build-rpi-usb.sh (line ~751)
# Activate .dpkg-new configs
for newconf in "${ROOTFS}/etc/nginx/secubox.d/"*.dpkg-new; do
  [[ -f "$newconf" ]] || continue
  mv "$newconf" "${newconf%.dpkg-new}"
done

Services Fixed on Running System:

systemctl enable --now secubox-system
cd /etc/nginx/secubox.d && for f in *.dpkg-new; do mv "$f" "${f%.dpkg-new}"; done
nginx -s reload

Session 93d — Performance Comparison: OpenWrt vs Debian

Test Environment:

  • 192.168.255.1 — SecuBox OpenWrt 24.10.5 (MOCHAbin 8GB)
  • 192.168.255.10 — SecuBox Debian Bookworm (MOCHAbin 8GB)

System Comparison

Metric OpenWrt Debian Notes
Kernel 6.6.119 6.1.0-42-arm64 OpenWrt newer
Uptime 2 days 22h 18 hours
Load Average 6.63 1.31 Debian 5x lower
Total Processes 1768 172 Debian 10x fewer
Memory Total 8GB 8GB Same
Memory Used 3.1GB (38%) 1.7GB (22%) Debian 45% less
Memory Available 4.8GB 6.2GB Debian +1.4GB
Swap Used 1.6GB 0 Debian no swap needed
Disk Used 10.6G/14.6G (73%) 3.5G/5.4G (69%) Similar %

Service Memory Usage (kB)

Service OpenWrt Debian Δ
nginx 2,184 12,036 +5.5x
haproxy 36,888 47,080 +28%
crowdsec 105,840 194,732 +84%
dnsmasq 2,404 2,000 -17%
netdata N/A 4,520
Python APIs N/A 1,250,656

Version Comparison

Component OpenWrt Debian
Python 3.11.14 3.11.2
OpenSSL 3.0.18 3.0.18
HAProxy 3.0.12 2.6.12
CrowdSec 1.7.6 1.7.7

Analysis

Debian Advantages:

  • 5x lower system load (1.3 vs 6.6) — more responsive
  • 45% less memory used — more headroom for services
  • No swap thrashing — better performance under load
  • 10x fewer processes — cleaner process tree
  • systemd — modern service management, dependencies
  • Standard Debian packages — easier updates, security patches

OpenWrt Advantages:

  • Newer kernel (6.6 vs 6.1) — more hardware support
  • Lighter nginx (2MB vs 12MB) — minimal footprint
  • Smaller crowdsec (105MB vs 194MB) — optimized binary
  • HAProxy 3.0 vs 2.6 — newer features

Debian Trade-offs:

  • Python FastAPI services use ~1.2GB total for 22 SecuBox APIs
  • This replaces shell-based RPCD (unmeasured but lighter)
  • Benefit: proper async, JWT auth, OpenAPI docs

Conclusion: Debian migration successful. Despite heavier individual services, overall system load and memory pressure significantly lower. The structured systemd architecture provides better resource management than OpenWrt's init scripts.

Session 93e — CrowdSec Firewall Bouncer Setup

Problem:

  • CrowdSec agent running but no firewall bouncer installed
  • CAPI blocklist (16k+ IPs) not enforced at firewall level

Fix:

# Clear stuck apt locks
pkill -9 apt dpkg
rm -f /var/lib/dpkg/lock-frontend

# Install bouncer
apt-get install -y crowdsec-firewall-bouncer-nftables

Result:

  • Bouncer auto-registered with CrowdSec API
  • nftables tables created: ip crowdsec, ip6 crowdsec6
  • Services enabled on boot

Verification:

cscli bouncers list
# cs-firewall-bouncer-1777957909  127.0.0.1  ✔️  v0.0.34

systemctl is-active crowdsec crowdsec-firewall-bouncer
# active
# active

Protection Active:

Category Decisions
http:dos 9,128
http:exploit 3,090
http:bruteforce 1,369
ssh:bruteforce 1,115
http:scan 890
generic:scan 657
ssh:exploit 353

Session 93f — Fix Service Restart Loops

Problem: Multiple SecuBox services in restart loops due to missing Python dependencies.

Root Cause:

  • python-multipart missing (required for FastAPI file uploads)
  • email-validator missing (required for Pydantic email fields)

Affected Services:

  • secubox-metablogizer, secubox-droplet, secubox-avatar, secubox-streamlit, secubox-users

Fix on Running System:

pip3 install --break-system-packages python-multipart email-validator
systemctl restart secubox-metablogizer secubox-avatar

Build Scripts Updated:

  • image/build-image.sh — added python-multipart, email-validator
  • image/build-rpi-usb.sh — added email-validator

Disabled Non-Critical Services (missing dependencies):

  • secubox-picobrew (IoT controller, needs hardware)
  • secubox-threats (needs Suricata)
  • secubox-eye-remote (import error)
  • secubox-openclaw (OSINT tool)
  • secubox-ui-manager (display manager)
  • secubox-net-fallback (network already configured)

Result:

  • 86 services running
  • 0 failed
  • Load: 7.7 → 3.7 (no more restart loops)

Session 93g — Dashboard System API Fix

Problem:

Root Cause:

  • secubox-system.service was running but socket /run/secubox/system.sock was missing
  • Service started at 05:03 but socket disappeared (possibly cleaned by systemd-tmpfiles)

Fix:

systemctl restart secubox-system

Verification:

curl -s https://localhost/api/v1/system/info
# {"hostname":"secubox-mochabin","board":"Globalscale MOCHAbin","arch":"aarch64"...}

Dashboard Status:

  • https://192.168.255.10/system/ working
  • System info, resources, services endpoints functional
  • JWT authentication enforced on protected endpoints

Session 93h — CrowdSec Dashboard & Socket Stability

Problem:

Root Cause:

  • Services running but Unix sockets not created
  • Bulk systemctl restart 'secubox-*' causes race conditions
  • Services need time to initialize and create sockets

Fix:

# Restart specific services individually
systemctl restart secubox-system secubox-crowdsec
sleep 5

Verified Working:

✅ /api/v1/hub/status      → JWT required (correct)
✅ /api/v1/system/info     → {"hostname":"secubox-mochabin"...}
✅ /api/v1/crowdsec/status → {"running":true,"version":"v1.7.7"...}

Note: Hub service uses TCP port 8001 (not socket) for VM compatibility.


2026-05-05

Session 94 — Socket RuntimeDirectory Fix & Dashboard Stability

Problem:

  • Multiple dashboard pages returning JSON parse errors
  • Services "active" but sockets missing in /run/secubox/
  • RuntimeDirectory causing socket deletion when services restart

Root Cause: All SecuBox services shared RuntimeDirectory=secubox which caused:

  1. Each service restart recreated /run/secubox/ with only its own socket
  2. Other service sockets were deleted
  3. Race conditions during bulk restarts

Fix Applied:

  1. Created tmpfiles.d config for persistent directory:
cat > /etc/tmpfiles.d/secubox.conf << 'CONF'
d /run/secubox 0775 secubox secubox -
CONF
  1. Disabled RuntimeDirectory in services:
for svc in auth system users crowdsec wireguard dpi dns vhost cdn qos waf nac netmodes admin hub; do
  mkdir -p /etc/systemd/system/secubox-$svc.service.d
  cat > /etc/systemd/system/secubox-$svc.service.d/runtime.conf << 'CONF'
[Service]
RuntimeDirectory=
RuntimeDirectoryPreserve=
CONF
done
systemctl daemon-reload
  1. Fixed nginx users.conf routing:
location /api/v1/users/ {
    rewrite ^/api/v1/users/(.*)$ /$1 break;
    proxy_pass http://unix:/run/secubox/users.sock;
    include /etc/nginx/snippets/secubox-proxy.conf;
}
  1. Installed udev rules for Eye Remote:
cat > /etc/udev/rules.d/90-secubox-otg.rules << 'RULES'
SUBSYSTEM=="net", ATTRS{idVendor}=="1d6b", ATTRS{idProduct}=="0104", DRIVERS=="cdc_ether", NAME="secubox-round"
SUBSYSTEM=="tty", ATTRS{idVendor}=="1d6b", ATTRS{idProduct}=="0104", KERNEL=="ttyACM*", SYMLINK+="secubox-console"
RULES

Result:

  • All 14+ sockets persisting across service restarts
  • Dashboard pages working: system, crowdsec, users, wireguard, dns, dpi, waf
  • CrowdSec console enrolled and active

Sockets Created:

/run/secubox/admin.sock
/run/secubox/auth.sock
/run/secubox/cdn.sock
/run/secubox/crowdsec.sock
/run/secubox/dns.sock
/run/secubox/dpi.sock
/run/secubox/nac.sock
/run/secubox/netmodes.sock
/run/secubox/qos.sock
/run/secubox/system.sock
/run/secubox/users.sock
/run/secubox/vhost.sock
/run/secubox/waf.sock
/run/secubox/wireguard.sock

USB Configuration:

  • USB1 (480 Mbps): Quectel EP06-E LTE modem
  • USB2 (5000 Mbps): Available for Eye Remote Pi Zero W
  • Eye Remote detection pending (needs to be plugged into USB3 port)

2026-05-03

Session 92 — Tow-Boot eMMC Support & MOCHAbin Documentation

Goal: Add eMMC boot partition support to Tow-Boot for MOCHAbin, document boot mode jumpers

Context:

  • MOCHAbin board with dead/intermittent SPI NOR flash (JEDEC 00,00,00)
  • eMMC works in U-Boot but BootROM communication fails
  • Original Tow-Boot build lacks mmc partconf command
  • No microSD slot on MOCHAbin (correction to documentation)

Implementation:

  1. Copied Tow-Boot to Project:

    • Source: /home/reepost/DEVEL/MOKATOOL/Tow-Boot/
    • Destination: tools/Tow-Boot/
  2. Enabled eMMC Boot Support:

    • Added mmcBootIndex = "0" to MOCHAbin board configs
    • Enables CONFIG_SUPPORT_EMMC_BOOT=y in U-Boot
    • New commands available: mmc partconf, mmc bootbus
  3. Built New Tow-Boot:

    sg nix-users -c "nix-build -A globalscale-mochabin-8gb"
    
    • Output: Tow-Boot.spi.bin, Tow-Boot.mmcboot.bin, Tow-Boot.noenv.bin
  4. Hardware Testing (Failed):

    • SPI flash intermittent (sometimes detected, mostly JEDEC 00,00,00)
    • eMMC boot partition: BootROM returns Error interrupt: 00018000
    • Tried boot partitions 1, 2, and user area — all fail at BootROM level
    • Verdict: Hardware defective — board abandoned

Files Modified:

  • tools/Tow-Boot/boards/globalscale-mochabin-2gb/default.nix
  • tools/Tow-Boot/boards/globalscale-mochabin-4gb/default.nix
  • tools/Tow-Boot/boards/globalscale-mochabin-8gb/default.nix

Files Created:

  • tools/Tow-Boot/output/ — Built binaries
  • tools/Tow-Boot/SECUBOX.md — SecuBox-specific documentation

Documentation Updated:

  • board/mochabin/README.md:
    • Added boot mode jumper table (J17-J22)
    • Added SPI → eMMC jumper change instructions
    • Documented Tow-Boot flashing procedures
    • Added known hardware issues section
    • Removed incorrect microSD slot reference

Boot Mode Jumpers (J17-J22):

Mode Code J17 J18 J19 J20 J21 J22
SPI 0x32 L R L L R R
eMMC 0x2B R R L R L R

Result:

  • Tow-Boot with eMMC support ready for working boards
  • Complete MOCHAbin boot documentation
  • Defective board identified and abandoned

Session 91 — Wiki Badges & VirtualBox VM Rebuild

Goal: Update wiki and README with build status badges, metrics dashboard, and rebuild VBox VM


Session 90 — Mitmproxy WAF Module Migration

Goal: Complete migration of mitmproxy WAF module from SecuBox-OpenWrt to SecuBox-DEB

Context:

  • Original OpenWrt module: luci-app-mitmproxy-waf (shell scripts + LuCI frontend)
  • Target: Full Debian package with FastAPI backend, LXC container isolation, HAProxy integration

Implementation (15 tasks via Subagent-Driven Development):

  1. Package Scaffold — debian/control, rules, postinst, prerm, systemd service
  2. Configuration — mitmproxy.toml (TOML), waf-rules.json (90+ patterns, 14 categories)
  3. mitmproxyctl CLI — Python CLI for LXC lifecycle (install, start, stop, restart, status, destroy, logs)
  4. Threat Detection Addon — secubox_waf.py mitmproxy addon with real-time threat detection
  5. FastAPI Backend — 5 routers with JWT auth:
    • status.py — Container control, stats, mode settings
    • settings.py — TOML configuration CRUD
    • alerts.py — Threat log, ban management, CrowdSec integration
    • haproxy.py — WAF enable/disable, route sync
    • waf.py — Rule category management
  6. WebUI — status.html, settings.html, filters.html (CRT-light theme)
  7. Integration — nginx config, CrowdSec acquisition config, menu.d entry

WAF Detection Categories (14):

  • SQL Injection, XSS, Command Injection, Path Traversal
  • SSRF, XXE, LDAP Injection, Log4Shell
  • Scanner Detection, Path Scanning, CVE Exploits, RCE
  • VoIP Attacks, XMPP Attacks

Files Created:

  • packages/secubox-mitmproxy/ — Complete package structure
  • debian/ — control, rules, postinst, prerm, service, mitmproxy.toml
  • api/ — main.py, routers/{status,settings,alerts,haproxy,waf}.py
  • addons/secubox_waf.py — Mitmproxy addon
  • bin/mitmproxyctl — CLI tool
  • data/waf-rules.json — 90+ detection patterns
  • www/mitmproxy/ — WebUI pages
  • nginx/mitmproxy.conf — API/static proxy
  • README.md — Comprehensive documentation

Code Review Findings (Fixed):

  1. LXC architecture hardcoded to amd64 → Now detects actual arch (arm64/amd64)
  2. Missing WebUI API endpoints → Added /set_mode, /save_settings, /setup_firewall, /clear_firewall, /wan_setup, /wan_clear, /clear_alerts
  3. Missing crowdsec dir in debian/rules → Added

Commits:

  • 17 commits from package scaffold through final fixes
  • d69dd43 fix(mitmproxy): Address code review findings
  • 87602fc fix(mitmproxy): Add crowdsec directory to debian/rules install

Result:

  • Complete secubox-mitmproxy package ready for dpkg-buildpackage
  • LXC-isolated WAF with 90+ threat detection patterns
  • Full CrowdSec integration for auto-banning
  • HAProxy route sync for traffic inspection
  • WebUI dashboard with real-time alerts

Session 91 — Wiki Badges & VirtualBox VM Rebuild

Goal: Update wiki and README with build status badges, metrics dashboard, and rebuild VBox VM

Context:

  • Session 90 completed mitmproxy WAF migration
  • ESPRESSObin has insufficient disk space for LXC container
  • Need VirtualBox VM for testing mitmproxy installation
  • Wiki and README need updated badges/metrics

Completed:

  1. VirtualBox VM Rebuild:

    • Built new x64-bookworm image with 8GB disk
    • Generated VDI (2.8GB) and compressed img.gz (963MB)
    • Fixed VM UUID mismatch after VDI recreation
    • VM now running with 4GB RAM, EFI boot
  2. Wiki Home.md Update:

    • Added workflow status badges (packages, live USB, installer, eye remote, multiboot)
    • Added development metrics table (131 packages, 94% migration, 2000+ APIs)
    • Added module status by category with progress indicators
    • Updated version announcement to v2.3.0
  3. README.md Update:

    • Added comprehensive workflow badges
    • Added metrics table (packages, migration %, APIs, architectures)
    • Updated version to v2.3.0
  4. Dependency Fix:

    • Added xz-utils to secubox-mitmproxy dependencies for LXC template extraction

Commits:

  • 22487f8 docs: Add build status badges and metrics to README
  • e041caa docs: Add build badges and metrics dashboard to wiki Home

Artifacts:

  • output/secubox-vm-x64-bookworm.img.gz (963MB)
  • output/secubox-vm-x64-bookworm.vdi (2.8GB)
  • SHA256: 13e69ae55ab185daaf6e9b04ff1fad69bc40cf53c5ae8daac9829334226deca6

Result:

  • Wiki now shows live build status for all components
  • VirtualBox VM ready for mitmproxy testing
  • GitHub Actions handles artifact creation on releases

Session 89 — Emancipate SecuBox-Dev Methodology

Goal: Extract and document the SecuBox development methodology as a standalone, reusable guide

Context:

  • Compared .claude/ tracking files between secubox-openwrt (15 files) and secubox-deb (18 files)
  • Identified core methodology patterns across 88+ development sessions
  • Methodology needs to be portable for other projects

Key Differences Found (OpenWrt vs Debian):

Aspect OpenWrt Debian
Focus Future features, themes, AI layers Migration completion, CSPN compliance
Tracking Version milestones (v0.19→v1.0) Session-based (S01→S88), phases
Unique files ROADMAP, EVOLUTION-PLAN, THEME_CONTEXT MIGRATION-MAP, PATTERNS, MODULE-COMPLIANCE

Methodology Document Created:

  • Part 1: Project tracking structure (WIP, TODO, HISTORY, PATTERNS)
  • Part 2: Session-based development workflow
  • Part 3: Migration patterns (Shell/UCI → FastAPI/TOML)
  • Part 4: Performance patterns for embedded systems
  • Part 5: Compliance verification checklists
  • Part 6: Quick reference
  • Part 7: How to apply to new projects
  • Appendix: Templates

Files Created:

  • docs/SECUBOX-DEV-METHODOLOGY.md — 762 lines standalone methodology guide

Commits:

  • 082ebe0 docs(methodology): Emancipate SecuBox-Dev methodology as standalone guide

Result:

  • Methodology portable and documented
  • Can be applied to any embedded/systems project with AI coding assistants

2026-05-02

Session 88 — Navbar Module Filtering Fix

Goal: Fix navbar showing modules that aren't installed, causing 403/404/500 errors

Problems Identified:

  1. Portal returning 403 (no index.html, only login.html)
  2. Modules without www directories showing in navbar
  3. Deploy script copying to wrong directory (secubox-hub vs hub symlink issue)
  4. Menu items without id field still appearing

Root Causes:

  • /portal/ had only login.html, no index.html for nginx to serve
  • _check_module_installed() was checking for service sockets/systemd, not actual www content
  • /usr/lib/secubox/hub (uvicorn workdir) was separate from /usr/lib/secubox/secubox-hub (deploy target)
  • Menu definitions (menu.d/*.json) included items without id field

Fixes:

  1. Created portal/index.html redirect to login.html
  2. Rewrote _check_module_installed() to only return True for modules with:
    • www directory at /usr/share/secubox/www/{module_id}
    • At least one HTML file in that directory
  3. Added filter to skip menu items without valid id or with console_only: true
  4. Created symlink: /usr/lib/secubox/hub -> /usr/lib/secubox/secubox-hub

Files Changed:

  • packages/secubox-hub/api/main.py — Updated _check_module_installed() and _compute_menu_sync()
  • packages/secubox-hub/www/portal/index.html — New redirect file

Commits:

  • 96b51ef fix(hub): Filter navbar to only show modules with www directories

Result:

  • Navbar shows only 8 modules with actual www content (was 29)
  • All menu items return HTTP 200
  • No more 403/404/500 errors from navbar links

Session 87 — HAProxy WebUI CRUD Enhancement

Goal: Add full CRUD operations for VHosts, Backends, Servers, and Certificates to the HAProxy WebUI dashboard

Context:

  • Compared OpenWrt SecuBox HAProxy implementation with secubox-deb
  • OpenWrt had 35+ RPCD methods, 8 separate JS view files
  • secubox-deb already had comprehensive FastAPI backend (40+ endpoints)
  • WebUI was read-only — needed CRUD operations

Implementation:

  1. Added modal system for add/edit forms
  2. Added toast notifications for success/error feedback
  3. Added client-side form validation with validateForm() function
  4. Added enhanced apiCall() function with comprehensive error handling
  5. VHost CRUD: add, edit, delete with domain/backend/SSL/WAF/ACME options
  6. Backend CRUD: add, edit, delete with mode/balance/health check options
  7. Server CRUD: nested under backends with address/port/weight management
  8. Certificate CRUD: request ACME certs with progress bar, delete existing
  9. Updated all tables with action buttons (Edit/Delete/Manage)
  10. Maintained P31 Phosphor theme consistency

Files Changed:

  • packages/secubox-haproxy/www/haproxy/index.html — ~1565 lines (was ~690)
  • docs/superpowers/specs/2026-05-02-haproxy-webui-enhancement-design.md — Design spec
  • docs/superpowers/plans/2026-05-02-haproxy-webui-crud.md — Implementation plan

Commits (10 total):

  • f0970d6 feat(haproxy-ui): Add modal and toast HTML containers with CSS and JS functions
  • 251b292 feat(haproxy-ui): Add validation and enhanced API functions
  • ebb1bb4 feat(haproxy-ui): Add VHost CRUD functions
  • d51e26e feat(haproxy-ui): Add Backend CRUD functions
  • 591d289 feat(haproxy-ui): Add Server CRUD functions
  • 4b2b214 feat(haproxy-ui): Add Certificate CRUD functions
  • 3df7247 feat(haproxy-ui): Update VHosts table with CRUD buttons
  • db04ffb feat(haproxy-ui): Update Backends table with CRUD buttons
  • acdb94b feat(haproxy-ui): Update Certificates table with CRUD buttons
  • 2a1dde2 fix(haproxy-ui): Add form CSS and remove unused function

Result:

  • Full CRUD operations for all HAProxy entities
  • Consistent UI with existing P31 Phosphor theme
  • All API endpoints already existed — frontend-only enhancement
  • Code review passed with Good quality rating

Session 86 — GitHub Actions Package Architecture Filtering Fix

Goal: Fix GitHub Actions workflow failures when building ARM64 images

Problem:

  • GitHub Actions build-image.yml workflow failing on arm64 boards (mochabin, espressobin-v7, espressobin-ultra, rpi400)
  • Error: package architecture (amd64) does not match system (arm64) for secubox-c3box and secubox-daemon
  • Slipstream code copied ALL .deb packages without architecture filtering
  • When building arm64 images, amd64 packages were being copied and dpkg failed to install them

Root Cause:

  • In image/build-image.sh line 675: cp "${DEBS_DIR}"/secubox-*.deb copied all packages regardless of architecture
  • Same issue in image/build-ebin-live-usb.sh and image/build-live-usb.sh
  • build-rpi-usb.sh already had correct filtering (only copied _all.deb and _arm64.deb)

Fix Applied:

  1. image/build-image.sh — Added architecture filtering in slipstream section:

    • Replaced blind cp with a loop that filters by DEBIAN_ARCH
    • Only copies *_all.deb and *_${DEBIAN_ARCH}.deb packages
    • Logs skipped packages count for debugging
  2. image/build-ebin-live-usb.sh — Added arm64 architecture filter:

    • Filter for *_all.deb and *_arm64.deb only
    • Updated cache search to also filter by architecture
  3. image/build-live-usb.sh — Added amd64 architecture filter:

    • Filter for *_all.deb and *_amd64.deb only
    • Updated cache search to also filter by architecture

Result:

  • ARM64 image builds will now skip amd64-only packages (secubox-daemon, secubox-c3box)
  • AMD64 image builds will skip arm64-only packages
  • Architecture-independent packages (_all.deb) are correctly installed on all platforms

2026-05-01

Session 85 — VirtualBox VM Network Detection Fix

Goal: Fix network configuration for VirtualBox VMs with multiple interfaces (NAT + host-only)

Problem:

  • VBox VMs with 2 interfaces (NAT + host-only) had host-only interface put into br-lan bridge
  • br-lan bridge got static IP 192.168.1.1/24 instead of DHCP from VBox host-only network
  • This broke host-only network access (should get IP in 192.168.56.x range)

Root Cause:

  • secubox-net-detect treated x64-vm and x64-baremetal identically
  • Both went through router mode logic that creates bridges for multi-interface setups
  • VMs don't need bridges - each interface should independently get DHCP

Fix Applied:

  1. image/sbin/secubox-net-detect — Separate VM handling:
    • New x64-vm) case in get_interface_config() with profile="vm"
    • VMs: first interface as WAN, empty LAN list (no bridge)
    • Added VM detection in generate_netplan(): when board="x64-vm", configure ALL physical interfaces with DHCP
    • In main(): profile="vm" forces mode="single" (skip bridge creation)

Result:

  • VBox VMs now correctly configure all interfaces with DHCP
  • Host-only interface gets IP from VBox DHCP server (192.168.56.x range)
  • NAT interface gets IP from VM's internal NAT (10.0.2.x range)
  • No br-lan bridge created for VMs

Testing:

  • Built new image with fix
  • VM visible at 192.168.56.110 on host-only network (DHCP working)
  • SSH service startup issue separate from network fix

Session 84 — AMD64 Real Hardware Network Fix

Goal: Fix network configuration for real AMD64 hardware (x64-live board)

Problem:

  • x64-live netplan used broken wildcard syntax (wan0: with match: name: "e*")
  • Missing set-name: directive caused netplan to not properly configure interfaces
  • On real hardware with multiple interfaces (enp2s0, enp3s0, eno1), this caused IP assignment failures

Fixes Applied:

  1. board/x64-live/netplan/00-secubox.yaml — Complete rewrite:

    • Changed from broken wan0: wildcard to proper eth-dhcp: and eth-legacy: match patterns
    • Both patterns get DHCP with different route metrics (100 vs 200) for determinism
    • Added optional: true to prevent boot blocking
    • Added documentation explaining secubox-net-detect role
  2. image/sbin/secubox-net-detect — Enhanced interface detection:

    • Added logging for interface discovery process
    • Expanded naming pattern matching for real hardware:
      • enp[0-9]*s0 patterns (PCI bus addressing)
      • eno[0-9]* patterns (onboard NICs)
      • ens* patterns (VMware ESXi)
    • Added fallback logic when no link detected
    • Improved YAML generation with DHCP overrides
    • Fixed empty LAN interface handling
  3. image/sbin/secubox-net-reset — New utility script:

    • --status to show current network detection state
    • --apply to re-detect and apply immediately
    • --reboot to clear marker and reboot (default)
    • Helpful for debugging network issues

Files Modified:

  • board/x64-live/netplan/00-secubox.yaml
  • image/sbin/secubox-net-detect
  • image/build-live-usb.sh — Updated embedded netplan config
  • CLAUDE.md — Added Debian shell scripting guidelines and mitmproxy docs

Files Created:

  • image/sbin/secubox-net-reset

Testing:

# On real AMD64 hardware:
secubox-net-reset --status    # Check current state
secubox-net-reset --apply     # Force re-detection
journalctl -u secubox-net-detect -f  # Watch detection logs

2026-04-30

Session 83 — Module Enhancement & Service Fixes

Goal: Complete stub/mockup implementations and fix service startup issues

Critical Security Fixes:

  • secubox-voip: Implemented PBKDF2-SHA256 password hashing (100k iterations)
    • Fixed plaintext password storage in extension/trunk creation
    • Added hash_password() and verify_password() functions

Core Functionality Completed:

  • secubox-dns-provider: Full OVH and Route53 adapter implementations
    • OVH: list_domains, list_records, create/update/delete, ACME challenges, zone export
    • Route53: Same full API coverage using boto3
  • secubox-ai-gateway: Auto-persist provider configuration
    • Added _persist_providers() helper function
    • Provider updates now automatically saved to disk
  • secubox-threat-analyst: WAF rule generation added
    • Complete JSON format rules for WAF module integration
    • Includes blocked IPs, patterns, and metadata
  • secubox-mirror: Added docker, npm, and pypi sync support
    • Docker: Registry v2 API verification
    • NPM: Registry ping endpoint check
    • PyPI: Simple API verification
  • secubox-eye-remote: Proper JWT auth import from secubox_core
    • Fallback for standalone Pi Zero deployment

System Module Performance:

  • secubox-system: 275ms → 40ms (6.8x faster) with batch systemctl calls

Service Startup Fixes:

  • Fixed uvicorn path in 31 systemd service files
  • Changed /usr/local/bin/uvicorn/usr/bin/python3 -m uvicorn
  • Services now start correctly on all Python installation types

Affected Packages: cloner, hexo, jabber, jellyfin, localai, lyrion, magicmirror, matrix, mesh, mmpm, netifyd, newsbin, ollama, ossec, p2p, peertube, photoprism, picobrew, redroid, rezapp, roadmap, simplex, soc-agent, soc-gateway, vault, vm, wazuh, webradio, zigbee, zkp

Files Modified:

  • packages/secubox-voip/api/main.py
  • packages/secubox-dns-provider/api/main.py
  • packages/secubox-ai-gateway/api/main.py
  • packages/secubox-threat-analyst/api/main.py
  • packages/secubox-mirror/api/main.py
  • packages/secubox-eye-remote/api/routers/devices.py
  • packages/secubox-eye-remote/api/routers/boot_media.py
  • packages/secubox-system/api/main.py
  • 31× packages/*/debian/*.service

Session 82 — API Performance Optimization Campaign

Feature: Applied double-buffer pre-cache pattern to all slow modules

Performance Results:

Module Before After Improvement
CrowdSec 1800ms 45ms 40x faster
HAProxy 353ms 50ms 7x faster
Users 1906ms 37ms 51x faster
Hub Menu ~2000ms 80ms 25x faster

Files Modified:

  • packages/secubox-crowdsec/api/routers/status.py — Complete rewrite with cache
  • packages/secubox-crowdsec/api/main.py — Added cache startup/shutdown
  • packages/secubox-haproxy/api/main.py — Added status cache + background refresh
  • packages/secubox-users/api/main.py — Added status cache + background refresh

Pattern Applied:

# Double-buffer pre-cache pattern
_cache: Dict = {}
CACHE_FILE = Path("/var/cache/secubox/module/status.json")

async def _refresh_cache():
    while True:
        data = await compute_in_threadpool()
        _cache.update(data)
        CACHE_FILE.write_text(json.dumps(data))
        await asyncio.sleep(30)

@app.get("/status")
async def status():
    return _cache or load_from_file() or compute_sync()

Target Profile: secubox-lite (ESPRESSObin 1GB) First home ISP secured solution with:

  • CrowdSec IDS/IPS
  • HAProxy reverse proxy
  • DNS filtering
  • Firewall (nftables)
  • All APIs responding in <50ms

Session 81 — Hub Menu Double-Buffer Pre-Cache

Feature: Implemented double-buffer pre-cache pattern for navbar menu

Problem: Navbar menu was slow (several seconds) due to synchronous systemctl calls for each module check.

Solution:

  • Added MENU_CACHE_FILE at /var/cache/secubox/menu.json for persistence
  • Added _menu_cache in-memory dict for instant responses
  • Added _refresh_menu_cache() background task (30s interval)
  • Added _compute_menu_sync() running in thread pool
  • Cache loaded from file on startup for fast navbar display

Performance:

  • Before: Several seconds per request (sequential systemctl calls)
  • After: ~80ms average response time

Files Modified:

  • packages/secubox-hub/api/main.py — Added cache infrastructure

Device: ESPRESSObin V7 (192.168.255.250)


Session 80 — Security Services Integration on ESPRESSObin

Feature: Integrated core security modules (CrowdSec, HAProxy, WAF, DNS) on ESPRESSObin

Services Deployed:

Service Port Status Dashboard
secubox-crowdsec 8010 Running /crowdsec/
secubox-haproxy 8011 Running /haproxy-dashboard/
secubox-waf 8012 Running /waf/
secubox-dns 8013 Running /dns/

Files Modified:

  • packages/secubox-dns/api/main.py — Fixed Pydantic v1 compatibility (field_validator → validator)

Systemd Overrides Created (ESPRESSObin):

  • /etc/systemd/system/secubox-crowdsec.service.d/override.conf — TCP port 8010
  • /etc/systemd/system/secubox-haproxy.service.d/override.conf — TCP port 8011
  • /etc/systemd/system/secubox-waf.service.d/override.conf — TCP port 8012
  • /etc/systemd/system/secubox-dns.service.d/override.conf — TCP port 8013

Nginx Configs Verified:

  • /etc/nginx/secubox.d/crowdsec.conf — API + static dashboard
  • /etc/nginx/secubox.d/haproxy.conf — API + static dashboard
  • /etc/nginx/secubox.d/waf.conf — API + static dashboard
  • /etc/nginx/secubox.d/dns.conf — API + static dashboard

API Endpoints Working:

  • CrowdSec: 75+ endpoints (decisions, alerts, bouncers, hub, console, migration)
  • HAProxy: 35+ endpoints (vhosts, backends, certs, stats, WAF toggle)
  • WAF: 15+ endpoints (rules, categories, bans, alerts, autoban)
  • DNS: 20+ endpoints (zones, records, stats, webhooks, export)

Dashboard Features (OpenWrt-inspired):

  • CrowdSec: Status monitoring, ban management, alerts, hub, bouncers, console enrollment
  • HAProxy: VHost management, backends, certificates, stats, WAF integration
  • WAF: Rule categories, auto-ban, alerts, IP banning
  • DNS: Zone management, records, validation, history, webhooks

Session 79 — Performance Benchmark Suite

Feature: Created comprehensive performance testing infrastructure for ARM64 optimization

Files Created:

  • scripts/bench/api-latency.py — API endpoint latency measurement (P50/P95/P99)
  • scripts/bench/memory-baseline.sh — Per-service memory tracking (RSS/PSS/USS)
  • scripts/bench/startup-time.sh — Service cold-start measurement via systemd
  • scripts/bench/cpu-profile.sh — Flame graph generation with py-spy
  • scripts/bench/locustfile.py — Load test scenarios for Locust framework
  • scripts/bench/README.md — Documentation for benchmark suite

Files Modified:

  • scripts/README.md — Added performance benchmarks section
  • remote-ui/round/agent/display/fallback/fallback_manager.py — Changed disk icon to floppy

Performance Targets Established:

Metric ESPRESSObin MOCHAbin
API P50 < 100ms < 50ms
API P99 < 500ms < 200ms
Service RSS < 50MB < 100MB
Cold start < 5s < 3s

MOCHAbin Analysis:

  • Identified critical state: Load 9.47, swap 99% exhausted
  • Gitea using 7.6GB (93% VSZ) — memory leak or misconfiguration
  • Created optimization plan in .claude/plans/shimmering-chasing-abelson.md

2026-04-29

Session 78 — Migration Tools v2.1.0 + Services Module

Feature: Extended migration with 19 modules covering all SecuBox services

Files Modified:

  • scripts/migration-export.sh — Added dns, databases, scripts, services modules (v2.1.0)
  • scripts/migration-import.sh — Added import functions for all new modules (v2.1.0)

New Migration Modules:

Module Export Import
dns BIND zones, Vortex RPZ, Unbound, AdGuard, Pi-hole BIND/Unbound configs, zones
databases SQLite, MySQL, PostgreSQL, Redis dumps DB restoration with permissions
scripts Custom scripts, systemd units, cron jobs, rc.local Scripts, systemd service creation
services All /srv/* directories (50+ services) Service restoration, Docker compose

Services Module Captures:

  • Streamlit instances (/srv/streamlit/*)
  • Metablogizer/Metabolizer apps
  • Gitea/Git repositories with full history
  • Docker compose configurations
  • LXC container configs
  • mitmproxy, config-vault, saas-relay

Enhanced HAProxy Export:

  • conf.d modular architecture
  • Certificate management
  • Lua scripts and maps
  • mitmproxy route integration

Total Modules: 19 (network, firewall, wireguard, crowdsec, dhcp, haproxy, nginx, certs, content, vhosts, users, state, git, media, mail, accounts, dns, databases, scripts, services)

Eye Remote Deployment:

  • Deployed agent to ESPRESSObin at /opt/eye-remote/
  • Fixed secubox-status to handle VLAN interfaces (wan@eth0)
  • Restored WAN connectivity after migration via /etc/netplan/10-wan.yaml

Session 77 — Migration Tools Extended (v2.0.0)

Feature: Extended migration to include Git, Media, Email, and User Accounts

Files Modified:

  • scripts/migration-export.sh — Added git, media, mail, accounts modules (v2.0.0)
  • scripts/migration-import.sh — Added import functions for new modules (v2.0.0)

New Migration Modules:

Module Export Import
git /srv/git, /var/lib/git, Gitea/Gogs/GitLab /srv/git, service configs
media /srv/media, PeerTube, Jellyfin, Nextcloud /srv/media, service restarts
mail Maildir, Postfix, Dovecot, DKIM Mail dirs, configs, crontabs
accounts Home dirs, passwd/shadow, sudo, cron User creation, home dirs

Export Test Results:

  • Git repositories: 4K
  • Media files: 8K
  • Email data: 4K
  • User accounts: 6 users, 96K
  • Total archive: 72K

Note: VBox VM SSH issue (banner timeout) prevented import test.


Session 76 — Migration Tools Validation on VirtualBox

Feature: Tested migration import on VirtualBox VM

Test Results:

  • Export: 66KB archive from SecuBox-OpenWrt (192.168.255.1)
  • Transform: UCI → Debian format (netplan, nftables, dnsmasq, vhost.toml)
  • Import: All modules successfully imported to VBox VM

Imported Configurations:

Config Destination Status
Network /etc/netplan/00-secubox.yaml Imported
Firewall /etc/nftables.conf Imported (78 rules)
DNS/DHCP /etc/dnsmasq.d/secubox.conf Imported
VHosts /etc/secubox/vhosts/vhost.toml Imported (4 services, 3 redirects)
Content /srv/www/ Imported (8KB)
Auth /etc/secubox/auth.toml Imported

Rollback Snapshot:

  • /var/lib/secubox/rollback/pre-migration-20260429-112849

Expected Warnings: Services not installed on test VM (CrowdSec, dnsmasq, HAProxy)


Session 75 — Eye Remote Recovery System + Design Charter Update

Feature: Board recovery via serial boot protocols + unified design charter

Files Created:

  • remote-ui/round/agent/recovery/protocols/mvebu64boot.py — 64-bit Marvell boot protocol
  • remote-ui/round/agent/recovery/protocols/xmodem.py — XMODEM-CRC file transfer (prior session)
  • remote-ui/round/agent/recovery/protocols/kwboot.py — Armada 3720 serial boot (prior session)
  • remote-ui/round/agent/recovery/recovery_controller.py — Main recovery controller (prior session)

Files Modified:

  • remote-ui/round/agent/recovery/protocols/__init__.py — Added Mvebu64Protocol export
  • remote-ui/round/agent/recovery/__init__.py — Added RecoveryMethod + Mvebu64Protocol
  • docs/design/graphic-charter.md — Updated to v2.0, synced with Eye Remote metrics
  • docs/hardware/smart-strip-v1.1.md — Updated to v1.2, synced with graphic charter

Recovery Protocols:

Protocol SoC Use Case
kwboot Armada 3720 ESPRESSObin serial boot
mvebu64boot Armada 7040/8040 MOCHAbin 64-bit serial boot
XMODEM-CRC All File transfer to BootROM

Design Charter Updates:

  • Module → Metric mapping table for Eye Remote dashboard
  • Alert thresholds unified across Eye Remote and Smart-Strip
  • RGB values for SK6812 LEDs documented
  • Pod layout diagram for round display
  • Transport badge colors (OTG=ROOT, WiFi=MESH, SIM=gray)

GitHub Issue #34: Confirmed fixed (closed with resolution comment)


Session 74 — Migration Data Saver v1.0.0

Feature: OpenWrt → SecuBox-DEB migration tools

Files Created:

  • scripts/migration-export.sh — SSH export from SecuBox-OpenWrt
  • scripts/migration-import.sh — Import to SecuBox-DEB with transformations
  • scripts/migration-transform.py — UCI parser and format converters

Files Modified:

  • scripts/README.md — Added migration documentation
  • .claude/WIP.md — Updated with session 74

Components:

  • UCIParser: Parse OpenWrt UCI config format
  • NetworkTransformer: UCI network → netplan YAML
  • FirewallTransformer: UCI firewall → nftables
  • DHCPTransformer: UCI dhcp → dnsmasq.conf

Supported Modules: network, firewall, wireguard, crowdsec, dhcp, haproxy, nginx, certs, content, vhosts, users, state

Security Features:

  • AES-256 archive encryption
  • SHA256 checksums
  • Pre-import rollback snapshots
  • Secrets separation

Session 73 — Eye Remote Interactive v1.9.0

Feature: Multi-mode USB gadget display system for Eye Remote

Files Modified:

  • remote-ui/round/fb_dashboard.py — Added mode detection, TTY terminal, flash progress, auth QR
  • packages/secubox-hub/debian/secubox-hub.service — Changed to TCP binding (port 8001)
  • packages/secubox-hub/nginx/hub.conf — Changed to TCP proxy
  • common/nginx/modules.d/hub.conf — Changed to TCP proxy

New Classes:

  • SerialTerminal — Read serial console output for TTY mode
  • FlashProgress — Track USB mass storage transfer progress
  • AuthState — QR code generation for backup authentication

New Functions:

  • get_gadget_mode() — Read current USB gadget mode from /etc/secubox/gadget-mode
  • draw_terminal() — Render serial terminal output on round display
  • draw_flash_progress() — Render flash transfer progress bar
  • draw_auth_mode() — Render QR code authentication screen

Fixes:

  • Hub service changed from Unix socket to TCP (VM compatibility)
  • FAQ and wiki updated with troubleshooting for common issues
  • Kiosk launcher fixed for VM sandbox issues (--no-sandbox flag)
  • Added public menu endpoint (/api/v1/hub/public/menu) for WebUI sidebar
  • Fixed Pydantic 1.x compatibility in auth.py for require_jwt dependency
  • Fixed "Failed to load menu: Invalid menu data" WebUI error

2026-04-28

Session 72 — v2.1.1 Release: Build and API Fixes

Release: v2.1.1 — Critical fixes for VirtualBox and ESPRESSObin builds

Issues Fixed:

  1. Python Dependencies (Debian Bookworm Compatibility)

    • Debian ships pydantic v1, but SecuBox requires v2
    • Added pip upgrade in build scripts: pydantic>=2.0, fastapi>=0.100, uvicorn>=0.25
    • Updated secubox-core postinst to auto-upgrade on install
  2. CORS Headers

    • Added CORS headers to common/nginx/secubox-proxy.conf
    • Fixes cross-origin API requests from web UI
  3. Login Endpoint Path

    • Fixed login.html: /auth/login/login
    • Affects both main and portal login pages
  4. Eye Remote Display Imports

    • Fixed display/__init__.py to import existing modules only
    • Changed service to use display_manager.py instead of main.py
  5. Eye Remote Rainbow Dashboard

    • Icons in rainbow circle: BOOT, AUTH, WALL, ROOT, MESH, MIND
    • Radar sweep syncs with targeted module glow
    • Metric arcs aligned with corresponding icon colors
    • Concentric rings: red (outer) → purple (inner)

Files Modified:

  • common/nginx/secubox-proxy.conf — CORS headers
  • packages/secubox-core/debian/postinst — pip upgrade
  • packages/secubox-hub/www/login.html — endpoint fix
  • packages/secubox-hub/www/portal/login.html — endpoint fix
  • image/build-live-usb.sh — version constraints
  • image/build-ebin-live-usb.sh — version constraints
  • image/multiboot/build-amd64-rootfs.sh — pip upgrade
  • remote-ui/round/agent/display/__init__.py — import fix

Wiki Updated:

  • Home.md — v2.1.1 announcement
  • Troubleshooting.md — API 502/auth fix section
  • Eye-Remote.md — HyperPixel dashboard info
  • Live-USB-VirtualBox.md — troubleshooting section

ESPRESSObin Live USB Rebuilt with Installer:

  • Built with --embed-image option for one-step eMMC flashing
  • Embedded: secubox-espressobin-v7-bookworm.img.gz (573MB)
  • Output: secubox-espressobin-v7-live-usb.img.gz (1.8GB)
  • Flash command: secubox-flash-emmc from live USB
  • Includes all v2.1.1 fixes (pydantic v2, CORS, login endpoints)

Session 73 — Eye Remote Real Metrics Integration

Feature: Real metrics fetching from connected SecuBox via OTG/WiFi

Components Created:

  1. Metrics Fetcher (remote-ui/round/agent/api/metrics_fetcher.py)

    • Async fetcher using aiohttp
    • Aggregates data from multiple SecuBox API endpoints
    • Connection state detection (OTG/WiFi/Disconnected)
    • Module-specific metrics (AUTH, WALL, MESH, etc.)
    • Double buffer for non-blocking display updates
  2. OTG Host Support for ESPRESSObin (packages/secubox-system/)

    • etc/udev/rules.d/90-secubox-eye-remote.rules — Detects Pi Zero CDC-ECM
    • usr/lib/secubox/eye-remote-connected.sh — Configures 10.55.0.1/30
    • usr/lib/secubox/eye-remote-disconnected.sh — Cleanup handler
  3. Display Integration (remote-ui/round/agent/display/fallback/fallback_manager.py)

    • Integrated MetricsFetcher for real data
    • Mode indicator shows connection type + latency
    • Module details show real vs local data source
    • Targeted metrics display with extra details

API Endpoints Used:

  • /api/v1/system/metrics — System metrics
  • /api/v1/auth/stats — Authentication stats
  • /api/v1/crowdsec/metrics — CrowdSec decisions
  • /api/v1/wireguard/status — WireGuard peers
  • /api/v1/dpi/stats — DPI flow data

Feature Plan Created:

  • .claude/plans/eye-remote-otg-features.md — 5 features roadmap:
    1. Real Metrics Display (implemented)
    2. OTG Tools Dashboard
    3. Gadget Parameters Control
    4. Storage Sync for Configs
    5. Self-Setup Portal

Session 71 — Eye Remote Display System v2.3.0

Feature: Complete display state machine with fallback, splash, and radar modes

Description: Implemented full Eye Remote display system with multiple visualization modes for Pi Zero W HyperPixel 2.1 Round (480x480). Includes connection state detection, animated splash screens, and local metrics radar visualization.

Components Created:

  1. Splash Screen System (display/splash.py)

    • Animated phoenix logo for boot/halt/start/reboot states
    • Pulsing glow effects with fire colors
    • Progress indicator ring
    • Fallback phoenix symbol if logo missing
  2. Fallback Display Manager (display/fallback/fallback_manager.py)

    • Connection state detection (OTG 10.55.0.1, WiFi secubox.local)
    • Four modes: OFFLINE, CONNECTING, ONLINE, COMMUNICATING
    • Local metrics radar with 6 concentric rings (AUTH, WALL, BOOT, MIND, ROOT, MESH)
    • 3D rotating cube with module icons when connected
    • Rainbow sweep line animation
  3. Touch Pattern Analyzer (display/fallback/touch_analyzer.py)

    • Noise pattern analysis for HyperPixel touch panel
    • Coordinate and delta frequency tracking
    • Discovered Y-axis oscillation at stable X (~240-250)
  4. Touch Calibration Tool (display/fallback/touch_calibrate.py)

    • Corner target display for manual calibration
    • Real-time coordinate overlay
  5. Radar Variants

    • radar_flashy.py — Vibrant colors with 3D cube and icons
    • radar_concentric.py — Balanced metric arcs centered at 12 o'clock
    • radar_rainbow.py — Rainbow colorization with sweep
    • radar_full.py — Complete feature set

Package Build:

  • Built all 128 SecuBox Debian packages successfully
  • ESPRESSObin V7 image rebuild with packages slipstreamed

Files Created:

  • remote-ui/round/agent/display/splash.py
  • remote-ui/round/agent/display/fallback/__init__.py
  • remote-ui/round/agent/display/fallback/fallback_manager.py
  • remote-ui/round/agent/display/fallback/touch_analyzer.py
  • remote-ui/round/agent/display/fallback/touch_calibrate.py
  • remote-ui/round/agent/display/fallback/radar_*.py (5 variants)

Version: v2.3.0


2026-04-27

Session 70 — Live Boot Complete Setup (v2.2.4-live)

Feature: Full live-boot implementation with squashfs and RAM boot

Description: Completed full live-boot setup for Pi Zero Eye Remote storage.img. Installed live-boot package, rebuilt initramfs with live-boot scripts, created squashfs filesystem, and updated boot.scr with proper live boot parameters.

Changes Made:

  1. Installed live-boot and busybox packages on ARM64 rootfs
  2. Rebuilt initramfs with live-boot scripts included
  3. Created /live/filesystem.squashfs (878MB) on data partition (sda4)
  4. Updated boot.scr with live boot parameters:
    • boot=live - enables live-boot mode
    • live-media=/dev/sda4 - partition with squashfs
    • live-media-path=/live - path to squashfs
    • toram - loads entire squashfs into RAM
    • DSA blacklist parameters preserved

Partition Layout:

  • sda1 (512MB): EFI - kernel, initrd, dtbs, boot.scr
  • sda2 (3GB): ARM64 rootfs (for reference)
  • sda3 (3GB): x86 rootfs (for VirtualBox/QEMU)
  • sda4 (9.5GB): Data + /live/filesystem.squashfs

Wiki Fix: Fixed sidebar link syntax from [[Page|Display]] to [Display](Page)

Version: v2.2.4-live


Session 69 — Live RAM Boot Cmdline Fix (v2.2.4-pre2)

Fix: Added missing boot=live live-media-path=/live parameters to bootargs

Description: Fixed critical issue where multiboot image was not configured for live RAM boot. The kernel command line was missing the required boot=live and live-media-path=/live parameters that the live-boot initramfs needs to work properly.

Files Modified:

  • image/multiboot/build-multiboot.sh — Added live boot parameters to setenv bootargs

Before:

setenv bootargs "root=${rootpart} rootfstype=ext4 rootwait rootdelay=10 ..."

After:

setenv bootargs "boot=live live-media-path=/live root=${rootpart} rootfstype=ext4 rootwait rootdelay=10 ..."

Version: v2.2.4-pre2


Session 68 — Multiboot Dual Boot Menu & Kernel Fix (v2.2.4-pre1)

Feature: Fixed ARM64 kernel installation and added interactive boot menu

Description: Fixed critical bug where ARM64 kernel, initrd, and DTB files were not being copied to the EFI partition. Added interactive dual boot menu with 5-second timeout, offering Live RAM Boot (default) or Flash to eMMC option.

Files Modified:

  • image/multiboot/build-multiboot.sh — Major fixes:
    • Fixed loop device release bug in install_arm64_rootfs() (was releasing before copying kernel)
    • Added build_arm64_rootfs_debootstrap() function with kernel installation
    • Added copy_arm64_kernel_to_efi() function to properly copy Image, initrd, DTBs
    • Updated boot.scr with interactive dual boot menu (5s timeout)
    • Added qemu-debootstrap and other optional dependency warnings
  • .github/workflows/build-multiboot.yml — Added prerelease support, bumped version
  • wiki/_Sidebar.md — Bumped version to v2.2.4-pre1

Boot Menu Options:

  1. Live RAM Boot (default with 5s timeout)
  2. Flash SecuBox to eMMC

Version: v2.2.4-pre1 (prerelease)


Session 67 — Multiboot Wiki & Eye Remote Docs (v2.2.3)

Feature: Wiki documentation for multiboot live OS and Eye Remote integration

Description: Added comprehensive wiki documentation for the multi-architecture boot system, including the new Multiboot wiki page, home page announcement banner, and sidebar navigation updates.

Files Created:

  • wiki/Multiboot.md — Full documentation for multiboot live OS

Files Modified:

  • wiki/Home.md — Added announcement banner for v2.2.3 multiboot
  • wiki/_Sidebar.md — Added Multiboot and Eye Remote links, bumped version
  • image/multiboot/README.md — Added Eye Remote integration section

Changes:

  • Eye Remote Pi Zero architecture documented with ASCII diagrams
  • Partition layout and boot flow explained
  • Build instructions and GitHub Actions CI docs
  • Troubleshooting section for common boot issues

Session 66 — Multiboot GitHub Action (v2.2.3)

Feature: GitHub Actions workflow for automated multiboot image builds

Description: Created automated CI/CD pipeline for building the multiboot live OS image with all SecuBox packages slipstreamed. Workflow builds .deb packages first, then creates the 16GB multiboot image with ARM64 and AMD64 rootfs partitions.

Files Created:

  • .github/workflows/build-multiboot.yml — CI workflow for multiboot image

Workflow Features:

  • Manual dispatch with configurable image size (8/16/32GB)
  • Optional desktop environment inclusion
  • Automatic .deb package builds from packages/
  • Debootstrap-based ARM64 and AMD64 rootfs creation
  • QEMU user-mode emulation for cross-arch chroot
  • XZ compression for releases
  • GitHub Release integration

Version: v2.2.3


Session 65 — Multi-Boot Storage System (v2.2.2)

Feature: Multi-architecture boot system for Pi Zero Eye Remote storage

Description: Created a multi-boot storage system that supports ARM64 (ESPRESSObin/MOCHAbin via U-Boot) and AMD64 (UEFI systems via GRUB) from a single USB storage device, with shared application data across both architectures.

Partition Layout (16GB+):

  • P1: EFI/FAT32 (512MB) — Boot files for both architectures
  • P2: ext4 (3GB) — ARM64 SecuBox rootfs
  • P3: ext4 (3GB) — AMD64 SecuBox rootfs
  • P4: ext4 (remaining) — Shared data partition

Features:

  • U-Boot boot.scr with USB/MMC auto-detection for ARM64
  • GRUB BOOTX64.EFI for AMD64 UEFI boot
  • Shared data partition with bind mounts for /etc/secubox, /var/lib/secubox, /srv/secubox
  • eMMC flasher image included for ARM64 installation
  • Debootstrap-based AMD64 rootfs builder with SecuBox packages

Files Created:

  • image/multiboot/README.md — Documentation
  • image/multiboot/build-multiboot.sh — Main build script
  • image/multiboot/build-amd64-rootfs.sh — AMD64 rootfs builder

Commits:

  • 5cf69c0 — feat(multiboot): Add multi-architecture boot system with shared data

Version: v2.2.2


Session 65 — Eye Remote USB Boot Fix (v2.2.1)

Issue: ESPRESSObin would not boot from Eye Remote USB mass storage. mv88e6xxx driver in infinite detection loop.

Root Cause: Live USB kernel had mv88e6xxx built-in (not a module), making modprobe.blacklist ineffective. The eMMC kernel has mv88e6xxx as a loadable module where blacklist works.

Fix:

  • Replaced storage.img boot partition with eMMC kernel/initrd/DTB
  • Replaced storage.img rootfs with working eMMC rootfs
  • Updated boot scripts with extended blacklist for future builds

Files Modified:

  • board/espressobin-v7/boot-live-usb.cmd
  • board/espressobin-v7/boot-usb.cmd
  • board/espressobin-v7/boot.cmd

Commits:

  • 942196b — fix(boot): Add mv88e6085 and initcall_blacklist to boot scripts

Version: v2.2.1

Session 65 — HAProxy Service Restart Loop Fix

Issue: secubox-haproxy.service in restart loop with NAMESPACE error.

Root Cause: RuntimeDirectory=haproxy triggers systemd namespace setup which expects /etc/haproxy to exist. HAProxy is Recommends: not Depends:.

Fix:

  • postinst creates /etc/haproxy if not present
  • Removed RuntimeDirectory=haproxy from service
  • Moved directory creation from import-time to startup event
  • Increased RestartSec 5→30s

Commits:

  • 4321a7c — fix(haproxy): Prevent service restart loop
  • 9f47e54 — fix(haproxy): Create /etc/haproxy and remove RuntimeDirectory=haproxy

2026-04-23

Session 64 — Eye Remote USB OTG Network Fix (v2.1.1)

Issue: USB OTG network connection showed NO-CARRIER on Linux hosts despite Pi Zero interface being UP.

Root Cause Analysis: The USB composite gadget creates two network interfaces on the Pi Zero:

  • usb0 → RNDIS function (Windows compatible)
  • usb1 → ECM function (Linux/Mac via cdc_ether driver)

Linux hosts use the ECM driver which maps to usb1. The old scripts configured usb0 only, or both interfaces with the same IP (10.55.0.2/30), causing asymmetric routing where packets received on usb1 could be replied via usb0.

Fix Applied:

  • Configure only usb1 (ECM) for Linux host compatibility
  • Fallback to usb0 only if usb1 doesn't exist

Files Modified:

  • remote-ui/round/secubox-otg-gadget.sh — Wait for and configure usb1
  • remote-ui/round/files/etc/secubox/eye-remote/gadget-setup.sh — Same fix
  • remote-ui/round/agent/main.pyensure_usb_network() prefers usb1
  • remote-ui/round/agent/network_debug.py — New debug script

Results:

  • USB OTG network connectivity working (0.3ms latency)
  • Display shows OTG mode instead of SIM
  • Host NetworkManager connection persisted ("SecuBox OTG")

Commits:

  • 48de244 — fix(eye-remote): Use usb1 (ECM) instead of usb0 for Linux hosts
  • f7b4bb4 — style(eye-remote): Adjust pod positions for hexagonal ring layout

Version: v2.1.1


2026-04-15

Session 59 — EspressoBin eMMC Flasher & VirtualBox Graphics Fix

v1.7.0 — EspressoBin Live USB with eMMC Flasher

  • Built EspressoBin V7 live USB image with embedded eMMC flasher
  • Fixed SquashFS path issue (/filesystem.squashfs/live/filesystem.squashfs)
  • Fixed boot partition sizing for embedded images (dynamic sizing)
  • Added secubox-flash-emmc command for easy eMMC flashing
  • Successfully booted live USB and flashed to eMMC on real hardware

v1.6.7.14 — VirtualBox VMSVGA Graphics Fix (Issue #29)

  • Root cause: VirtualBox with VMSVGA controller (default since VBox 6) needs vmware X11 driver
  • systemd-detect-virt returns "oracle" but GPU shows "VMware SVGA" in lspci
  • Created secubox-x11-setup.service for boot-time VM detection and X11 driver selection
  • Updated kiosk launcher (v3.3) to defer to X11 setup service
  • Driver selection: VBox+VMSVGA→vmware, VBox+VBoxVGA→modesetting, VMware→vmware, KVM→modesetting

Slipstream Default Change

  • Changed SLIPSTREAM_DEBS default from 0 to 1 in build-image.sh
  • All images now include 126 SecuBox packages by default

Files Modified

  • image/build-live-usb.sh — X11 auto-setup service, vmware driver install
  • image/build-ebin-live-usb.sh — Dynamic boot partition sizing, SquashFS path fix
  • image/build-image.sh — SLIPSTREAM_DEBS=1 default
  • image/sbin/secubox-kiosk-launcher — v3.3, vmware driver for VBox VMSVGA
  • image/systemd/secubox-kiosk.service — depends on x11-setup service

Builds In Progress

  • AMD64 live USB with VBox graphics fix
  • EspressoBin eMMC image with 126 packages

2026-04-14

Session 57 — Live USB Fixes & VirtualBox Testing

v1.6.7.12 — Lenovo Boot Fix (Issue #26)

  • Added fallback EFI bootloader at /EFI/BOOT/BOOTX64.EFI for Lenovo/HP/Dell
  • Fixed CI --slipstream flag in build-live-usb.sh
  • Fixed banner alignment in secubox-flash-disk
  • Tested and confirmed working on real Lenovo hardware

v1.6.7.13 — VirtualBox Detection Fix (Issue #27)

  • Fixed VM detection using systemd-detect-virt ("oracle") instead of lspci
  • VBox with VMSVGA was incorrectly detected as VMware
  • Result: WebUI works in VBox, kiosk works on real hardware

v1.6.7.14 — Network Auto-Discovery (Issue #28)

  • Enhanced secubox-net-fallback with LAN auto-discovery
  • Probes common gateways (192.168.1.1, 192.168.0.1, 192.168.255.1, 10.0.0.1...)
  • Auto-configures IP .250 on discovered subnet when DHCP fails
  • Only uses 169.254.1.1 as last resort

Wiki Updates

  • All Home pages (EN, FR, DE, ZH) now use /releases/latest/download/ URLs
  • Fixed script paths (scripts/ → image/)
  • Removed hardcoded version numbers

Builds Completed

  • x64: secubox-live-amd64-bookworm.img (8GB)
  • ARM64: secubox-espressobin-v7-live-usb.img (539MB)

GitHub Issues Closed

  • #26 Lenovo Error 1962 boot fix
  • #27 VBox kiosk not starting
  • #28 Network fallback 169.254.1.1

Tags: v1.6.7.12, v1.6.7.13, v1.6.7.14


2026-04-03

Session 34 — Build Timestamp & System Fixes

secubox-hub v1.2.0 — Build Timestamp Display

  • Added _get_build_info() API function to read /etc/secubox/build-info.json
  • Dashboard header now displays build timestamp badge (date + time)
  • Tooltip shows git commit hash, branch, and board type on hover
  • Build scripts create build-info.json during image generation

Build System Improvements

  • Fixed build-live-usb.sh package priority (prefers output/debs over cache)
  • Fixed secubox-soc-web nginx config (installs to secubox.d/ not sites-available/)
  • Removed broken secubox-repo.conf symlink creation from postinst scripts

Packages Updated

  • secubox-hub_1.2.0-1~bookworm1_all.deb — Build timestamp feature
  • secubox-soc-web_1.1.0-1_all.deb — Nginx config path fix

Release v1.4.0

  • Tag: v1.4.0
  • Commit: 19ca292
  • All changes pushed to origin/master

2026-03-30

Plymouth Boot Splash & Kiosk Fixes

  • Added Plymouth boot splash with VT100/DEC PDP-style green phosphor theme
  • Boot graphics now show DURING boot (not just at login)
  • Fixed kiosk mode service configuration:
    • Changed from tty1 to tty7 (like standard display managers)
    • Proper VT allocation and switching
    • Better wlroots environment variables for VMs
    • Added tty supplementary group for DRM access
  • Updated GRUB menu entries with splash parameter
  • Added initramfs configuration for Plymouth framebuffer
  • RPi 400 build: Added Plymouth support with ARM64 theme
  • Tags: v1.3.6

Previous Boot Fixes (v1.3.2-v1.3.5)

  • Added VT100 retro CRT DEC PDP-style cyber splash
  • Added hardware auto-check boot mode (secubox.hwcheck=1)
  • Fixed boot hanging services with timeouts
  • RPi 400 image builder with HDMI console autologin

2026-03-29

Kiosk Mode Bug Fixes

  • Fixed UID mismatch issue — service now detects actual kiosk user UID
  • Fixed timing issue — cmdline handler defers package installation to after network
  • Fixed marker file confusion (.kiosk-installed vs .kiosk-enabled)
  • Updated build-live-usb.sh to fully setup kiosk when --kiosk flag used
  • Improved start-kiosk.sh to wait for nginx/hub services (30s max)
  • Service now uses ConditionPathExists to check enabled state

2026-03-28

Network Auto-Detection & Preseed System

  • Created secubox-net-detect — Auto-detection of WAN/LAN interfaces
    • Board detection: MochaBin, ESPRESSObin v7/Ultra, x64 VM/baremetal
    • Interface mapping based on device model (eth0=WAN, lan*=LAN)
    • Netplan generation for router/bridge/single modes
    • Link detection for x64 auto-discovery
  • Board configurations created:
    • board/x64-live/config.mk — Live USB settings
    • board/x64-vm/config.mk — VM-specific settings
    • Netplan templates for each board
  • Kernel cmdline handler:
    • secubox-cmdline-handler — Parses secubox.* kernel params
    • secubox.netmode=router|bridge|single
    • secubox.kiosk=1 for GUI mode
  • Kiosk GUI mode:
    • secubox-kiosk-setup — Install/enable/disable minimal GUI
    • Cage Wayland compositor + Chromium fullscreen
    • Perfect for touchscreen/kiosk deployments
  • Updated build-live-usb.sh:
    • GRUB menu entries for Kiosk Mode, Bridge Mode
    • Installs net-detect, cmdline-handler, kiosk-setup
    • Systemd services for early boot configuration
  • Updated firstboot.sh with network auto-detection integration

secubox-localai v1.0.0 Complete

  • Fifth Phase 8 package ported from OpenWRT
  • FastAPI backend with 15+ endpoints
  • Features: Container management, model gallery, chat completion
  • OpenAI-compatible API proxy (/v1/chat/completions, /v1/completions)
  • Model gallery with popular LLMs (Llama, Phi, Gemma, Mistral)
  • CRT-light P31 phosphor theme with LocalAI purple accents
  • Deployed to VM at https://localhost:9443/localai/
  • Total modules: 59

secubox-zigbee v1.0.0 Complete

  • Fourth Phase 8 package ported from OpenWRT
  • FastAPI backend with 20+ endpoints
  • Features: Container management, device pairing, MQTT integration
  • USB serial dongle detection and passthrough (/dev/ttyUSB*, /dev/ttyACM*)
  • Device management: rename, remove, permit_join toggling
  • CRT-light P31 phosphor theme with Zigbee green accents
  • Deployed to VM at https://localhost:9443/zigbee/
  • Total modules: 58

secubox-lyrion v1.0.0 Complete

  • Third Phase 8 package ported from OpenWRT
  • FastAPI backend with 18+ endpoints
  • Features: Container management, player control, library scanning
  • Squeezebox JSON-RPC API integration for library stats
  • CRT-light P31 phosphor theme with Lyrion orange accents
  • Backup and restore functionality
  • Deployed to VM at https://localhost:9443/lyrion/
  • Total modules: 57

secubox-jellyfin v1.0.0 Complete

  • Second Phase 8 package ported from OpenWRT
  • FastAPI backend with 15+ endpoints
  • Features: Container management, library config, backup/restore
  • CRT-light theme with Jellyfin blue accents
  • Deployed to VM at https://localhost:9443/jellyfin/
  • Total modules: 56

secubox-ollama v1.0.0 Complete

  • First Phase 8 package ported from OpenWRT
  • FastAPI backend with 15+ endpoints
  • Features: Container management, model pulling, chat, generation
  • CRT-light P31 phosphor theme frontend
  • Deployed to VM at https://localhost:9443/ollama/

Migration Preparation Workflow Complete

  • Created .claude/REMAINING-PACKAGES.md — 53 packages remaining inventory
  • Classified packages by complexity: Easy (25), Medium (18), Complex (10)
  • Identified 25 packages with different naming (already ported)
  • Defined Phase 8 (21 apps), Phase 9 (22 tools), Phase 10 (10 security)
  • Set priority: ollama → jellyfin → vault → homeassistant

Previous Session Highlights

  • 52 Debian packages complete (~1000+ API endpoints)
  • All Phases 1-7 completed
  • CVE Triage enhanced with CISA KEV, NVD, EPSS feeds
  • CRT-light theme standardized across all modules
  • Master-Link admin dashboard with P31 phosphor theme

2026-03-27

Live ISO Boot Console Fixes

  • Fixed flickering console on live ISO boot
  • Masked 14 incompatible services for live mode
  • Fixed getty autologin conflict
  • Disabled martian packet logging

C3Box Clone System

  • build-installer-iso.sh — Hybrid Live USB / Headless Installer (886 lines)
  • export-c3box-clone.sh — Export device configuration
  • build-c3box-clone.sh — Combined export + ISO workflow

2026-03-26

  • Admin dashboard at /master-link/admin.html
  • Token-based mesh enrollment
  • Multi-master support (Debian + OpenWRT)
  • sbx-mesh-invite and sbx-mesh-join CLI tools

Socket Directory Fix

  • secubox-runtime.service ensures /run/secubox exists

ReDroid Integration

  • Android in Container LXC setup scripts

2026-03-25

Documentation Phase Complete

  • API Reference in 3 languages (EN/FR/ZH)
  • Module documentation for all 48 modules
  • UI Guide with CRT theme documentation
  • 45 module screenshots captured

Go Daemon Organization

  • Moved to daemon/ directory structure
  • Unix socket control server implemented
  • secuboxd and secuboxctl binaries

2026-03-22

Phase 5 — CSPN Hardening Complete

  • AppArmor profiles for all services
  • Kernel sysctl hardening
  • Module blacklist
  • auditd rules
  • nftables DEFAULT DROP policy

2026-03-21

Phase 3 — All 33 Modules Complete

  • 1000+ API endpoints total
  • All services running on VM
  • Dynamic menu system
  • Shared sidebar.js

Phase 4 — APT Repo Complete

  • apt.secubox.in configured
  • reprepro + GPG signing
  • CI publish workflow
  • Metapackages (full/lite)

2026-03-20

Phase 2 — Infrastructure Complete

  • secubox_core Python library
  • nginx reverse proxy template
  • rewrite-xhr.py script

Phase 1 — Hardware Bootstrap Complete

  • build-image.sh for arm64 + amd64
  • VirtualBox VM support
  • Board configs (MOCHAbin, ESPRESSObin, VM)

Project Statistics

Metric Value
Debian packages 61
API endpoints ~1200+
OpenWRT packages (total) 103
Remaining to port 46
Phases completed 7 of 10 (Phase 8: 9/21)
Current release v1.4.0
Target completion Phases 8-10 remaining

Session 99 (continued) — MOCHAbin Migration Execution

Date: 2026-05-06

Completed:

  1. Exported from C3BOX:

    • 93 SSL certificates
    • 99 nginx secubox.d configs
    • HAProxy config
    • 4 LXC container configs
    • Error pages (400, 403, 408, 500, 502, 503, 504)
  2. Transferred to MOCHAbin (192.168.255.1)

  3. HAProxy configured:

    • All 93 SSL certs in /data/haproxy/certs/
    • LXC routing: gitea, nextcloud, mail, matrix
    • Default backend: nginx_vhosts (port 9080)
    • All backends UP
  4. Nginx configured:

    • Default 503 server for unknown domains
    • WebUI served only for specific hostnames
    • 99 module API configs in secubox.d
  5. CTL tools deployed:

    • 14 CTL tools copied to /usr/sbin/
    • Tools Debian-native (OpenWrt refs only in migrate commands)
    • Tested: vhostctl, metablogizerctl, crowdsecctl, streamlitctl
  6. Vhost auto-creation:

    • Created /usr/local/bin/secubox-vhost-create
    • Supports: proxy, streamlit, static vhost types

Verified:

  • admin.gk2.secubox.in → 200 (WebUI)
  • git.maegia.tv → 200 (Gitea LXC)
  • unknown.test.com → 503 (blocked)

  1. WAF configured:
    • HAProxy ACL-based WAF active
    • Blocks: SQLi, XSS, Path Traversal, Scanners
    • Mitmproxy WAF disabled (pyOpenSSL ARM64 incompatibility)
    • Full mitmproxy WAF requires internet to install updated packages

Test Results:

Normal request:     200 ✓
SQLi attempt:       403 ✓ (blocked)
XSS attempt:        403 ✓ (blocked)
Path traversal:     403 ✓ (blocked)
Scanner UA:         403 ✓ (blocked)

Session 118 — 2026-05-08

Focus: Kernel build completion with LED driver, USB network modules, documentation

Accomplishments

  1. Kernel 6.12.85-openwrt-led build completed:

    • IS31FL319X LED driver built-in (=y)
    • DSA mv88e6xxx switch built-in (=y)
    • All network/storage drivers built-in
    • USB network modules (cdc_ether, usbnet) as =m for Eye Remote
  2. Documentation created:

    • kernel-build/README.md - Full build instructions
    • Config fragment: board/mochabin/kernel/config-6.12-openwrt-merged.fragment
  3. GitHub Issue #60 updated with build progress

  4. Kernel deployed to MOCHAbin /boot/Image-openwrt

Config Fragment Additions:

CONFIG_OF_OVERLAY=y
CONFIG_OF_CONFIGFS=y
CONFIG_LEDS_IS31FL319X=y
CONFIG_USB_USBNET=y
CONFIG_USB_NET_CDCETHER=y
CONFIG_USB_NET_CDC_NCM=y
CONFIG_USB_NET_RNDIS_HOST=y

USB Gadget Architecture Documented:

  • SecuBox = USB HOST (sees gadgets as peripherals)
  • Eye Remote = USB DEVICE/GADGET (Pi Zero W)
  • SecuBox needs cdc_ether HOST driver, not gadget drivers
  • udev rules at /etc/udev/rules.d/90-usb-gadget.rules

Pending:

  • Test LED functionality after reboot
  • Test USB network with Eye Remote
  • Verify DSA switch works

LED Fixes (Session 118 continued)

  • Fixed I2C communication errors (brightness value 10 optimal)
  • Removed old conflicting LED scripts (secubox-led-heartbeat)
  • Updated healthbump with rate-based security detection
  • Timer enabled (30s interval)
  • Activity pulse on each check
  • Color gradient for hardware load level

Final LED Status:

  • LED1 (HW): Green/Yellow/Orange/Red based on load %
  • LED2 (SVC): Green=ok, Red=error
  • LED3 (SEC): Green=clear, Blue=mitigating, Yellow=elevated, Red=attack

Session 130 — NAC ARP Discovery Fix

Problem: NAC dashboard showing "0 clients" despite active clients on network.

Root Cause:

  • NAC module relied exclusively on /var/lib/misc/dnsmasq.leases for client discovery
  • dnsmasq not configured as DHCP server (leases file empty)
  • Clients using static IPs or getting DHCP from external router

Solution: Added ARP-based client discovery as fallback:

  • New _parse_arp() function reads kernel ARP table via ip neigh show
  • New _discover_clients() combines DHCP leases + ARP fallback
  • Filters out gateway IPs (.1, .254) and non-LAN interfaces
  • Includes ARP state (REACHABLE/STALE) for online detection
  • Deduplicates clients by MAC address

Files Modified:

  • packages/secubox-nac/api/main.py — Added ARP discovery (~80 lines)

Results:

  • NAC now discovers 2 clients via ARP: 192.168.1.36 (REACHABLE), 192.168.255.2 (STALE)
  • Dashboard shows correct client count and online status
  • Clients start in quarantine zone (default for new discoveries)

Technical Details:

  • LAN interfaces scanned: lan0, lan1, lan2, lan3, br0, br-lan, eth0, eth1
  • ARP states mapped to online: REACHABLE, DELAY, PROBE, PERMANENT = online
  • 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.
  • Documented the three feature-flagged, OPAD opt-in evolutions delivered by Tasks 1-16 on feature/p2p-dht-federation (126 tests passing), built on the existing WireGuard mesh + annuaire-backed registry — no new code in this pass, docs only (Task 17, finalization).
  • DHT discovery (api/dht.py): custom Kademlia DHT (asyncio/UDP, JSON wire), Ed25519-signed reachability records, iterative lookup, bootstrap, routing-table persistence, advisory health store. Endpoints: GET /dht/peers, POST /dht/announce (JWT), GET /dht/find/{did}.
  • Service federation health-checks (api/federation.py): debounced up/down HealthStore, async HealthChecker sweep (semaphore-capped), default_probe (HTTP GET /health, TCP fallback), publishes into the DHT when both subsystems are enabled. Endpoints: GET /federation/services, POST /federation/healthcheck (JWT).
  • Hierarchical master-link (api/masterlink.py): deterministic elect(), monotonic TermStore, MasterLink heartbeat/election-timeout state machine with Ed25519-signed heartbeats and equal-term tie-break. Endpoints: GET /masterlink/topology, POST /masterlink/promote (JWT).
  • Config: [dht], [federation], [masterlink] sections in p2p.toml, all disabled by default (DHT_DEFAULTS/FEDERATION_DEFAULTS/ MASTERLINK_DEFAULTS in api/mesh.py). Audit events (dht_announce, masterlink_promote) append to /var/log/secubox/p2p-audit.log.
  • Updated packages/secubox-p2p/README.md with an "Evolutions (#774)" section covering all of the above (endpoints, real config defaults, audit log). No .py files touched, no tests run/modified.

2026-07-03 → 07-04 — ToolBox privacy report: fidelity, media types, character sheet (#785 #790 #792 · +incident)

Three features, brainstorm → spec → plan → subagent-driven SDD with two-stage review + adversarial whole-branch review, all merged to master, deployed + validated live on gk2.

  • #785 — kbin report PDF fidelity + media types + WebUI DPI (PR #787). The report PDF now mirrors its (excellent) HTML page: DPI-Exfil (me) + Overall (all) render as matplotlib donut-grids instead of text bullets; new 🎬 media-types block (real MIME captured by sbxmitm — video/audio/HLS·DASH — and the DPI service category media), me + overall, in PDF and web. New shared secubox_core.media_catch aggregator (bounded tail-read, fail-empty). Two DPI WebUI cards (services-by-category bytes + MIME media types) + public /media_types endpoint. Follow-up #786 filed (double-caching invariant for the media aggregation).
  • Incident (during #785 deploy): heavier PDFs (more matplotlib grids, ~9 s render) ran synchronously on the single-worker event loop and the WAF 504-page auto-retry storm wedged the whole toolbox vhost (board-wide 504). Hardened live + in source: render off-loop (threadpool) + serialized (asyncio lock; pyplot isn't thread-safe) + per-device PDF cache (double-checked → a retry storm = 1 render, rest instant) + persistent MPLCONFIGDIR (drop-in 30-mplcache.conf + postinst). See memory project_toolbox_pdf_render_eventloop.
  • #790 — rich Netrunner character sheet + route parity (PR #794). The PDF fiche de personnage is now faithful to the HTML .nr card: Caractéristiques with pips ●●●○○○ + notes, 🎒 Inventaire ✓/✗, 🐉 Bestiaire, and a new ⚔️ Quêtes/menaces section. _enrich_report_data(mac_hash, data, ua="") factored out of report_me and applied to /report/{token} + /admin/.../report → all PDF routes now emit the same rich content (were bare before). Full-fpdf, no new matplotlib.
  • #792 (folded into #790) — real Quêtes. _dpi_stats now exposes alerts_raw (kind/service/dst/detail) beside the donut alerts ({label,count}); the Quêtes section (HTML + PDF) shows each threat's destination + detail (🗡️ NEW CLOUD — AWS S3, 🗡️ BEACONING — Google LLC · 15 flux périodiques) instead of a dangling em-dash.

Live on gk2: real device renders a 5-page PDF with pip bars, ✓/✗ inventory, and populated Quêtes; token route enriched; board stable (offload+lock+cache intact). 202 toolbox tests green. New wiki page ToolBox documenting the cabine use case + report features.