Commit Graph

3143 Commits

Author SHA1 Message Date
7807772e94 docs(fleet-metrics): implementation plan (5 tasks) (ref fleet-metrics)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 07:59:14 +02:00
526da7bea4 docs(fleet-metrics): transport=dedicated last-wins store + :8799 mesh pull (journal is immutable-chained, can't hold periodic snapshots)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 07:55:49 +02:00
f82078806b docs(fleet-metrics): design — centralized+meshed node snapshots (trilogy 3/3)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 07:50:30 +02:00
b2005dd44b docs: WIP/HISTORY — release-rings + assist fixes + p2p-ephemeral deployed (trilogy 1-2)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 07:20:13 +02:00
3159e132ba Merge feat/p2p-ephemeral-cli: ephemeral-peer CLI + assist escalation foundation (#911)
secubox-p2pctl + wg-ephemeral iface (10.11.0.0/24, udp/51825), session-scoped
peers auto-revoked via TTL backstop; assist join execs via sudo -n. opus review
MERGEABLE. secubox-p2p 1.11.0 + secubox-assist 0.2.3.
2026-07-27 07:16:49 +02:00
cf06d9dd01 build(p2p): package ephemeral CLI — sweep units, scoped sudoers, nft udp/51825; assist 0.2.3 join exec
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:50:01 +02:00
1c2e43bd99 feat(assist): join redeems a link by exec'ing secubox-p2pctl via sudo -n (was builders-only)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:42:28 +02:00
a99c5f59dc fix(p2p): secubox-p2pctl save() inside try (JSON error not traceback) + honor --ttl 0
- Move ephemeral.save() into try/except in all 5 command handlers (iface-up,
  peer-add, peer-del, ephemeral-revoke, sweep) to catch save-time OSError
  (missing dir, perms, disk full) and return JSON error instead of raw traceback.
- Honor --ttl 0 (immediate expiry) by explicit None check instead of silent
  fallback to DEFAULT_TTL_S; clamp negatives to 0.
- Add regression test proving save-failure surfaces as JSON, not traceback.

Fixes: review feedback from feat/p2p-ephemeral-cli

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:38:56 +02:00
cda9c116bd feat(p2p): secubox-p2pctl — iface-up/peer-add/peer-del/ephemeral-revoke/sweep (argv, guards, DRYRUN)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:32:21 +02:00
129e9a3e0c fix(p2p): ephemeral.py fail-closed on non-string ts + fresh load() dict
Fix 1: load() now returns {"boot_id": None, "peers": []} directly instead
of returning dict(_EMPTY), preventing shared mutable list between failed loads.
Removed now-unused _EMPTY constant.

Fix 2: expired() now checks isinstance(ts, str) before len(ts) to fail-closed
on non-string expires_ts (malformed on-disk data treated as expired, never crash).

Fix 3: Added 3 regression tests for boundary conditions:
- test_in_range_excludes_network_and_broadcast: verify network/broadcast excluded
- test_expired_failclosed_on_non_string_ts: verify non-string ts swept safely
- test_load_missing_returns_independent_dicts: verify no mutable list sharing

ref #p2p-ephemeral

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:29:28 +02:00
72625ece37 feat(p2p): ephemeral.py — pure registry + guards for wg-ephemeral peers (ref p2p-ephemeral)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:24:12 +02:00
517e5f67fc docs(p2p-ephemeral): implementation plan (4 tasks) (ref p2p-ephemeral)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:22:30 +02:00
4041b65b62 docs(p2p-ephemeral): design — ephemeral-peer CLI + escalation foundation
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-27 06:11:38 +02:00
github-actions[bot]
39060cbdde ci: Update metrics [skip ci] 2026-07-26 09:23:48 +00:00
964bf3f5d3 fix(assist): python3-websockets Recommends not Depends (pip-websockets boards)
A hard Depends blocked dpkg install on gk2, where python3-websockets was
removed so pip can provide websockets 16.0 (uvicorn version skew). Recommends
installs it on normal apt boards without blocking dpkg -i where it's absent.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 10:35:52 +02:00
0980c0be79 Merge remote-tracking branch 'origin/master' 2026-07-26 10:25:10 +02:00
f8359f15f2 Merge branch 'feat/assist-dual'
# Conflicts:
#	packages/secubox-annuaire/annuaire/model.py
#	packages/secubox-annuaire/annuaire/verbs.py
#	packages/secubox-annuaire/debian/changelog
2026-07-26 10:23:21 +02:00
github-actions[bot]
1feabca712 ci: Update metrics [skip ci] 2026-07-26 08:16:39 +00:00
9776112de1 fix(assist): join link defaults to the public hub URL, not hostname.local
The joinlink base_url defaulted to https://assist.local (LAN-only), so a
shared assistance join link was unreachable over the internet. Derive the
public hub from [api].sso_cookie_domain in secubox.conf
(.gk2.secubox.in -> https://admin.gk2.secubox.in); ASSIST_BASE_URL/--base-url
still override; falls back to assist.local only when no domain is configured.
Same public-base-URL pattern as the Nextcloud _public_base_url fix.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 10:16:24 +02:00
06b6e0b8c1 build(annuaire): 0.8.1 — ship the assist session-resolver author-binding fix
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 09:36:57 +02:00
c6463bc63a Merge fix/assist-socle-session-author-binding: bind verified author in session resolvers (#910)
Closes phantom-session DoS: assist resolvers bound to entry.author not
payload issued_by. Same author-vs-payload class as active_grants.

# Conflicts:
#	packages/secubox-annuaire/annuaire/grants.py
2026-07-26 09:35:47 +02:00
ee53353037 Merge feat/release-rings: progressive release rings (#909)
Center-driven artifact delivery draft→internal→published; secubox-release
0.1.0 + annuaire 0.8.0. Final review fixed author-vs-payload sovereignty at
active_grants + current_ring.
2026-07-26 09:34:53 +02:00
59b6e7a31a fix(annuaire): assist session resolvers bind verified author, not payload issued_by
The socle assist resolvers (active_session/console_active/pending_requests/
can_open) filtered sovereignty on payload[issued_by] — attacker-controllable
— instead of the authenticated entry.author. import_entries federates ANY
validly-signed op, so a mesh peer could sign ASSIST_SESSION_OPEN{issued_by=
victim} with its own key; the victim's active_session then counted it as its
own → phantom session trips the single-session invariant → DoS of real
sessions. Same author-vs-payload class fixed in active_grants (release-rings).

Bind self-scoped ops (SESSION_OPEN/CLOSE, CONSOLE_GRANT/REVOKE) to
_author == self_did (sovereign), and ASSIST_REQUEST to _author == issued_by
(self-certifying), fail-closed on missing author. Add _author accessor to
grants.py. Repair test fixtures to carry author (Journal.append always sets
it from the signing key; every assist op is self-certifying).

Follow-up: ASSIST_ACCEPT is not yet author-bound to the request's center_did
(distinct cross-op vector; accept payload has no issued_by).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 09:17:28 +02:00
756ec56862 docs(release-rings): implementation plan (9 tasks) (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:51:22 +02:00
44bc14ce11 fix(annuaire): current_ring sovereignty-filtered by verified grant-holding author (ref release-rings)
current_ring counted ANY author's RELEASE_PROMOTE/DEMOTE; sync-repo drives
reprepro copy off it, so a federated promote by an ungranted center would
publish fleet-wide. Like box_ring, count a ring op only when its verified
author holds an active capability=release grant from the honoring box.

Add current_ring(entries, evo_id, self_did=None): fail-closed author+grant
gate when self_did is given, unfiltered back-compat when None. Add public
published_evo_ids() and thread self_did through evolutions_in_ring. Callers
pass the sovereign self: verbs.release_promote/demote pass self_did; releasectl
cmd_list/cmd_sync_repo gain _self_did_public() (public did, no private key on
read paths); api /evolutions passes _self_did(). Read paths now use
published_evo_ids() instead of reaching into grants._op/_payload. Tests cover
ungranted promote ignored, granted promote counted, and author-less ignored.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:47:45 +02:00
c7f92033cd fix(annuaire): active_grants binds verified author, not payload issued_by (sovereignty) (ref release-rings)
A GRANT_ISSUE is self-certifying (model.py Grant: entry.author == issued_by).
Grant ops federate via dir_sync, so a mesh peer can validly sign a GRANT_ISSUE
whose payload lies "issued_by=VICTIM" while author=ATTACKER; on import it is
well-formed and correctly signed. active_grants honored the payload's claimed
issued_by, letting a victim believe it delegated authority it never signed.

Add a fail-closed author check in the grant_issue branch: skip unless the
verified _author(entry) equals payload issued_by (missing/None author -> unequal
-> skipped). Repair bare-dict fixtures to carry author == issued_by (reflects
Journal.append reality). Regression test proves the forged release grant is not
honored and has_release_grant(ATTACKER, VICTIM) is False.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:47:21 +02:00
0124197c23 build(release): packaging — API unit, scoped sudoers, install, secrets traversal; annuaire ships releases.py
secubox-release-api.service (User=secubox, /run/secubox/release.sock,
RuntimeDirectoryPreserve=yes, NoNewPrivileges=true — the API execs
secubox-releasectl directly, never via sudo), sudoers/secubox-release
scoped to /usr/sbin/secubox-releasectl only, debian/secubox-release.install
wiring every artifact (release/*.py, api/*.py, sbin, www/releases, menu.d,
nginx dropin, systemd unit, sudoers), and postinst enabling the API service
+ guarded traversal-only chmod o+x on the shared /etc/secubox/secrets
parent (same fix secubox-assist shipped in 0.2.1) — never chowning any
shared parent. secubox-annuaire bumped to ship annuaire/releases.py, the
ring resolver secubox-release consumes.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:25:17 +02:00
9188f8f62e fix(release): menu requires the owning package, not annuaire
590-releases.json belongs to secubox-release; requires should gate the
sidebar entry on that package being installed, matching the sibling
convention (secubox-appstore/secubox-p2p menu entries require themselves).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:15:57 +02:00
ad93adebac feat(release): /releases panel (evolution×ring + box×ring matrices) + menu + vhost
Operator webui for the release-rings control plane: promote/demote per
evolution, a publish form (artifacts JSON + notes), and a box×ring assign
form, all delegating to the /api/v1/release/* endpoints. Event delegation
only (no inline onclick), API-derived strings via textContent/dataset only
(no innerHTML) per the WEBUI-PANEL-GUIDELINES. Sidebar entry at /releases
(category mesh) and the nginx vhost (static alias + proxy to
release.sock via the proven rewrite + secubox-proxy.conf snippet pattern).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:15:22 +02:00
5b516ef06a feat(release): /releases API (JWT, reads in-process, writes via ctl)
GET /status and /health stay public; /evolutions, /box-ring, /publish,
/promote, /demote, /assign require JWT. Reads resolve current_ring/
box_ring in-process from the annuaire journal; every mutation shells
out to secubox-releasectl via argv list (never a shell string). self_did
resolves from the public node.did file (SECUBOX_SELF_DID env or
/etc/secubox/annuaire/node.did) — the private signing key is never read
by the API.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 08:07:36 +02:00
4a9332905b feat(release): secubox-releasectl (publish/promote/demote/assign/list/sync-repo/apply, DRYRUN)
Center/repo/box CLI over annuaire.verbs.release_* + release/repo.py +
release/boxapply.py, mirroring secubox-assistctl's conventions (_key/_journal/
_self_did/_die/_dry, argparse subparsers, ValueError -> {"error": ...} exit 1).
sync-repo and apply are DRYRUN-guarded since reprepro/apt-get are not present
in test environments; secubox-release-repo is a thin exec wrapper for cron/timers.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:58:50 +02:00
0c195c8701 fix(release): boxapply validate-first + rollback-only-on-success, no leaked temp files
Rewrite apply_4r() to call sources_line(ring) first (fail-closed on invalid ring,
no stray .shadow file). Only create .rollback copy after successful validation,
leaving no temp files on failure path. Removes unused shutil import. Adds 3
regression tests: invalid_ring_no_shadow, failure_no_rollback, and fn_raise_failclosed.

6 boxapply tests + 2 repo tests (8 total) PASSING.

Ref release-rings

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:53:08 +02:00
50367801f0 feat(release): box actuator — 4R ring sources drop-in (apt-fail rolls back, never bricks) (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:47:29 +02:00
a5e5bd4dbe feat(release): package scaffold + reprepro-copy actuator (arm64-guard, op-gated argv) (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:42:25 +02:00
0b1cff48a1 feat(annuaire): release_publish/promote/demote/ring_assign grant-gated verbs (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:36:50 +02:00
5cb2b22d26 fix(annuaire): box_ring fail-closed on unverified author + regression test (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:33:26 +02:00
498cbf663b feat(annuaire): releases.py — pure resolver (ring state, box ring, sovereignty) (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:28:04 +02:00
5ea74bee75 feat(annuaire): release ops + Evolution/RingState/RingAssign models (ref release-rings)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:23:09 +02:00
56e592def2 Merge remote-tracking branch 'origin/master' into feat/release-rings 2026-07-26 07:07:12 +02:00
aec4353657 fix(assist): client-side guards for required reason/center on request forms
Both request forms require a non-empty reason (and the targeted one a center
DID); guard client-side with a friendly notify() message instead of letting the
raw pydantic 'string_too_short' validation error surface. Mark motif required.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:05:33 +02:00
fd8a68fc4c fix(assist): postinst grants ctl traversal to the node signing key (0.2.1)
The assist ctl runs as `secubox` (invoked by the API) and must read
/etc/secubox/secrets/annuaire/node.key to sign journal ops, but the shared
/etc/secubox/secrets parent is root:secubox-toolbox 0750 which secubox can't
traverse — so every mutating assist op (offer/request/session) failed with a
Permission denied the panel swallowed. postinst adds o+x (traversal-only) to
the parent; subdirs keep their 0700. Fixes the socle mutating path too.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:01:29 +02:00
6692a8c3cd fix(assist): marketplace actions show success/error feedback (was silent)
The offer/request-open/match-accept/joinlink POSTs swallowed failures with no
r.ok check, so a backend error (e.g. the ctl unable to sign) or a missing JWT
showed 'nothing' to the operator. Add a non-blocking notify() banner + a
postAction() helper that surfaces the API error detail (and a clear 401/403
'not authenticated' message), plus empty-tag guards. textContent only (XSS-safe).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 07:00:11 +02:00
f01e7ec21d fix(assist): action-oriented marketplace labels (Proposer/Demander/Partager)
The tabs used abstract labels (Disponibilité/Inviter) that didn't map to what
the operator wants to do — publish an offer, launch an open request, share an
invite link. Relabel tabs+buttons+headers to action verbs with emoji + inline
hints explaining each; data-tab/data-act attributes unchanged (JS intact).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-26 06:52:05 +02:00
f170fc3535 fix(assist): readable pending list, clarify targeted vs open request, poll active tab only
- Journal de session renders raw JSON.stringify([]) when empty; replace
  with an "Aucune demande en attente." empty-state and readable per-item
  rows (DID/mode/scope/reason) built via textContent/createElement.
- Rename section header to "Demandes en attente" to match its content.
- Add short muted hints under the targeted (socle) and open (marketplace)
  request forms so the operator understands which one to use.
- refresh() only re-fetches the active marketplace tab's endpoint on the
  5s interval instead of all three; tab-switch still triggers an
  immediate fetch.
- Guard dataset[k] assignment against undefined/null so buttons never
  get a literal "undefined" data attribute.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 18:39:24 +02:00
d590aa4ef3 harden(assist): author_prefix 48-bit → 128-bit (id namespacing collision-resistant)
The id-shadowing consent-forgery fix rests on author_prefix being
collision-resistant; a 48-bit prefix was brute-forceable in days on a cluster
(targeted collision → reopen the shadow forge once the live join-path escalates
a forged match). Widen to 128-bit; ids auto-propagate via author_prefix().

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 18:36:28 +02:00
22354f83da fix(annuaire): close assist-dual id-shadowing consent-forgery via author-self-certifying ids
offer_id/req_id were free-form attacker-choosable strings, so a signed mesh
peer could publish a SHADOW ASSIST_OFFER carrying a victim's exact offer_id;
the matcher's offers_by_id map (keyed by bare id) collapsed onto the shadow
(last-writer-wins), its own author-check passed trivially, and match_ready
could return True for the victim's real match without the victim ever
accepting.

Ids are now author-self-certifying: admission (active_offers/
active_open_requests, and offer-revoke) requires offer_id/req_id to carry a
12-hex prefix derived from the author's own DID (author_prefix/
_id_matches_author), making ids globally unique per author. secubox-assistctl
mints ids under the box's own prefix instead of bare "off-"/"orq-" strings.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 18:29:19 +02:00
CyberMind
f6bc3d5115
Merge pull request #907 from CyberMind-FR/fix/aggregator-watchdog-grace-and-wpad-port
Some checks failed
License Headers / check (push) Has been cancelled
fix: aggregator-watchdog startup grace + wpad-vhost :9080 (board-stability, backport gk2)
2026-07-25 18:13:30 +02:00
4396fb46f3 fix(annuaire): match_ready binds accepts to mid's own offer/req ids
An ASSIST_MATCH_ACCEPT credited a side by checking only that the
accept's author owns the offer_id/req_id IT references — never that
those ids are the ones hashing to the target mid. Attacker C could post
his own active offer o2 + request r2 (passing the author-binding
checks), then submit both accept sides carrying match_id=mid (A's
offer + B's request) while pointing offer_id/req_id at o2/r2, forging
mutual consent for a rendezvous he was never party to.

match_ready now requires match_id(accept.offer_id, accept.req_id) ==
mid before crediting either side, so an accept can only ever speak for
the exact pair that produces the mid it targets.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 17:53:44 +02:00
007bc6a631 fix(annuaire): bind assist-dual matcher to signing author, not payload claims
active_offers/active_open_requests/match_ready trusted attacker-controlled
payload fields (issued_by, side) instead of the authenticated entry author,
letting any signed mesh member revoke someone else's offer, forge a mutual
match-accept alone, or spoof issued_by onto a victim's DID. Bind every
admission check to entry.author, mirroring the existing socle resolvers in
assist.py/grants.py.

Also make matches() fail-closed on offers/requests missing their id
(previously raised KeyError instead of skipping the malformed entry).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 17:47:24 +02:00
be277d951e build(assist): package marketplace+escalate (0.2.0) + annuaire assist_match (0.7.0)
secubox-assist.install already globs assist/*.py so rendezvous.py/
joinlink.py/escalate.py ship without changes; secubox-annuaire's
`cp -r annuaire/*` already covers assist_match.py — both verified via
dpkg-deb -c on the built .debs. Added secubox-p2p to Depends (the
escalate flow's ephemeral peer add/revoke goes through it) and a
guarded, idempotent postinst block that provisions the wg-ephemeral/
10.11.0.0/24 range when secubox-p2p exposes the CLI for it, and
no-ops with a breadcrumb note otherwise — secubox-p2p has no
ephemeral-iface CLI yet, so this never invents one. Extended
test_packaging_dual.py to lock in shipped modules, the p2p Depends,
the still-scoped sudoers entry, and that postinst never touches the
shared /etc/secubox parent.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-25 17:33:01 +02:00