fix(profiles): sleeper unit not ProtectSystem=strict (drives LXC in-process) (ref #896)

secubox-sleeper.service actuates in-process as root (no sudo/systemd-run,
unlike the panel/waker which escape their own sandbox before touching
systemd/LXC). ProtectSystem=strict made everything under /run read-only
outside the listed ReadWritePaths, but lxc-start/lxc-stop write
LXC-internal runtime/lock/cgroup paths (/run/lxc/, /run/lock/lxc/, mount-
namespace setup) that are not part of this codebase's stable contract to
enumerate. api/actuate.py::_issue only hard-fails on rc is None, so the
resulting EROFS would have been silent: auto-sleep for every LXC-backed
on-demand module would quietly stop working.

Drop ProtectSystem=strict and ReadWritePaths=; keep ProtectHome=true,
PrivateTmp=true, NoNewPrivileges=true, and RuntimeDirectory=secubox. A
root daemon driving LXC in-process without ProtectSystem is at this
codebase's normal floor, not a regression.

debian/postinst: keep pre-creating /var/log/secubox and
/var/lib/secubox/profiles/rollback (still needed by the actuator
regardless of sandboxing), reword the comment to drop the
ProtectSystem=strict framing.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-20 17:14:53 +02:00
parent d67e2c03b7
commit dee65ed6c8
2 changed files with 26 additions and 29 deletions

View File

@ -25,16 +25,16 @@ case "$1" in
fi
done
# secubox-sleeper.service runs ProtectSystem=strict with an explicit
# ReadWritePaths= (see debian/secubox-sleeper.service) — every
# directory it lists MUST already exist before the unit's first
# start, or systemd's sandbox will not make it writable. Pre-create
# this package's own two leaves here (shared parents /var/lib/secubox
# and /var/log/secubox themselves come from secubox-core, 0755
# secubox:secubox — reasserted here matching the repo-wide
# shared-parent convention, never narrowed to 0750). /run/secubox
# comes from RuntimeDirectory=secubox; /data/lxc and /etc/secubox/waf
# are provisioned by lxc/secubox-waf-ng respectively, not here.
# The actuator (api/actuate.py + snapshot.py + audit.py, driven both
# by secubox-profilectl apply/rollback and by secubox-sleeper's
# in-process STOPs) writes snapshot.SNAP_DIR and audit.AUDIT_LOG —
# pre-create both here so a fresh board has them before the first
# apply/sleep, regardless of unit sandboxing (secubox-sleeper.service
# is deliberately NOT ProtectSystem=strict, see its unit file, but
# these two leaves are needed either way). Shared parent
# /var/log/secubox comes from secubox-core (0755 secubox:secubox) —
# reasserted here matching the repo-wide shared-parent convention,
# never narrowed to 0750.
install -d -o secubox -g secubox -m 0755 /var/log/secubox
install -d -o root -g root -m 0750 /var/lib/secubox/profiles/rollback

View File

@ -28,27 +28,24 @@ RuntimeDirectory=secubox
RuntimeDirectoryPreserve=yes
RuntimeDirectoryMode=0775
# ProtectSystem=strict makes / read-only by default; the actuator's actual
# write set (traced through api/actuate.py, api/snapshot.py, api/audit.py —
# apply_plan's STOP path is the only one the sleeper ever drives) is listed
# explicitly below so this sandbox stays FUNCTIONAL, not just hardened:
# - /run/secubox — the socket dir + api.sleeper.WAKE_LOCK_FILE
# (read, but ReadWritePaths is directory-granular) and waker-active.json
# - /var/lib/secubox/profiles/rollback — snapshot.SNAP_DIR, the 4R chain
# shared with secubox-profilectl/secubox-wakectl (actuate_paths.py)
# - /var/log/secubox — audit.AUDIT_LOG (append-only audit.log)
# - /data/lxc — actuate.py::_lxc_autostart edits
# <lxcpath>/<container>/config (lxc.start.auto) before lxc-stop
# - /etc/secubox/waf — actuate.py::ROUTES_FILE
# (haproxy-routes.json): a STOP on a routed module calls
# _portal_remove, which rewrites this file (temp+rename in the SAME
# directory) — omitting it here would reproduce, one layer up, the
# exact EROFS lesson that motivated wrapping the OTHER two ctls in
# systemd-run in the first place (secubox-profilectl 0.6.1).
ProtectSystem=strict
# Deliberately NOT ProtectSystem=strict (ref #896 T12 review). This daemon
# drives lxc-start/lxc-stop + systemctl IN-PROCESS as root — unlike the
# panel/waker, which escape their own sandbox via systemd-run into PID1's
# context before touching systemd/LXC. lxc-start/lxc-stop write LXC-internal
# runtime/lock paths (/run/lxc/, /run/lock/lxc/, cgroup + mount-namespace
# setup) that are no part of this codebase's stable contract to enumerate;
# under ProtectSystem=strict everything under /run outside an explicit
# ReadWritePaths entry is read-only, so those writes would EROFS — silently,
# since api/actuate.py::_issue only hard-fails on rc is None, meaning
# auto-sleep for every LXC-backed on-demand module would quietly stop
# working (the whole RAM-savings target of ref #896). A root daemon that
# drives LXC in-process without ProtectSystem is at this codebase's normal
# floor (many root daemons here have no sandboxing) — dropping strict here
# is a fix, not a regression. ProtectHome/PrivateTmp stay: the actuator
# needs neither /home nor a shared /tmp (its atomic writes are all
# temp-file-in-target-dir, not /tmp).
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/run/secubox /var/lib/secubox/profiles/rollback /var/log/secubox /data/lxc /etc/secubox/waf
[Install]
WantedBy=multi-user.target