From 81a64766ae0f97dede4a0ef5cce4c1c5f8517496 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Wed, 13 May 2026 03:11:06 +0200 Subject: [PATCH] feat(deploy): Pi kiosk bring-up via cage + low-priv bfkiosk user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the user-mode kiosk service with a system unit that runs cage (single-app Wayland compositor) on tty1 as a dedicated unprivileged user. No desktop, no display manager, auto-restart on crash via Restart=always. setup-pi-kiosk.sh provisions the user, installs cage + seatd, disables any display manager, points default.target at multi-user, drops the PAM stack, and enables the service. Idempotent. Screen wake "auto-login": with no DM and no lockscreen, DPMS-driven sleep just turns the panel back on — the kiosk process is already running. --- deploy/pam.d/cage | 8 +++ deploy/scripts/setup-pi-kiosk.sh | 75 ++++++++++++++++++++++++ deploy/systemd/betterframe-kiosk.service | 40 +++++++++---- 3 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 deploy/pam.d/cage create mode 100644 deploy/scripts/setup-pi-kiosk.sh diff --git a/deploy/pam.d/cage b/deploy/pam.d/cage new file mode 100644 index 0000000..9f9870a --- /dev/null +++ b/deploy/pam.d/cage @@ -0,0 +1,8 @@ +# PAM stack for cage when invoked via systemd's PAMName=cage. +# Permissive auth (no password) because the session is launched by systemd as +# bfkiosk after auto-login at the multi-user target. pam_systemd sets up the +# XDG_RUNTIME_DIR + session, pam_loginuid wires the audit id. +auth required pam_permit.so +account required pam_permit.so +session required pam_loginuid.so +session required pam_systemd.so diff --git a/deploy/scripts/setup-pi-kiosk.sh b/deploy/scripts/setup-pi-kiosk.sh new file mode 100644 index 0000000..2d5cb2f --- /dev/null +++ b/deploy/scripts/setup-pi-kiosk.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Provision a Raspberry Pi (Bookworm or later) as a BetterFrame kiosk. +# +# - Creates the unprivileged `bfkiosk` user +# - Installs `cage` (single-app Wayland compositor) and `seatd` +# - Disables any installed display manager and sets multi-user.target +# - Disables getty on tty1 (cage owns the tty) +# - Installs the betterframe-kiosk system unit + PAM stack +# - Enables + starts the service +# +# Re-run safely: every step is idempotent. The kiosk binary itself must +# already be installed at /opt/betterframe/kiosk/betterframe-kiosk. +# +# Usage: sudo ./deploy/scripts/setup-pi-kiosk.sh + +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "error: must run as root (sudo $0)" >&2 + exit 1 +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" + +echo "==> Installing cage + seatd" +apt-get update +apt-get install -y --no-install-recommends cage seatd + +echo "==> Enabling seatd" +systemctl enable --now seatd + +echo "==> Ensuring bfkiosk user exists" +if ! id -u bfkiosk >/dev/null 2>&1; then + useradd -m -s /usr/sbin/nologin bfkiosk +fi +# nologin shell means no interactive login. The systemd unit launches cage +# directly so a shell is not needed. The user joins the hardware-access +# groups required by GStreamer/GTK/cage. +for grp in video render input audio seat; do + if getent group "$grp" >/dev/null; then + usermod -a -G "$grp" bfkiosk + fi +done + +echo "==> Disabling any display manager" +for dm in lightdm gdm gdm3 sddm; do + systemctl disable --now "${dm}.service" 2>/dev/null || true +done +systemctl set-default multi-user.target + +echo "==> Disabling getty on tty1 (cage owns it)" +systemctl disable --now getty@tty1.service 2>/dev/null || true + +echo "==> Installing PAM stack and systemd unit" +install -m 644 "${REPO_ROOT}/deploy/pam.d/cage" /etc/pam.d/cage +install -m 644 "${REPO_ROOT}/deploy/systemd/betterframe-kiosk.service" \ + /etc/systemd/system/betterframe-kiosk.service + +if [ ! -e /etc/default/betterframe-kiosk ]; then + cat > /etc/default/betterframe-kiosk <<'EOF' +# Runtime env for betterframe-kiosk. Edit and `systemctl restart betterframe-kiosk`. +# BETTERFRAME_SERVER=http://192.168.1.10 +EOF +fi + +if [ ! -x /opt/betterframe/kiosk/betterframe-kiosk ]; then + echo "warn: /opt/betterframe/kiosk/betterframe-kiosk is missing or not executable." + echo " Build the kiosk with 'cargo build --release' and copy the binary there" + echo " before starting the service." +fi + +systemctl daemon-reload +systemctl enable betterframe-kiosk.service + +echo "==> Done. Reboot to take effect, or 'systemctl start betterframe-kiosk' now." diff --git a/deploy/systemd/betterframe-kiosk.service b/deploy/systemd/betterframe-kiosk.service index b70206d..62813ec 100644 --- a/deploy/systemd/betterframe-kiosk.service +++ b/deploy/systemd/betterframe-kiosk.service @@ -1,22 +1,38 @@ [Unit] -Description=BetterFrame Kiosk +Description=BetterFrame Kiosk (cage + betterframe-kiosk) Documentation=https://github.com/BetterCorp/BetterFrame -After=graphical-session.target network-online.target +After=systemd-user-sessions.service plymouth-quit-wait.service network-online.target seatd.service Wants=network-online.target -PartOf=graphical-session.target +Conflicts=getty@tty1.service +After=getty@tty1.service [Service] Type=simple -WorkingDirectory=%h/.betterframe-kiosk -Environment=XDG_RUNTIME_DIR=/run/user/%U -Environment=WAYLAND_DISPLAY=wayland-0 -Environment=NO_AT_BRIDGE=1 -Environment=GST_DEBUG=1 -ExecStart=/opt/betterframe/kiosk/betterframe-kiosk -Restart=on-failure -RestartSec=3 +User=bfkiosk +Group=bfkiosk +SupplementaryGroups=video render input audio seat +PAMName=cage +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +StandardInput=tty-fail StandardOutput=journal StandardError=journal +UtmpIdentifier=tty1 +UtmpMode=user +WorkingDirectory=/home/bfkiosk +EnvironmentFile=-/etc/default/betterframe-kiosk +Environment=XDG_SESSION_TYPE=wayland +Environment=XDG_SESSION_CLASS=user +Environment=GST_DEBUG=1 +Environment=BETTERFRAME_SERVER=http://localhost +ExecStart=/usr/bin/cage -s -- /opt/betterframe/kiosk/betterframe-kiosk +Restart=always +RestartSec=2 +# After 10 fast restarts, back off for 30s so a broken binary doesn't burn the CPU. +StartLimitIntervalSec=60 +StartLimitBurst=10 [Install] -WantedBy=graphical-session.target +WantedBy=multi-user.target