diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index 47af6f3..40f5e46 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -43,6 +43,8 @@ env: # per-environment DNS name (e.g. nvme.fleet.lab) resolved at boot so one UKI # works across environments — the serving stack's dnsmasq resolves it. NVMET_TRADDR: "192.168.50.10" + # URL the fullscreen Chromium kiosk loads at boot. Empty disables the kiosk. + KIOSK_URL: "https://www.reddit.com" # =========================================================================== # TEMPORARY: 802.1X + MACsec are DISABLED for this build (rd.dot1x=0) so the # UKI / rootfs / NVMe-oF boot path can be validated on an OPEN switch port. @@ -156,4 +158,5 @@ jobs: -var disable_dot1x=${{ env.DISABLE_DOT1X }} \ -var nvmet_traddr="${{ env.NVMET_TRADDR }}" \ -var ssh_authorized_key="${{ secrets.SSH_AUTHORIZED_KEY }}" \ + -var kiosk_url="${{ env.KIOSK_URL }}" \ . diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index d7d6dc4..c70b510 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -112,6 +112,12 @@ variable "admin_user" { default = "fleetadmin" } +variable "kiosk_url" { + type = string + description = "URL the fullscreen Chromium kiosk loads at boot (cage Wayland session on tty1, baked into the immutable root). Empty disables the kiosk install entirely. A runtime config-agent could override it later via /etc/kiosk/kiosk.env." + default = "https://www.reddit.com" +} + variable "sb_key" { type = string description = "Secure Boot signing key (PEM) for the UKI. Empty = unsigned lab build. Production: CI/HSM-held; never enters the build VM." @@ -186,6 +192,14 @@ build { destination = "/tmp/etc-stage" } + # Kiosk unit + session wrapper (installed by the kiosk provisioner below). + # Dir-without-trailing-slash upload -> /tmp/kiosk/ (mirrors the dracut module + # upload form, which stages reliably). + provisioner "file" { + source = "files/kiosk" + destination = "/tmp/" + } + # --- 2. Packages, configs, diskless adaptations -------------------------- provisioner "shell" { inline = [ @@ -244,6 +258,41 @@ build { ] } + # --- 2b. Kiosk mode: Wayland (cage) + fullscreen Chromium --------------- + # Post-pivot graphical kiosk (no initramfs impact). Baked into the immutable + # root, fleet-generic. Empty kiosk_url skips it. EPEL is enabled for cage + + # chromium — a supply-chain surface (accepted for the lab; see TODO #4). + # NOTE: pulls in Chromium + Mesa (~hundreds of MB) — grows the uncompressed + # erofs namespace noticeably. Graphical bring-up needs a display to verify; + # likely tweak points: cage availability in EPEL, chromium ozone/wayland, + # WLR_RENDERER, --disable-gpu, and SELinux AVCs for the GUI stack. + provisioner "shell" { + environment_vars = ["KIOSK_URL=${var.kiosk_url}"] + inline = [ + "set -e", + "if [ -z \"$KIOSK_URL\" ]; then echo 'kiosk_url empty -> skipping kiosk install.'; exit 0; fi", + "echo '==> Enabling EPEL + installing kiosk stack (cage, chromium, mesa)'", + "dnf install -y epel-release", + "dnf install -y cage chromium mesa-dri-drivers mesa-libEGL mesa-libgbm dejavu-sans-fonts", + # Unprivileged, passwordless kiosk user; video/input as a seat fallback. + "id kiosk >/dev/null 2>&1 || useradd -m -G video,input kiosk", + # Install the staged unit + wrapper. + "install -D -m0755 /tmp/kiosk/kiosk-session /usr/local/bin/kiosk-session", + "install -D -m0644 /tmp/kiosk/kiosk.service /etc/systemd/system/kiosk.service", + # URL (from the var) + software wlroots renderer for VMs without a GPU. + "install -d -m0755 /etc/kiosk", + "printf 'KIOSK_URL=%s\\nWLR_RENDERER=pixman\\n' \"$KIOSK_URL\" > /etc/kiosk/kiosk.env", + "chmod 0644 /etc/kiosk/kiosk.env", + # Label the new files so they survive into the enforcing erofs. + "restorecon -R /usr/local/bin/kiosk-session /etc/systemd/system/kiosk.service /etc/kiosk", + # Boot graphical and start the kiosk (replaces the tty1 getty). + "systemctl enable kiosk.service", + "systemctl set-default graphical.target", + "rm -rf /tmp/kiosk", + "echo \"==> kiosk enabled: $KIOSK_URL\"", + ] + } + # --- 2c. Admin access: sudo-enabled key user + locked root -------------- # Fleet-generic (same user + key on every host => NOT per-host state). The # key comes from var.ssh_authorized_key (CI secret). If no key is supplied we diff --git a/docs/TODO.md b/docs/TODO.md index b64980b..38f55de 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -81,7 +81,37 @@ re-export a **v3** PDF (key change: `rootovl` → `systemd.volatile=overlay` in step 8 and §5). Retire/reset the errata afterward. Needs the design-doc source (not in this repo). -## [ ] 4. Kiosk mode — autologin + fullscreen Chromium loading a URL +## [~] 4. Kiosk mode — autologin + fullscreen Chromium loading a URL + +**First cut implemented (2026-06-16) — needs a display to verify.** `cage` +(Wayland single-app compositor) + Chromium from EPEL, baked into the root: +- `kiosk_url` Packer var (default `https://www.reddit.com`; empty disables the + whole install). Wired in CI as `KIOSK_URL` env. +- Provisioner enables EPEL, installs `cage chromium mesa-dri-drivers mesa-libEGL + mesa-libgbm dejavu-sans-fonts`, creates a passwordless unprivileged `kiosk` + user (video/input groups). +- `files/kiosk/kiosk.service` — runs `cage -s -- /usr/local/bin/kiosk-session` + as `kiosk` on tty1 via `PAMName=login`+`TTYPath` (gets seat0 for DRM/input), + `Conflicts=getty@tty1`, `WantedBy=graphical.target`; default target set to + graphical. +- `files/kiosk/kiosk-session` — Chromium with `--kiosk --ozone-platform=wayland + --disable-gpu` + tmpfs profile/cache; URL from `/etc/kiosk/kiosk.env`. +- `WLR_RENDERER=pixman` in kiosk.env for VMs without a GPU. +- **Verify on a VM with a console (Proxmox VNC/SPICE):** cage comes up on tty1, + Chromium loads the URL over the booted network. Likely tweak points: + `cage` availability in EPEL 9 (else fall back to `weston --shell=kiosk-shell`), + Chromium ozone/wayland support, dropping `--disable-gpu` on real GPUs, and + **SELinux AVCs** for the GUI stack (check `ausearch -m avc`; now that the image + is enforcing with labels, the kiosk may need a small policy module or booleans + — do NOT just set permissive). Admin SSH (the key user) still works for debug + even if the kiosk fails (it lands at graphical.target either way). +- **Image size:** Chromium+Mesa add hundreds of MB to the *uncompressed* erofs — + feeds the "shrink the rootfs" concern (and #7); reconsider whether a lighter + browser/render path is viable. + +### Original task + + Make the diskless workstation boot straight into a locked-down kiosk: a fullscreen Chromium showing a configured site (initial target: Reddit). All diff --git a/files/kiosk/kiosk-session b/files/kiosk/kiosk-session new file mode 100644 index 0000000..2b84fcb --- /dev/null +++ b/files/kiosk/kiosk-session @@ -0,0 +1,22 @@ +#!/bin/bash +# kiosk-session — launched by cage (Wayland) from kiosk.service. +# Starts Chromium fullscreen on the configured URL. +# +# KIOSK_URL comes from /etc/kiosk/kiosk.env (baked from the kiosk_url Packer +# var; a runtime config-agent could override it later). The Chromium profile is +# pinned to a tmpfs path so writes land in RAM (the root is read-only erofs + +# volatile overlay) — watch RAM if the page is heavy. +# +# VM/GPU notes: --disable-gpu + the pixman wlroots renderer (WLR_RENDERER in +# kiosk.env) keep this working on VMs with no real GPU. On hardware with a GPU, +# drop --disable-gpu (and set WLR_RENDERER=gles2) for compositing. +set -euo pipefail + +BROWSER=$(command -v chromium-browser || command -v chromium) + +exec "$BROWSER" \ + --kiosk --noerrdialogs --disable-infobars --incognito --no-first-run \ + --disable-session-crashed-bubble --start-maximized \ + --ozone-platform=wayland --disable-gpu \ + --user-data-dir=/tmp/kiosk-profile --disk-cache-dir=/tmp/kiosk-cache \ + "${KIOSK_URL:?KIOSK_URL not set in /etc/kiosk/kiosk.env}" diff --git a/files/kiosk/kiosk.service b/files/kiosk/kiosk.service new file mode 100644 index 0000000..d598d28 --- /dev/null +++ b/files/kiosk/kiosk.service @@ -0,0 +1,33 @@ +[Unit] +Description=Wayland kiosk (cage + Chromium) +Documentation=https://github.com/cage-kiosk/cage +# Take over tty1 from the login getty and start after the network is up (the +# kiosk loads a URL). plymouth-quit lets the splash hand off cleanly. +After=systemd-user-sessions.service plymouth-quit-wait.service getty@tty1.service network-online.target +Wants=network-online.target +Conflicts=getty@tty1.service + +[Service] +Type=simple +User=kiosk +# A real PAM login session on tty1 so logind assigns seat0 — cage/wlroots needs +# a seat for DRM master + libinput. (PAMName runs account/session, not auth, so +# the passwordless kiosk user is fine.) +PAMName=login +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +StandardInput=tty-fail +StandardOutput=journal +StandardError=journal +UtmpIdentifier=tty1 +UtmpMode=user +EnvironmentFile=/etc/kiosk/kiosk.env +# cage -s: single-client kiosk compositor; runs the wrapper, exits when it does. +ExecStart=/usr/bin/cage -s -- /usr/local/bin/kiosk-session +Restart=always +RestartSec=2 + +[Install] +WantedBy=graphical.target