Two board-stability fixes verified live on gk2 during the assist deploy:
- secubox-aggregator-watchdog.sh: 480s startup grace so the auto-heal watchdog
stops restart-looping the aggregator during its slow (~5min) in-process cold
mount (RuntimeDirectoryPreserve stale socket → probe fails → kill mid-start).
- secubox-proxypac wpad-vhost: listen :9080 not :80 (HAProxy owns :80; the bind
conflict aborted every nginx reload, silently freezing config board-wide).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Livraison progressive d'artifacts (deb/image/www) par rings draft→internal→
published sur apt.secubox.in (reprepro distributions). Promotion + assignation
de ring par-box pilotées par un centre via grant capability=release (Centres &
Grants), honorées souverainement (issued_by==self_did). Ring published = profil
base auto. Actuateur repo reprepro-copy op-gated + actuateur box 4R fail-safe
(drop-in sources, apt échoué ne brique pas). apt GPG signé bout-en-bout.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
can_open() accepted mode=="standing" unconditionally, letting a center open a
session on the strength of the request alone. Per spec, standing mode is a
persistent authorization and must be backed by a real, self-issued delegation:
can_open() now additionally requires an active, SELF-issued (issued_by ==
self_did) capability="assist" Grant naming that request's center_did, via
grants.active_grants() — the sovereignty filter is already enforced there.
Missing grant -> (False, "no-standing-grant"). Per-incident mode is unchanged:
the box-authored request remains its own authority.
ref sous-projet 2
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Consolidated fix wave from the whole-branch review of feat/assist-request:
- daemon.py: self_did is resolved from a PUBLIC source (SECUBOX_SELF_DID env,
else world-readable /etc/secubox/annuaire/node.did), cached once at module
load instead of re-derived per connection. The WS daemon (User=secubox-assist,
not in group secubox) previously opened the sovereign private key on every
connection and got PermissionError, silently authorizing zero sessions.
postinst now derives node.did once from the key as root (guarded: a missing
key does not fail the install), writing only the file world-readable —
the shared /etc/secubox/annuaire parent is left untouched.
- daemon.py: the mid-session recheck now requires active_session().session_id
to match the session this socket originally authorized, not merely "some
session is active" — closing session s1 and opening s2 no longer lets a
socket authorized under s1 keep running catalog actions under s2's window.
- secubox-assist.service: NoNewPrivileges=false — catalog.resolve() dispatches
service/config actions as `sudo -n secubox-assistctl ...` run by this unit;
NNP=true silently blocked sudo's setuid transition for every one of them.
secubox-assist-api.service is unaffected (it never sudoes).
- www/assist/index.html + changelog: console-grant/revoke buttons are disabled
and relabelled "(à venir)" — the control-plane (ops, guard, keystroke audit)
is real and tested, but no pty-over-WS channel consumes it yet; ship that
honestly instead of a dead-looking-live button.
ref sous-projet 2
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Four review findings on the packaging added in 857a1fe9:
- nft/secubox-assist.nft (was zz-secubox-assist.conf): the standalone table
used `policy drop` on the input hook — any base chain's terminal drop at
that hook blackholes the whole board (SSH, webui) the instant postinst
loads it, and /etc/secubox/nft.d/ is never boot-included so it also
vanished on reboot. Now `policy accept` (self-contained, own chains,
mirrors secubox-toolbox's nftables.d/secubox-toolbox-wg.nft) and shipped
to /etc/nftables.d/, which /etc/nftables.conf actually globs in at boot
(see secubox-vortex-firewall's postinst).
- apparmor/secubox-assist: profiled the bare /usr/bin/python3 interpreter,
which would confine every python invocation on the box, including this
package's own units and every other secubox module. No module in the repo
confines a python daemon this way (secubox-macro keys its profile to a
dedicated launcher script, secubox-macroctl, not the interpreter) and
assist has no such launcher yet, so the profile is removed; confinement is
deferred until a dedicated launcher exists for the WS daemon.
- sudoers/secubox-assist: granted `secubox`, but catalog.py's privileged
argv runs via wsserver.dispatch from assist.daemon.handler, which is
invoked under secubox-assist.service's own user (secubox-assist), not the
webui/API's secubox user — every service.*/config.* action was silently
denied by sudo -n. Principal corrected, still scoped to
/usr/sbin/secubox-assistctl only.
- assist/daemon.py: handler() called wsserver.dispatch (sync subprocess.run,
up to 60s, plus diag.collect's chained sync subprocess calls) directly on
the single asyncio loop, stalling every other session. Offloaded via
asyncio.to_thread, along with the per-message journal re-reads.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Sidebar renderer reads item.name and hub API reads item.category —
the assist menu shipped with label/group so the sidebar showed
"undefined" and misfiled it under Other. Matches 570-centers.json
schema (federation/mesh module bucket). Test now asserts name is a
non-empty string and category is one of the valid CATEGORY_META
buckets.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
cmd_request called verbs.assist_request with no try/except, so a bad
--mode or --scope let pydantic's ValidationError escape as a raw
traceback on stderr instead of the {"error": ...} JSON contract every
other mutating subcommand honors. Wrapped it the same way cmd_open and
cmd_accept already do (ValidationError subclasses ValueError). Added
the same defensive wrap to cmd_close, cmd_console_grant and
cmd_console_revoke for consistency across all mutating subcommands.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
annuaire.assist.active_session() raises AssistError when the journal
holds more than one active session (invariant breach). That exception
propagated uncaught out of authorize(), which would reach the WebSocket
serve loop as an unexpected type instead of a clean, auditable
rejection. Catch it and re-raise as AuthError, preserving the cause.
Also corrects wsserver docstrings that implied a console gate in this
file — console escalation is handled by the separate console module.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Adversarial re-review found two redaction leaks in assist/diag: WireGuard
private keys in journalctl output (interface/env/JSON key styles) and
DSN passwords embedded in connection URIs (user:pass@host) were passed
through unredacted. Extend _SECRET_KV's keyword alternation with
private-key/key and add a dedicated URI-credential rule that masks only
the password segment.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
_SECRET_KV used a leading \b before the keyword group, but \b treats
underscore as a word char, so access_token/refresh_token/client_secret
and other snake_case compound field names were never matched and their
values leaked in the clear. Replace the leading \b with a lookbehind
that also treats '_' as a separator.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Bearer/Basic credentials survived redaction because the value-match
stopped at the first space (consuming only the scheme word) and a
bare "Bearer <token>" line with no ":"/"=" separator wasn't matched
at all. JSON-quoted pairs like {"password": "..."} also slipped
through since the keyword's closing quote sat between the key and
the colon. _SECRET_KV now tolerates an optional closing quote after
the keyword, a colon/equals-or-whitespace separator, and an optional
Bearer/Basic prefix before consuming the credential.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
pending_requests() ignored self_did entirely, surfacing every not-yet-
accepted ASSIST_REQUEST regardless of author. can_open() only checked
existence/acceptance/no-active-session, letting a per-incident request
issued_by a federated peer be opened locally. Both now require
issued_by == self_did OR mode == "standing" (standing-grant check stays
the caller's job); can_open returns (False, "not-authorized") otherwise.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
/connections only consulted ssl_domain (unset in secubox.conf [nextcloud],
which carries `domain`) and fell back to http://localhost:8080 — unreachable
from a phone/desktop client. The panel independently hardcoded localhost,
ignoring the web_url the API already resolves.
Add a pure _public_base_url() helper (ssl_domain > public domain > localhost),
reuse it in both /connections and /status, and render status.web_url in the
panel. Regression tests cover the domain-set, ssl-precedence, placeholder,
and bare-host cases.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Task 10: ship the Centres & Grants substrate built in tasks 1-9.
- debian/rules installs sbin/sbx-centersctl (0755), www/centers/ (panel),
menu.d/570-centers.json, and a new nginx/centers.conf static route for
/centers/ (reuses the existing /api/v1/annuaire/ API route, so no new
backend listener). annuaire/grants.py, config_compose.py and
config_router.py already ship via the existing `cp -r annuaire/*` install
of the annuaire/ library.
- debian/control: add python3-tomli-w (annuaire.config_compose composes
TOML layers with tomli_w.dumps).
- Reconcile the box signing key: sbx-centersctl previously defaulted to its
own ANNUAIRE_KEY at /etc/secubox/annuaire/box.key, a second key file never
provisioned anywhere. The box's actual sovereign identity is the node key
annuairectl already establishes at /etc/secubox/secrets/annuaire/node.key
(env ANNUAIRE_KEY_PATH), also used by api/main.py's own
_publish_self_node/_self_did_best_effort. Unified sbx-centersctl and
api/main.py on ANNUAIRE_KEY_PATH / node.key -- one box, one identity. The
CLI still never generates a key (annuairectl init remains the sole
provisioning path).
- No sudoers added: sbx-centersctl runs subprocess-direct (no sudo) under
the same `secubox` user as secubox-annuaire.service, and only touches
paths that user already owns (journal, node key, /etc/secubox) -- unlike
secubox-proxypac's ctl, it never needs root.
- changelog bump 0.4.0 -> 0.5.0 (feature release).
- Full package suite (275 tests) passes; dpkg-buildpackage -us -uc -b
produces a clean secubox-annuaire_0.5.0-1~bookworm1_all.deb with all new
files present and no build cruft (__pycache__, debian/secubox-annuaire/)
leaking into the tree.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Ajoute le panneau webui /centers : matrice de propriété scope×calque
(baseline/override/local), formulaire d'octroi, liste des centres
accordés, section propositions (pushs non-accordés, acceptation en un
clic) et diff effectif/local par scope. Consomme l'API /centers de la
tâche 8, avec Authorization: Bearer sbx_token sur chaque appel (les
endpoints mutants sont JWT-gated). Aucune donnée d'API n'est jamais
interpolée dans un attribut d'événement inline (leçon proxypac) : les
actions de ligne (révoquer, accepter) passent par data-* + un listener
délégué. Test garde XSS renforcé copié de
secubox-toolbox/tests/test_rlevel_panel.py (détecte la concaténation,
pas seulement ${}).
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
Task 8. GET /centers, /centers/ownership, /centers/proposals, and
/centers/effective/{scope} resolve in-process from the journal
(annuaire.grants.active_grants / annuaire.config_router.route_config) —
public reads, no privileged action. POST /centers/grant, /centers/revoke,
and /centers/proposal/accept never hold the box signing key in-process:
they delegate to sbx-centersctl via a fixed-argv subprocess call
(_centers_ctl, overridable via SBX_CENTERSCTL), JWT-gated.
route_config() gains an `apply` kwarg (default True, existing callers
unaffected) so the read-only proposals/effective endpoints can collect the
same composed candidates and proposals a real route would produce without
ever calling apply_composed — nothing on disk is touched by those GETs.
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>