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>
This commit is contained in:
CyberMind-FR 2026-07-27 06:50:01 +02:00
parent 1c2e43bd99
commit cf06d9dd01
10 changed files with 222 additions and 2 deletions

View File

@ -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 <devel@cybermind.fr> 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

View File

@ -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)")

View File

@ -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 <devel@cybermind.fr> 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

View File

@ -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

View File

@ -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/

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,75 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# 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