Root-scoped ctl for the per-peer wg-toolbox MITM R-level policy (task 4 of
the rlevel-per-peer plan): set-floor/force/set-chosen/set-default/list.
Shadow-write + atomic rename of peer-rlevel.json (the rename itself is the
hot-reload signal the Go workers poll for), server-side clamp of set-chosen
to the peer's floor, regeneration of the nft off-bypass drop-in from peers
whose effective mode is off (one priority below the toolbox DNAT fanout),
and an append-only JSON audit line per mutation. Invalid mode or unknown
pubkey aborts before any write.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Both sbxmitm accept paths (CONNECT/handleConnect and transparent/
handleTransparent) now go through a single decideForPeer(clientIP, host, sni)
helper: it calls the existing px.pol.Decide, then clamps the verdict to the
calling peer's R-level via px.rlevel.ModeForIP + clampVerdict when a
PeerPolicy is wired in. Factoring both call-sites onto the same helper means
they can never drift on how the clamp is applied.
Proxy gains an rlevel *PeerPolicy field, loaded in main() from
SECUBOX_PEER_RLEVEL / SECUBOX_WG_PEERS (env-overridable, falling back to
peer-rlevel.json and machash.go's wgPeersPath). A nil rlevel is a total
no-op — every Proxy built without it (existing suite, CONNECT PoC) keeps
today's exact px.pol.Decide behavior.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Joins peer-rlevel.json (pubkey -> {chosen,forced,floor}) with wg-peers.json
(pubkey -> ip) into an ip -> RLevel map. ModeForIP resolves an unknown IP to
the file's declared defaults, and any unreadable/corrupt input degrades to
Passive for every peer (fail-closed). Hot-reloads both files on mtime change,
mirroring the reload.Watcher pattern already used by policy.go.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
PAC qui dévie les .onion vers le SOCKS Tor local (SOCKS5 = remote DNS, Tor
résout le .onion), tout le reste en DIRECT (le transparent wg-toolbox inspecte
déjà). SOCKS confiné sur 192.168.1.200:9050 (SocksPolicy accept LAN+wg reject *,
jamais open-relay). Servi par nginx en application/x-ns-proxy-autoconfig,
LAN-only. Prérequis signalé : tor est inactive/failed sur le board, le plan doit
le réparer avant de livrer une pièce morte.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
L'UI réelle de picobrew_pico vit sur son sous-domaine LAN-confiné
(picobrew.gk2, avec zoom), donc une iframe pleine page dans la page admin était
redondante. La page admin devient un cockpit de supervision :
- contrôle : démarrer / arrêter / redémarrer / rafraîchir ;
- cartes statut : état, version figée, session, mémoire du conteneur ;
- bloc « accès distant » : lien Ouvrir vers l'interface complète, adresse LAN
directe, note mode zoom, redirection DNS de l'appareil ;
- journal picobrew_pico (tail du conteneur).
ctl : status --json expose mem_mb (best-effort cgroup, toujours un entier pour
ne pas casser le JSON) ; nouveau verbe restart (stop+start ; la garde de session
ne concerne que update). API : routes /restart et /logs (dégradation propre
conteneur à l'arrêt). Sudoers inchangé (tout picobrewctl déjà accordé).
Vérifié live : status mem_mb=88, /logs rend le vrai journal, panel sans iframe.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Le pipeline MITM sert un flux déchiffré avec un handler manuel (up.Do → resp →
write) : un http.Client ne peut PAS porter un « 101 Switching Protocols », donc
tout wss:// sur un hôte MITM'd traversant wg-toolbox échouait (Socket.IO de
picobrew_pico, zigbee2mqtt, toute app temps-réel). Seuls les hôtes splicés
passaient (leur io.Copy brut portait nativement le WebSocket).
Fix (comme httputil.ReverseProxy) : détecter l'upgrade (Connection: upgrade +
Upgrade: websocket), dialer upstream en forçant http/1.1 (un 101 ne peut pas
rider h2), transmettre le handshake verbatim, relayer le 101, puis piper les
octets bruts bidirectionnellement jusqu'à fermeture. Fait AVANT anonymize/DPI/
inject (un upgrade est un canal de contrôle, pas une requête inspectable ; les
en-têtes Sec-WebSocket-* doivent atteindre upstream intacts). La copie
client→upstream lit le bufio.Reader du pipeline (frames déjà bufferisées),
sinon elles seraient perdues.
dialUChrome gagne un paramètre ALPN (dialUChromeALPN) — un seul call-site — pour
que la voie WS force http/1.1 sans toucher la voie normale (h2 préféré).
Tests : détection (liste de Connection, casse, faux positifs) + relais 101 +
pipe bidirectionnel echo sur un upstream TLS réel. build + vet + suite complète
-race verts, zéro régression.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Webui admin la plus complète possible sans dev : bandeau de contrôle SecuBox
(état + start/stop/rafraîchir + plein écran) au-dessus d'un iframe de l'UI
complète de picobrew_pico, servie via son sous-domaine LAN-confiné
picobrew.gk2.secubox.in (même domaine parent → cert wildcard valide, pas de
mixed-content). L'iframe n'est chargée que conteneur en marche.
Réf. montage vhost (live gk2, patron lyrion) : sous-domaine LAN-only
(snippet exposure allow-privé/deny-all) → nginx:9080 → LXC:80, TLS terminée
par HAProxy, route dans /etc/secubox/waf/haproxy-routes.json, upgrade WebSocket
(Upgrade/Connection comme spiderfoot). CAVEAT: un wss:// via wg-toolbox échoue
tant que sbxmitm ne pipe pas l'upgrade WebSocket dans un flux MITM'd (général,
pas spécifique picobrew).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
« apt install secubox-picobrew » provisionne désormais le conteneur tout seul :
nouveau service oneshot secubox-picobrew-provision.service qui lance
`picobrewctl install` (debootstrap + clone picobrew_pico + venv + nginx/TLS),
démarré --no-block par le postinst (dpkg rend la main en ~40s, pas 5-8 min) et
gardé par ConditionPathExists=!pinned-sha (provisionne une fois, no-op ensuite).
Prouvé live sur gk2 : reset LXC → dpkg -i 2.0.1 → auto-provision en arrière-plan
→ LXC RUNNING 10.100.0.150, panel 200, /api/v1/picobrew/status JSON, appareil
:80 et TLS :443 à 200. Le webui est bien le frontend de picobrewctl via l'API.
Documente le motif comme guideline recommandée (.claude/MODULE-COMPLIANCE.md,
section « Install activates — LXC / external-service modules ») : install =
activation complète, provisionnement lourd en oneshot --no-block gardé par
marqueur, contrat ctl install (idempotent, pin-not-float, marqueur au succès
seul), et le piège de routage gk2 (webui.conf exact-match ne charge pas
secubox.d → module à socket dédié = location explicite modèle waf). Réf :
secubox-picobrew.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Réécrit le panneau selon le look management par défaut du dépôt
(hybrid-dark, Courier Prime, sidebar via /shared/sidebar.js), au lieu du
squelette minimal C3BOX. Ajoute des toasts d'action (échec start/stop plus
silencieux), vérifie response.ok, et un lien vers l'interface réelle de
l'appareil (https://<ip>/) affiché quand le conteneur tourne.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Découvert à l'installation live sur gk2 : `systemctl reload unbound` (SIGHUP)
ne recharge PAS les local-zone d'unbound — le drop-in picobrew.com n'était donc
jamais appliqué avant un restart. Préférer `unbound-control reload` quand il est
configuré, sinon try-restart, sinon reload en dernier recours (best-effort).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
C2: fix IP collision — picobrew's 10.100.0.140 is already used by
secubox-frigate on br-lxc (verified /data/lxc/frigate/config); reassign to
10.100.0.150 everywhere (ctl, unbound drop-in, tests, specs/plans docs).
C3: postinst no longer aborts under set -e when the unit is masked
(unmask before enable, tolerate failure like the start line already does).
C4: drop ReadWritePaths=/sys/bus/w1 from the unit — the path doesn't exist
on the board and phase 1 ships no sensor controller (preserved untouched in
lib/stillwatch/); no other sensor paths were present to remove.
C5: run the API as User=secubox/Group=secubox instead of root, matching the
sudoers grant (secubox -> picobrewctl) and the repo's single-privileged-
surface rule; drop the now-pointless ExecStartPost socket chown/chmod;
NoNewPrivileges=no kept and documented (required for sudo to work). No
shared parent (/run/secubox, /etc/secubox, /var/log/secubox) is touched.
I1: prerm no longer removes the nginx conffile on upgrade (only
remove/purge), so an upgrade keeps the API/panel route alive.
I2: /status, /start, /stop become plain `def` handlers — they call the
synchronous, blocking `_ctl` (subprocess.run, 20s timeout) and must not run
on the async event loop.
I3: add python3-fastapi | python3-pip to Depends (api/main.py imports
fastapi directly).
I4: drop ConditionArchitecture=arm64 and the stale 1-Wire/I2C sensor
comment — phase 1 is a sensor-less LXC appliance, nothing arch-specific.
I5/I6: rewrite debian/control's Description and README.md to describe what
phase 1 actually ships (LXC-hosted picobrew_pico server + DNS rewrite +
panel), moving fermentation/sensor tracking to "later phase, not shipped".
I7: panel now links to the real PicoBrew interface (https://<lxc-ip>/),
shown only while the container is running.
I8: document in the plan's manual verification recipe that LAN -> br-lxc
forwarding (default-DROP nftables) must be checked before the first real
device test, since that test is also the first test of that network path.
Mineurs: panel act()/load() gain try/catch and response.ok checks with
visible error feedback (previously implemented alongside C1, already
committed: argv fix + guard test, _wait_running polling loop replacing the
fixed `sleep 3` in cmd_install, cmd_status validating the pinned SHA before
emitting it, and a comment documenting PICOBREW_SESSION_FILE as test-only).
`cmd_update` called `lxc_attach '<script>' _ "$sha"` on top of `lxc_attach`'s
own internal `sh -c "$cmd" _ "$@"` framing, shifting the SHA one position to
the right. The remote script's `$1` landed on the literal `_` placeholder
instead of the SHA, so `git fetch --depth 1 origin "$1"` always failed and
`update` could never work.
Adds a discriminating test that fakes lxc-attach/lxc-info/id in PATH to
capture the real argv delivered to the remote `sh -c` invocation and assert
the SHA lands on $1. Verified to fail when the superfluous `_` is
reintroduced (see final-fixes-report.md for the captured failure).
debootstrap ships nginx with an auto-enabled systemd preset, so on a fresh
LXC nginx is already active (default vhost, listen 80) before cmd_install
writes the TLS config. `systemctl enable --now` is a no-op on an already
active unit: the new 443 config never loads, and nginx keeps holding :80,
which then makes picobrew.service (Flask) fail to bind. Replace with
`systemctl enable nginx && systemctl restart nginx`, which covers both the
stopped and already-active cases. nginx -t still gates activation and the
existing failure handling is unchanged.
Les 3 tests existants patchaient api.main._ctl en entier : le corps de
_ctl (argv sudo exact, try/except OSError/SubprocessError, garde
JSONDecodeError) n'était jamais exécuté. Ajoute 5 tests qui patchent
subprocess.run à la place, chacun vérifié discriminant en cassant puis
restaurant temporairement api/main.py (sudo/-n retiré, try/except
retiré, garde JSON retirée) — preuves dans .superpowers/sdd/task-2-report.md.
api/main.py inchangé.
Revue pré-vol : la config nginx faisait écouter nginx sur :80 tout en
proxifiant vers 127.0.0.1:80, où Flask écoute déjà. nginx se serait parlé à
lui-même. nginx ne prend que le 443 ; les appareils non-Z joignent Flask
directement en :80.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Plan d'implémentation de la phase 1 de la spec : 9 tâches TDD, 46 étapes.
Le provisionnement réel (debootstrap, root, réseau) n'étant pas testable en
unitaire, les parties décidables sont isolées derrière des entrypoints cachés
testables : __guard (validateur de SHA), __emit-config (config LXC) et
__emit-nginx (terminaison TLS). Le reste est couvert par une recette de
vérification manuelle explicite plutôt que par de faux tests.
Auto-revue : écart trouvé et corrigé — la spec place le TLS série Z en phase 1
alors que le plan le renvoyait hors périmètre ; or la série Z n'accepte que
HTTPS, un appareil Z aurait donc été inutilisable. Tâche 8 ajoutée.
Le contrôleur de capteurs (992 lignes) est déplacé intact dans lib/stillwatch/
plutôt que supprimé : la phase 2 le reprendra sans archéologie git.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Réaligne le paquet Debian (squelette hôte : 992 lignes de contrôleur de
fermentation à capteurs, aucun LXC) sur l'intention du portage OpenWrt, SANS
jeter l'existant : le contrôleur déménage dans le LXC et devient sbx-stillwatch,
étendu à la distillation.
Décisions : LXC Debian natif (pas Alpine, pas Docker) ; upstream picobrew_pico
(Pico S/C/Pro + Z + Zymatic) plutôt que le fork CyberMind limité au Zymatic ;
clone GitHub latest puis SHA figé, update explicite refusé pendant une session
(un changement upstream en plein brassage est inacceptable) ; drop-in DNS
picobrew.com actif par défaut, le cloud constructeur étant éteint depuis 2020 ;
héberger l'éprouvé (CraftBeerPi 4) et n'écrire que le manquant.
Limite de conception posée : stillwatch surveille et guide la distillation mais
n'actionne pas d'alambic — les coupes têtes/coeur/queues sont matérialisées et
tracées, la décision reste humaine. cuts.py est une fonction pure, testable sans
matériel.
Livraison en 3 phases utilisables seules, avec porte de réévaluation YAGNI sur
CraftBeerPi en phase 3. LXC alloué en 10.100.0.140 (motif appliances 1x0).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Neutralisation des pixels traceurs dans les emails entrants, en deux couches :
réécriture à la réception (data: URI 1x1 inline, zéro requête émise) avec
archivage du brut DKIM-intact, plus le filet MITM existant à l'ouverture.
Décisions actées : heuristique conservatrice (invisible OU domaine/chemin
connu), aucune réconciliation des compteurs entre couches, intégrité du
courrier non négociable (toute exception => remise de l'original intact).
Deux trous comblés en auto-revue : emplacement/déploiement du code (LXC mail,
pas l'hôte) et synchronisation hôte->LXC de learned-trackers.txt, sans laquelle
le partage de la liste entre couches était illusoire.
Signale au passage une anomalie hors périmètre : rspamd tourne mais n'est
câblé dans aucun milter Postfix (smtpd_milters vide) => ne filtre rien.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Correctif de fond du #ads affamé : rien n'empêchait autolearn de promouvoir les
régies pub en splice. Or splice = passthrough, et le feed de candidats (#662) ne
se déclenche que sur le chemin allow/mitm ("never the block 204 / splice
paths") — une régie splicée est donc structurellement inobservable, et la boucle
d'apprentissage s'étiole en silence.
Le piège est que les endpoints pub correspondent PARFAITEMENT à l'heuristique de
splice (hits >= SPLICE_MIN_HITS ET html_hits == 0 : ils ne servent jamais de
HTML et sont très sollicités). C'est ainsi que adform/rubicon/google-analytics
et consorts ont atterri dans splice-learned.txt et gelé l'apprentissage 9 jours.
_splice_feed() filtre désormais via _never_splice(), calqué sur le NEVER_LEARN
de #685 : graine des principaux réseaux pub/analytics, vérification sur l'hôte
ET son registrable, extensible par SECUBOX_NEVER_SPLICE sans changement de code.
2 tests (échouent sans la garde) ; 19 tests autolearn/ads passent.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
L'onglet #ads affichait "aucun tracker/pub détecté dans la fenêtre" alors que
67 hôtes réels (permutive, amplitude, byteintlapi, digiteka…) attendaient leur
promotion en base. Cause : ad_candidates est un BACKLOG (upsert host+site,
promu à AD_MIN_SITES sites distincts), pas une série temporelle — le fenêtrer
à `hours` masque tout dès que l'alimentation se tarit, rendant une boucle
d'apprentissage à l'arrêt invisible au lieu d'évidente.
ad_stats() bascule sur le cumul quand la fenêtre est vide et le signale via
candidates_cumulative ; le panneau l'étiquette explicitement ("cumul — rien de
neuf dans la fenêtre") pour ne pas faire passer un backlog ancien pour de la
détection récente.
Deux tests couvrent le repli et la non-régression du cas fenêtré (le premier
échoue en KeyError sans le correctif).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
HISTORY: entrées datées pour secubox-meshtastic (#897/PR #898/v2.15.0), les
correctifs de l'image rpi400 (deps auth, --kiosk CI, postinst mediaflow
#DEBHELPER#, prompts conffile) et la page wiki Support.
WIP: les deux chantiers passent en ✅ avec leurs follow-ups ; le blocage réseau
du rpi400 est documenté avec les deux pistes de reprise.
TODO: nouvelle section « Afficheurs & appliances » pour les trois sujets mis en
pause (réseau rpi400, reflash round eye, console double écran Pepper's ghost),
avec pour chacun le point de reprise exact.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Corrige la cause racine du login cassé sur l'image rpi400 (deps argon2/pyotp/
qrcode jamais tirées à cause de dpkg -i --force-depends), câble --kiosk dans
la CI, et corrige deux bugs latents révélés par l'apt-get strict du kiosk :
le token #DEBHELPER# dans un commentaire du postinst mediaflow (cassait
chaque install) et les prompts conffile non auto-répondus.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Two failures surfaced by the strict apt-get pass that --kiosk runs in the
rpi chroot (the lenient `dpkg -i --force-depends || true` had been swallowing
the first one on every build):
1. secubox-mediaflow postinst had `#DEBHELPER#` inside a comment. dh_installsystemd
substitutes that token textually wherever it appears, so it expanded the
systemd block mid-comment and orphaned "; kick one refresh" onto its own
line → `syntax error near ';'` → configure fails on EVERY install. Reword
the comment (real token stays alone on line 22). Bump to ~bookworm3.
2. The kiosk apt-get relied on DEBIAN_FRONTEND=noninteractive, which governs
debconf, not dpkg's conffile prompt — a pre-existing /etc/secubox/mesh.toml
triggered an interactive prompt that EOFs on the closed chroot stdin and
aborted the build. Pass --force-confdef/--force-confold (as the main
dpkg --configure pass already does) to auto-keep existing conffiles.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
secubox-auth/users crash at import on the rpi400 image because it is built
with `dpkg -i --force-depends` + debootstrap --include, so their declared
Depends (python3-argon2, python3-pyotp, python3-qrcode) are never pulled —
unlike normal apt installs on mochabin. The engine does `from argon2 import
...` / `import pyotp` / `import qrcode`, so the daemon dies at startup →
nginx 502 → no login. Add pyotp+qrcode (pure-Python) to INCLUDE_PKGS and
argon2 via the QEMU-proven pip step (mirroring the cryptography exclusion).
Also expose the kiosk GUI through CI: a `kiosk` workflow_dispatch boolean
→ build-image.sh --kiosk → forwarded to build-rpi-usb.sh, so a rebuilt
rpi400 image can ship the chromium fullscreen dashboard.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
uvicorn/uvloop refuses to rebind an existing UDS ('Address already in use') →
crash-loop on restart. ExecStartPre=+/bin/rm -f the socket (root, clears any
owner in the sticky /run/secubox) before ExecStart.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>