Files
punktfunk/scripts/punktfunk-scripting.service
T
enricobuehler 7b7231fdbe feat(security): gate public-registry plugin installs + sandbox the runner unit
Supply chain: resolvePackage() used to pass any punktfunk-plugin-* or
foreign-scope name straight to bun add — a typo or a squatted look-alike
on npmjs.org would install operator-privileged code. Only the @punktfunk
scope (pinned to the Gitea registry by the bunfig scope map) resolves by
default now; anything else throws with an explanation unless
--allow-public-registry is passed, and even then installs print a loud
warning. Removal never gates — uninstalling stays safe regardless of
origin.

systemd (user unit): free hardening for well-behaved plugins —
NoNewPrivileges, PrivateTmp, ProtectSystem=strict with ReadWritePaths=%h
(plugin state and download dirs under $HOME keep working), and
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6. Plugins writing
outside $HOME, and distros that restrict unprivileged user namespaces
for user units, are handled via a documented systemctl --user edit
drop-in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:45:27 +02:00

50 lines
2.7 KiB
Desktop File

# punktfunk plugin/script runner — systemd USER unit (bun runtime, OPT-IN).
#
# Runs the operator's automation under supervision: loose files in ~/.config/punktfunk/scripts/ and
# installed `punktfunk-plugin-*` packages under ~/.config/punktfunk/plugins/. Each unit is an Effect
# fiber (a plugin restarts on failure with capped-jittered backoff; a bare script is one-shot).
# SIGTERM interrupts the whole tree STRUCTURALLY, so every plugin's scoped finalizers run before
# exit (clean deregister / preset release) — hence the generous stop timeout below.
#
# OPT-IN — unlike punktfunk-web, the package does NOT auto-enable this: the runner does nothing until
# you add scripts or install plugins. Turn it on once you have automation to run:
# systemctl --user enable --now punktfunk-scripting
#
# Auto-wired like the console: a plugin's connect() reads the host's SCOPED plugin token + identity
# cert from ~/.config/punktfunk/{plugin-token,cert.pem} (written by the host's `serve`) — no env
# editing. The plugin token authorizes the plugin surface but not hook registration or pairing
# administration; a script that needs the admin surface sets PUNKTFUNK_MGMT_TOKEN explicitly.
[Unit]
Description=punktfunk plugin/script runner
Documentation=https://git.unom.io/unom/punktfunk
# Plugins talk to the host's loopback mgmt API; order after it. Soft (ordering only, no Requires):
# the runner supervises each unit with backoff, so a plugin started before the host simply retries.
After=punktfunk-host.service
[Service]
Type=simple
ExecStart=/usr/bin/punktfunk-scripting
Restart=on-failure
RestartSec=2
# Deliver the stop signal to the runner process itself (it orchestrates the structural shutdown of
# its unit fibers), and give it room to run their finalizers before the cgroup is reaped.
KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=30
# Sandbox: free hardening for well-behaved plugins. The filesystem is read-only outside the home
# directory (ReadWritePaths keeps plugin state, download dirs, and ~/.config/punktfunk writable);
# /tmp is private; no setuid re-escalation; sockets limited to what automation actually uses
# (loopback mgmt API, LAN/IPv6 webhooks, unix sockets). A plugin that must write OUTSIDE $HOME
# (e.g. a library on another mount) gets a drop-in:
# systemctl --user edit punktfunk-scripting → [Service]\nReadWritePaths=/mnt/games
# NOTE: the mount-namespace options (ProtectSystem/PrivateTmp) need unprivileged user namespaces
# for a *user* unit; on kernels/distros that restrict those, drop them via the same drop-in.
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=%h
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=default.target