fix(nix): port three NixOS-module divergences from the shipped systemd units

A sweep of the Nix packaging against the units the deb/rpm actually install
found three decisions that were made, documented and deliberate everywhere
else, and simply not carried into packaging/nix/nixos-module.nix.

punktfunk-web — StartLimitIntervalSec=0.
  The unit's EnvironmentFile for the mgmt token is mandatory ON PURPOSE, so the
  console genuinely fails until the host's first `serve` writes it. systemd's
  default rate limit (5 starts / 10 s) against RestartSec=2 then gives up
  permanently after ~10 s — which on an appliance is exactly the window before
  the host is ready, so a console enabled before the host's first run stayed
  dead until someone restarted it by hand. scripts/punktfunk-web.service has
  carried the override since that defect was found; the Nix module omitted it
  while its own comment went on promising "Restart retries until the host has
  created it".

punktfunk-web — Restart=always, not on-failure.
  A console that exits 0 has still stopped serving, and on-failure leaves it
  down. Matches the shipped unit and web-run.cmd on Windows, both of which
  relaunch bun on ANY exit. An explicit `systemctl --user stop` is unaffected.

punktfunk-scripting — the sandbox was missing entirely.
  The shipped unit confines the runner with NoNewPrivileges, ProtectSystem=
  strict, ReadWritePaths=%h /tmp and an AF_UNIX/AF_INET/AF_INET6 address-family
  restriction, plus PrivateTmp=no (a field report: a private /tmp hides
  /tmp/vhclient and /tmp/.X11-unix, so a plugin launches its vendor binary and
  then cannot reach the daemon behind it). The NixOS unit had none of it — so
  the one unit here that executes arbitrary operator TypeScript by design ran
  strictly LESS confined on NixOS than on every other channel.

Verified by evaluating the module against the pinned nixpkgs and rendering the
units: assertions clean, cap_sys_nice=ep on the encode-worker wrapper, firewall
47984/47989/47990/47992/47993/48010, and each unit carrying exactly the
directives above. That evaluation is NOT something CI does — measured: `nix
flake check` passes a nixosModule containing a nonexistent option, a nonexistent
pkgs attribute and a nonexistent lib function, printing "checking NixOS module
... all checks passed!" while never evaluating it against nixpkgs. nix.yml's
header claims that leg covers the module. It does not; tracked separately.
This commit is contained in:
2026-08-10 19:27:14 +02:00
parent 002702bcec
commit 159bbdbfc2
+46 -1
View File
@@ -508,6 +508,15 @@ in
];
wants = [ "punktfunk-web-init.service" ];
wantedBy = optional cfg.web.autoStart "default.target";
# Retry INDEFINITELY while the host is still writing the mgmt token + identity cert. The
# EnvironmentFile below is mandatory on purpose, so the unit genuinely fails until those
# exist — and systemd's default rate limit (5 starts / 10 s) against `RestartSec = 2` gives
# up permanently after ~10 s, which on an appliance is exactly the window before the host's
# first `serve` completes. A console enabled before the host's first run then stayed dead
# until someone restarted it by hand. The shipped unit (scripts/punktfunk-web.service) has
# carried this since that defect was found; it was missed in the port, while the comment
# below went on promising the behaviour it removes.
unitConfig.StartLimitIntervalSec = 0;
environment = {
PUNKTFUNK_MGMT_URL = "https://127.0.0.1:47990";
PORT = "47992";
@@ -525,7 +534,11 @@ in
"-%h/.config/punktfunk/web-password"
];
ExecStart = "${cfg.web.package}/bin/punktfunk-web-server";
Restart = "on-failure";
# `always`, not `on-failure`: a console that exits 0 has still stopped serving, and
# `on-failure` would leave it down. An explicit `systemctl --user stop` is still honoured
# (Restart= never fights that). Matches scripts/punktfunk-web.service and the Windows
# web-run.cmd, both of which relaunch bun on ANY exit.
Restart = "always";
RestartSec = 2;
};
};
@@ -554,6 +567,38 @@ in
KillMode = "mixed";
KillSignal = "SIGTERM";
TimeoutStopSec = 30;
# Sandbox — the same confinement scripts/punktfunk-scripting.service gives the deb/rpm
# installs. The runner `import()`s the operator's own `.ts` files, so this is the one unit
# here that executes arbitrary code by design; without these it ran strictly LESS confined
# on NixOS than on every other channel. Read-only outside $HOME, no setuid re-escalation,
# and only the address families automation actually uses (loopback mgmt API, LAN/IPv6
# webhooks, unix sockets).
NoNewPrivileges = true;
# PrivateTmp deliberately OFF (field report 2026-08-03, the VirtualHere plugin). A
# plugin's whole job is integrating with things already running on this box, and on Linux
# those talk over /tmp: VirtualHere's client IPC is the FIFO pair /tmp/vhclient +
# /tmp/vhclient_response, X11 is /tmp/.X11-unix. A private /tmp hides all of it — the
# plugin launches the vendor binary fine and then cannot reach the daemon behind it,
# which presents as an error no amount of config fixes.
PrivateTmp = false;
ProtectSystem = "strict";
# ReadWritePaths puts back the write bit ProtectSystem=strict takes away: plugin state and
# ~/.config/punktfunk under $HOME, plus the /tmp above. A plugin that must write OUTSIDE
# $HOME (a game library on another mount) gets it with
# systemctl --user edit punktfunk-scripting → [Service] ReadWritePaths=/mnt/games
# ⚠ ProtectSystem is a MOUNT-NAMESPACE option, and for a *user* unit that needs
# unprivileged user namespaces. On a kernel/config that restricts those it fails the unit
# rather than degrading — drop it via the same drop-in if this box is one of them.
ReadWritePaths = [
"%h"
"/tmp"
];
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
};
};
})