Files
punktfunk/packaging/bootc/Containerfile
T
enricobuehler 4f8cce6751 feat(packaging): grant CAP_SYS_NICE to the encode worker on all six channels, and assert the host never gets it
767e67ca's per-channel mechanics were correct; they were aimed at the wrong binary. Each one is
restored here pointed at punktfunk-encode-worker, and every host-side removal from #136 stays
verbatim. All grants remain best-effort — an uncapped worker still encodes, at default priority, so
a failed setcap must never fail an install.

  * Arch: setcap in post_install AND post_upgrade (a replaced binary is a new inode).
  * RPM: %caps(cap_sys_nice=ep) in %files, never a %post setcap — %caps applies, restores and
    verifies, and covers Fedora as well as Bazzite via rpm-ostree layering.
  * Bazzite + Arch sysext: setcap on the staging tree before mksquashfs, which does record
    security.capability. The assertion is amended, not removed: host EMPTY is still a hard fail, and
    the worker must carry exactly cap_sys_nice=ep — missing is fine, anything else is not.
  * deb: setcap in postinst.
  * NixOS: security.wrappers for the WORKER plus PUNKTFUNK_ENCODE_WORKER in the unit. A file
    capability cannot live on a store path, and an ambient grant is right here precisely because
    nothing ever identifies the worker. The host's ExecStart stays on the store path.
  * Steam Deck: setcap the worker; the .desktop the script writes stays valid this time.

Four things the plan's channel table missed:

  * packaging/arch/build-sysext.sh had no capability handling at all, and a sysext can never run a
    pacman scriptlet — the SteamOS image would have shipped the lever permanently inert.
  * scripts/steamdeck/update.sh had none either. It rebuilds both binaries, so a new inode drops the
    grant, and it is the documented steady-state path: the lever would have died on the first update.
    It also never healed a Deck already capped by 0.26.0-1.
  * A capped worker is AT_SECURE, and glibc drops $ORIGIN-expanded RPATH entries for secure binaries
    unless they normalise into a trusted system dir. Copying the host's rpath under BUNDLE_FFMPEG=1
    would have left the capped worker unable to find libavcodec on exactly the channel that bundles
    it. Absolute DT_RPATH instead.
  * Nix crane scopes by -p, so the worker would not have been built at all, and it needs its own
    addDriverRunpath.

scripts/ci/assert-cap-matrix.sh mechanizes the lesson from 0.26.0-1 — verify the PACKAGE, never the
board. It unpacks the built Arch package, the deb, the rpm and the mounted sysext raw and asserts one
matrix: the host carries NOTHING (hard fail), the worker exactly cap_sys_nice=ep. The sysext reader
first proves it can round-trip a capability through mksquashfs/unsquashfs at all, so an unreadable
artifact fails rather than issuing a blind PASS, and --self-test red-teams the assertions themselves.

Red-teaming the leg found a real bug: setcap originally ran BEFORE the assertion, so "the worker
arrived carrying something unexpected" was unreachable and a stray %caps would have been silently
overwritten. Both sysext scripts now assert, then grant, then assert again.
2026-08-09 12:50:47 +02:00

68 lines
4.1 KiB
Docker

# bootc / OCI image layer that bakes punktfunk into a Bazzite-based atomic image.
#
# Bazzite is already a bootc image (Fedora Atomic + gamescope + PipeWire + the NVIDIA
# stack), so we layer punktfunk on top: enable RPM Fusion (for the NVENC ffmpeg) and unom's
# Gitea RPM registry (NOT COPR — only the registry carries the punktfunk-web subpackage),
# install the packages, and pre-enable the udev rule. Build + push this image, then
# `bootc switch` (or rebase) a Bazzite host onto it for an image-based, atomic install —
# no per-host `rpm-ostree install` drift.
#
# podman build -t ghcr.io/<you>/bazzite-punktfunk -f packaging/bootc/Containerfile .
# podman push ghcr.io/<you>/bazzite-punktfunk
# # on the target Bazzite host:
# sudo bootc switch ghcr.io/<you>/bazzite-punktfunk # then reboot
#
# Pick the base tag that matches your hardware (NVIDIA shown). See ublue-os/bazzite tags.
ARG BASE_IMAGE=ghcr.io/ublue-os/bazzite-nvidia:stable
FROM ${BASE_IMAGE}
# punktfunk's RPMs come from unom's Gitea RPM registry (the recommended path — see
# packaging/rpm/README). Use it rather than COPR specifically because it carries the
# punktfunk-web management console subpackage, which COPR's mock chroot can't build (no `bun`).
# Group "bazzite" == the Fedora 43 base; override for a different base. Gitea signs the repo
# metadata (repo_gpgcheck=1) and the packages are GPG-signed (gpgcheck=1, the packages@unom.io key).
ARG PUNKTFUNK_RPM_GROUP=bazzite
# RPM Fusion nonfree provides the NVENC-capable ffmpeg-libs punktfunk records/encodes with.
# (Bazzite usually has RPM Fusion enabled already; this is belt-and-suspenders.)
RUN dnf5 -y install \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm \
|| true
# Add the Gitea RPM repo and install the host + the web console (punktfunk-web pulls nodejs).
RUN printf '%s\n' \
'[gitea-unom-punktfunk]' \
'name=punktfunk (unom)' \
"baseurl=https://git.unom.io/api/packages/unom/rpm/${PUNKTFUNK_RPM_GROUP}" \
'enabled=1' 'gpgcheck=1' 'repo_gpgcheck=1' \
'gpgkey=https://git.unom.io/api/packages/unom/rpm/repository.key https://git.unom.io/api/packages/unom/generic/punktfunk-keys/1/RPM-GPG-KEY-punktfunk' \
> /etc/yum.repos.d/punktfunk.repo \
&& dnf5 -y install punktfunk punktfunk-web \
&& dnf5 clean all
# The udev rule + systemd *user* units ship in the RPMs; nothing else to enable at image build
# time (host + console run per-user in the graphical session, enabled after first boot with
# `systemctl --user enable --now punktfunk-host punktfunk-web`).
# NO `setcap` here — deliberately, in BOTH directions, and this file must stay that way.
#
# /usr/bin/punktfunk-host must carry NO capability. A capability-carrying process
# cannot have its /proc/<pid>/exe read, so KWin cannot identify
# it, never advertises zkde_screencast_unstable_v1, and every
# KDE desktop session dies. That is the 0.26.0-1 incident; a
# layered/bootc image is as unrepairable in place as a sysext.
# /usr/bin/punktfunk-encode-worker carries cap_sys_nice=ep, declared with %caps in
# packaging/rpm/punktfunk.spec. rpm applies file capabilities
# from package metadata during the dnf5 install above and the
# ostree commit preserves the security.capability xattr — so it
# arrives correctly without anything to do here, and the
# capability matrix is asserted on the .rpm in CI
# (scripts/ci/assert-cap-matrix.sh, .gitea/workflows/rpm.yml).
#
# If a capability is ever wanted in this image, change the SPEC, never this file: a setcap here
# would apply to one channel and drift from the other four.
# bootc image hygiene: the container build must leave a clean ostree commit.
RUN ostree container commit