From cf06d9dd016a47f51945859584e55e707779fafc Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 27 Jul 2026 06:50:01 +0200 Subject: [PATCH] =?UTF-8?q?build(p2p):=20package=20ephemeral=20CLI=20?= =?UTF-8?q?=E2=80=94=20sweep=20units,=20scoped=20sudoers,=20nft=20udp/5182?= =?UTF-8?q?5;=20assist=200.2.3=20join=20exec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Gerald KERMA --- packages/secubox-assist/debian/changelog | 15 ++++ .../tests/test_packaging_dual.py | 4 +- packages/secubox-p2p/debian/changelog | 28 +++++++ packages/secubox-p2p/debian/postinst | 35 +++++++++ packages/secubox-p2p/debian/rules | 21 ++++++ .../secubox-p2p/nft/secubox-p2p-ephemeral.nft | 21 ++++++ .../secubox-p2p/sudoers/secubox-p2p-ephemeral | 5 ++ .../secubox-p2p-ephemeral-sweep.service | 10 +++ .../systemd/secubox-p2p-ephemeral-sweep.timer | 10 +++ .../tests/test_packaging_ephemeral.py | 75 +++++++++++++++++++ 10 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 packages/secubox-p2p/nft/secubox-p2p-ephemeral.nft create mode 100644 packages/secubox-p2p/sudoers/secubox-p2p-ephemeral create mode 100644 packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.service create mode 100644 packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.timer create mode 100644 packages/secubox-p2p/tests/test_packaging_ephemeral.py diff --git a/packages/secubox-assist/debian/changelog b/packages/secubox-assist/debian/changelog index 81893d03..d8f72f69 100644 --- a/packages/secubox-assist/debian/changelog +++ b/packages/secubox-assist/debian/changelog @@ -1,3 +1,18 @@ +secubox-assist (0.2.3-1~bookworm1) bookworm; urgency=medium + + * feat(escalate): the `join` path now actually execs secubox-p2pctl — + escalate.py's builders (peer-add/ephemeral-revoke argv, --did carried + through) are run via `sudo -n /usr/sbin/secubox-p2pctl ...` under the + scoped sudoers entry secubox-p2p now ships + (sudoers/secubox-p2p-ephemeral, Depends already present). Previously + the CLI didn't exist upstream, so escalation failed closed; the + guarded ephemeral-init postinst breadcrumb + (/etc/secubox/assist/EPHEMERAL-RANGE.note) is now moot on boards + running an updated secubox-p2p, since the CLI's own postinst + provisions the wg-ephemeral key/iface/sweep-timer directly. + + -- Gerald KERMA Mon, 27 Jul 2026 11:15:00 +0200 + secubox-assist (0.2.2-1~bookworm1) bookworm; urgency=medium * packaging: move python3-websockets from Depends to Recommends. The WS diff --git a/packages/secubox-assist/tests/test_packaging_dual.py b/packages/secubox-assist/tests/test_packaging_dual.py index 92e90c55..9ba2543f 100644 --- a/packages/secubox-assist/tests/test_packaging_dual.py +++ b/packages/secubox-assist/tests/test_packaging_dual.py @@ -69,6 +69,6 @@ def test_postinst_ephemeral_block_only_touches_its_own_subdirectory(): assert "chmod -R /etc/secubox" not in post -def test_changelog_bumped_to_0_2_2(): +def test_changelog_bumped_to_0_2_3(): changelog = (ROOT / "debian" / "changelog").read_text() - assert changelog.startswith("secubox-assist (0.2.2-1~bookworm1)") + assert changelog.startswith("secubox-assist (0.2.3-1~bookworm1)") diff --git a/packages/secubox-p2p/debian/changelog b/packages/secubox-p2p/debian/changelog index 3ef4249d..b825be4d 100644 --- a/packages/secubox-p2p/debian/changelog +++ b/packages/secubox-p2p/debian/changelog @@ -1,3 +1,31 @@ +secubox-p2p (1.11.0-1~bookworm1) bookworm; urgency=medium + + * feat: session-scoped ephemeral WireGuard peer CLI (secubox-p2pctl) on a + dedicated wg-ephemeral iface (10.11.0.0/24, udp/51825) — the only + privileged surface secubox-assist's escalate flow sudos into + (peer-add/peer-del/ephemeral-revoke/iface-up/sweep; guards fail closed + on out-of-range or non-ephemeral requests; ephemeral peers never enter + the persistent gondwana mesh state; boot_flush() tears down/rebuilds + the iface if the kernel rebooted since the registry was last written). + * packaging: new secubox-p2p-ephemeral-sweep.service/.timer (root + oneshot, OnUnitActiveSec=60s) — a TTL backstop that removes expired + ephemeral peers even if the assist-side session teardown never runs. + New scoped sudoers/secubox-p2p-ephemeral (secubox ALL=(root) NOPASSWD: + /usr/sbin/secubox-p2pctl — exact path, no wildcard). New own-table nft + drop-in (inet secubox_p2p_ephemeral, policy accept, udp dport 51825) + shipped to /etc/nftables.d/, reload-on-install like secubox-assist's. + * postinst: install -d /var/lib/secubox/p2p (root-owned, never chowns + the shared parent); generates /etc/secubox/secrets/p2p/wg-ephemeral.key + once via `wg genkey` if absent (0600, guarded — wg may be absent in a + build container); runs `secubox-p2pctl iface-up` guarded; enables the + sweep timer; reloads nftables to pick up the new drop-in. + * Cross-package note: the Freebox/router UDP 51825 forward is an + out-of-package deploy prerequisite for a remote center to actually + reach wg-ephemeral live (mirrors the existing wg-mesh 51822 forward); + the nft INPUT allow itself ships in this package. + + -- Gerald KERMA Mon, 27 Jul 2026 11:00:00 +0200 + secubox-p2p (1.10.0-1~bookworm2) bookworm; urgency=medium * Phase 2: Requires=secubox-core.service -> Wants= on this module's unit diff --git a/packages/secubox-p2p/debian/postinst b/packages/secubox-p2p/debian/postinst index 2d4f8ab9..353ac759 100755 --- a/packages/secubox-p2p/debian/postinst +++ b/packages/secubox-p2p/debian/postinst @@ -72,6 +72,41 @@ case "$1" in if systemctl is-active --quiet nginx; then nginx -t >/dev/null 2>&1 && systemctl reload nginx || true fi + + # --- ephemeral wg-ephemeral iface prerequisites (Task 4) ---------- # + # State dir for the ephemeral peer registry (api/ephemeral.py). Root + # owned — secubox-p2pctl (the only writer) runs as root via the + # sweep timer / sudoers, never chown the shared /var/lib/secubox + # parent (#494/#511). + install -d /var/lib/secubox/p2p + + # Generate the wg-ephemeral private key once if absent. Guarded: + # `wg` may be unavailable in a build/CI container, and this must + # never fail the package install. + install -d -m 700 /etc/secubox/secrets/p2p + if [ ! -f /etc/secubox/secrets/p2p/wg-ephemeral.key ]; then + (umask 077; wg genkey > /etc/secubox/secrets/p2p/wg-ephemeral.key) 2>/dev/null || true + fi + chmod 600 /etc/secubox/secrets/p2p/wg-ephemeral.key 2>/dev/null || true + + # Bring the wg-ephemeral iface up idempotently. Guarded: no wg/root + # in a build container, and the CLI itself no-ops safely on repeat. + /usr/sbin/secubox-p2pctl iface-up 2>/dev/null || true + + # Enable the TTL-backstop sweep timer (removes expired ephemeral + # peers even if the assist-side teardown never runs). + systemctl daemon-reload || true + systemctl enable --now secubox-p2p-ephemeral-sweep.timer 2>/dev/null || true + + # Durable udp/51825 nft allow (own base chain, policy accept — see + # nft/secubox-p2p-ephemeral.nft for why it can never drop unrelated + # traffic). dpkg already placed the conffile at + # /etc/nftables.d/secubox-p2p-ephemeral.nft; reload nftables.service + # so it picks up the include, falling back to a direct load. + if systemctl is-active --quiet nftables.service 2>/dev/null; then + systemctl reload nftables.service 2>/dev/null \ + || nft -f /etc/nftables.d/secubox-p2p-ephemeral.nft 2>/dev/null || true + fi ;; esac diff --git a/packages/secubox-p2p/debian/rules b/packages/secubox-p2p/debian/rules index 91375245..f00870d9 100755 --- a/packages/secubox-p2p/debian/rules +++ b/packages/secubox-p2p/debian/rules @@ -47,3 +47,24 @@ override_dh_auto_install: # Create runtime directory install -d $(CURDIR)/debian/secubox-p2p/run/secubox + + # Install ephemeral CLI (root-only surface for wg-ephemeral, Task 2) + install -d $(CURDIR)/debian/secubox-p2p/usr/sbin + install -m 755 $(CURDIR)/sbin/secubox-p2pctl $(CURDIR)/debian/secubox-p2p/usr/sbin/ + + # Install ephemeral sweep timer/service (TTL backstop, Task 4) + install -d $(CURDIR)/debian/secubox-p2p/lib/systemd/system + install -m 644 $(CURDIR)/systemd/secubox-p2p-ephemeral-sweep.service \ + $(CURDIR)/debian/secubox-p2p/lib/systemd/system/ + install -m 644 $(CURDIR)/systemd/secubox-p2p-ephemeral-sweep.timer \ + $(CURDIR)/debian/secubox-p2p/lib/systemd/system/ + + # Install scoped sudoers (secubox -> secubox-p2pctl, exact path only) + install -d $(CURDIR)/debian/secubox-p2p/etc/sudoers.d + install -m 440 $(CURDIR)/sudoers/secubox-p2p-ephemeral \ + $(CURDIR)/debian/secubox-p2p/etc/sudoers.d/ + + # Install nft drop-in (own table, policy accept, udp/51825) + install -d $(CURDIR)/debian/secubox-p2p/etc/nftables.d + install -m 644 $(CURDIR)/nft/secubox-p2p-ephemeral.nft \ + $(CURDIR)/debian/secubox-p2p/etc/nftables.d/ diff --git a/packages/secubox-p2p/nft/secubox-p2p-ephemeral.nft b/packages/secubox-p2p/nft/secubox-p2p-ephemeral.nft new file mode 100644 index 00000000..af2890e0 --- /dev/null +++ b/packages/secubox-p2p/nft/secubox-p2p-ephemeral.nft @@ -0,0 +1,21 @@ +# SecuBox p2p ephemeral WireGuard listener (wg-ephemeral, 10.11.0.0/24). +# Session-scoped assist escalation peers reach this box on udp/51825. +# +# Own base chain with an accept default — a standalone table whose base +# chain defaults to dropping is terminal for the whole netfilter hook the +# instant this file loads: it would blackhole SSH, the webui, everything, +# not just unmatched p2p-ephemeral traffic. Mirrors the secubox-assist / +# secubox-toolbox precedent (packages/secubox-assist/nft/secubox-assist.nft) +# — a fully self-contained table so load never depends on some other table +# already existing on this board. This file must never issue a global +# ruleset reset and never touches the main firewall. +# +# Loaded by /etc/nftables.conf via `include "/etc/nftables.d/*.nft"` +# (packages/secubox-vortex-firewall ships and enables that include) so +# this survives reboot. +table inet secubox_p2p_ephemeral { + chain input { + type filter hook input priority filter; policy accept; + udp dport 51825 accept + } +} diff --git a/packages/secubox-p2p/sudoers/secubox-p2p-ephemeral b/packages/secubox-p2p/sudoers/secubox-p2p-ephemeral new file mode 100644 index 00000000..74a699f2 --- /dev/null +++ b/packages/secubox-p2p/sudoers/secubox-p2p-ephemeral @@ -0,0 +1,5 @@ +# secubox-assist's escalate.py (running its ctl as the `secubox` user, via +# secubox-assistctl) needs to sudo into secubox-p2pctl — the ONLY privileged +# surface for the wg-ephemeral iface (Task 2/3). Scoped to the ctl binary +# only, exact path, no wildcard, no unrestricted grant. +secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-p2pctl diff --git a/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.service b/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.service new file mode 100644 index 00000000..6c3efb4f --- /dev/null +++ b/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.service @@ -0,0 +1,10 @@ +[Unit] +Description=SecuBox P2P — ephemeral wg-ephemeral peer sweep (TTL backstop) +After=network.target + +[Service] +Type=oneshot +# Root oneshot: secubox-p2pctl manages the wg-ephemeral kernel iface +# (wg/ip calls) and its own registry file — no User= line here, matches +# the sudoers principal being root, not the secubox service user. +ExecStart=/usr/sbin/secubox-p2pctl sweep diff --git a/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.timer b/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.timer new file mode 100644 index 00000000..ba8991c7 --- /dev/null +++ b/packages/secubox-p2p/systemd/secubox-p2p-ephemeral-sweep.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Periodic sweep of expired secubox-p2p ephemeral WireGuard peers + +[Timer] +OnBootSec=2min +OnUnitActiveSec=60s +Unit=secubox-p2p-ephemeral-sweep.service + +[Install] +WantedBy=timers.target diff --git a/packages/secubox-p2p/tests/test_packaging_ephemeral.py b/packages/secubox-p2p/tests/test_packaging_ephemeral.py new file mode 100644 index 00000000..e0d7707b --- /dev/null +++ b/packages/secubox-p2p/tests/test_packaging_ephemeral.py @@ -0,0 +1,75 @@ +# SPDX-License-Identifier: LicenseRef-CMSD-1.0 +# Copyright (c) 2026 CyberMind — Gérald Kerma +# Source-Disclosed License — All rights reserved except as expressly granted. +# See LICENCE-CMSD-1.0.md for terms. +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def test_sudoers_scoped_exact(): + s = (ROOT / "sudoers" / "secubox-p2p-ephemeral").read_text() + assert "/usr/sbin/secubox-p2pctl" in s and "NOPASSWD: ALL" not in s and "*" not in s + + +def test_sudoers_exact_line(): + s = (ROOT / "sudoers" / "secubox-p2p-ephemeral").read_text() + assert "secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-p2pctl" in s + + +def test_sweep_units_present(): + svc = (ROOT / "systemd" / "secubox-p2p-ephemeral-sweep.service").read_text() + tmr = (ROOT / "systemd" / "secubox-p2p-ephemeral-sweep.timer").read_text() + assert "secubox-p2pctl sweep" in svc and "OnUnitActiveSec" in tmr + + +def test_sweep_service_is_root_oneshot(): + svc = (ROOT / "systemd" / "secubox-p2p-ephemeral-sweep.service").read_text() + assert "Type=oneshot" in svc + assert "User=secubox" not in svc + + +def test_sweep_timer_install_and_cadence(): + tmr = (ROOT / "systemd" / "secubox-p2p-ephemeral-sweep.timer").read_text() + assert "OnUnitActiveSec=60s" in tmr + assert "OnBootSec=" in tmr + assert "WantedBy=timers.target" in tmr + + +def test_nft_own_table_policy_accept(): + n = (ROOT / "nft" / "secubox-p2p-ephemeral.nft").read_text() + assert "51825" in n and "policy accept" in n and "flush ruleset" not in n + + +def test_nft_is_own_table_never_the_main_firewall(): + n = (ROOT / "nft" / "secubox-p2p-ephemeral.nft").read_text() + assert "table inet secubox_p2p_ephemeral" in n + assert "policy drop;" not in n + + +def test_postinst_no_shared_parent_chown_and_key_guarded(): + p = (ROOT / "debian" / "postinst").read_text() + for bad in ("chown -R secubox /run/secubox", "chown -R secubox /etc/secubox", + "chown -R secubox /var/lib/secubox"): + assert bad not in p + assert "wg genkey" in p and "#DEBHELPER#" in p + + +def test_postinst_key_generation_is_guarded_idempotent(): + p = (ROOT / "debian" / "postinst").read_text() + assert "/etc/secubox/secrets/p2p/wg-ephemeral.key" in p + assert "if [ ! -f /etc/secubox/secrets/p2p/wg-ephemeral.key ]" in p + + +def test_postinst_enables_sweep_timer_and_reloads_nft(): + p = (ROOT / "debian" / "postinst").read_text() + assert "secubox-p2p-ephemeral-sweep.timer" in p + assert "secubox-p2p-ephemeral.nft" in p + + +def test_rules_installs_ctl_units_sudoers_nft(): + r = (ROOT / "debian" / "rules").read_text() + assert "secubox-p2pctl" in r + assert "secubox-p2p-ephemeral-sweep.service" in r or "systemd/*.service" in r + assert "secubox-p2p-ephemeral" in r + assert "secubox-p2p-ephemeral.nft" in r