Public Raspberry Pi kiosk image tooling and signed auto-update channel for Printago dashboards
  • Python 85.8%
  • Shell 14.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-26 15:15:27 -04:00
.forgejo/workflows fix(ci): use Forgejo-compatible artifact upload 2026-07-24 15:35:39 -04:00
config Clarify channel-managed Pi dashboard URLs 2026-07-21 03:17:21 -04:00
docs feat(ops): monitor Pi release channel health 2026-07-24 10:36:57 -04:00
monitoring feat(ops): monitor Pi release channel health 2026-07-24 10:36:57 -04:00
pi-gen/stage-printago feat: add manual no-signing image mode 2026-07-22 19:24:27 -04:00
tests test: skip OpenSSL fixtures when unavailable 2026-07-25 15:08:25 -04:00
tools fix(ci): support loop partitions without udev 2026-07-24 16:24:24 -04:00
.gitattributes Add signed atomic Pi kiosk update channel 2026-07-20 17:49:49 -04:00
.gitignore Add kiosk deployment and harden dashboard operations 2026-07-20 16:14:19 -04:00
artifact-manifest.json Harden privileged Pi release workflows 2026-07-22 13:15:55 -04:00
build-image.sh feat: add manual no-signing image mode 2026-07-22 19:24:27 -04:00
CURRENT-RELEASE-STATUS.md docs: preserve exact Pi run authority 2026-07-26 15:15:27 -04:00
image-build.env.example feat: add manual no-signing image mode 2026-07-22 19:24:27 -04:00
LICENSE Prepare public Pi kiosk release 2026-07-20 18:07:02 -04:00
MIGRATION.md Add signed atomic Pi kiosk update channel 2026-07-20 17:49:49 -04:00
NOTICE Prepare public Pi kiosk release 2026-07-20 18:07:02 -04:00
README.md feat(ops): monitor Pi release channel health 2026-07-24 10:36:57 -04:00
SECURITY.md Add signed atomic Pi kiosk update channel 2026-07-20 17:49:49 -04:00
validate.sh feat(ops): monitor Pi release channel health 2026-07-24 10:36:57 -04:00

Printago Raspberry Pi kiosk image

License: MIT

Current public image/update status is tracked in CURRENT-RELEASE-STATUS.md. Check it before giving a Pi tester a download link.

To verify the stable update channel from an unauthenticated machine, run:

python3 tools/check-public-release-status.py

Add --require-ready in a handoff or release gate; a 404, missing content length, or wrong MIME type means there is still no production-safe Pi channel. For a signed five-minute channel/dashboard monitor that does not repeatedly download the runtime bundle, see Public channel and dashboard monitoring.

This repository is a source-pinned, repeatable pi-gen image recipe for a dedicated Printago kiosk. It targets:

  • Raspberry Pi 4 Model B (2 GB or more) and Raspberry Pi 5
  • Raspberry Pi OS Lite (64-bit), Debian Bookworm
  • Chromium under the Cage Wayland compositor

The image boots straight into the configured dashboard without a desktop or browser chrome. Chromium keeps its profile at /var/lib/printago-kiosk/chromium, so the Printago login, cookies, local storage, and browser-held permission state are not discarded merely because the browser or Pi restarts. Device-grant persistence and reconnection are separate, device-specific acceptance items: never infer them from login persistence.

This repository contains the complete image recipe. A .img is only a delivered artifact after build-image.sh finishes on a supported Linux builder and the resulting image passes the physical checklist below. Merely checking this recipe out does not constitute a tested image.

The exact source revision and pi-gen commit are pinned, but builds are not promised to be byte-for-byte reproducible: Raspberry Pi/Debian APT packages are resolved from their vendor repositories at build time, filesystem/image timestamps can vary, and the workflow generates a kiosk password when no protected value is supplied. The signed manifest, image checksum, resolved artifact evidence, and source SHA make each produced image independently verifiable. A distributor that needs identical rebuilds must additionally pin an authenticated APT snapshot and every generated build input.

Build the image

The supported builder needs a Debian/Ubuntu x86-64 or arm64 host with Git, Docker, and at least 20 GB free space. Docker must support privileged containers and binfmt when the builder is not arm64.

image-build.env is trusted administrator input and may contain build and network secrets. The builder still shell-quotes every value written to pi-gen's config, requires an immutable 40-character Git commit, and refuses the example password. Keep this file out of source control (the local .gitignore does). SSH is disabled by default. If remote administration is required, use a unique key with ENABLE_SSH=1, PUBKEY_ONLY_SSH=1, and PUBKEY_SSH_FIRST_USER=...; do not deploy password SSH to unattended kiosks.

git clone https://git.humpf.tech/jdh/printago-pi-kiosk.git
cd printago-pi-kiosk
cp image-build.env.example image-build.env
# Edit image-build.env. Never commit the real password or Wi-Fi secret.
bash ./build-image.sh image-build.env

The script checks out the exact pi-gen revision from PI_GEN_COMMIT, copies the Printago stage into that clean tree, and invokes build-docker.sh. The finished compressed image, checksum, and recipe manifest are copied into dist/. The generated dist/artifact-manifest.json records the actual image filename, size, SHA-256, source revision, and builder architecture; it does not reuse the checked-in source manifest's truthful not built status. Checksums use relative filenames so the artifact directory remains portable.

Operators who want no custom signing can set RUNTIME_UPDATE_MODE=manual and leave UPDATE_PUBLIC_KEY_FILE empty. That build masks the Printago network runtime updater and is updated only by reviewing and reflashing a complete new image. It still retains vendor-signed Raspberry Pi OS/Debian security updates. See docs/UNSIGNED-MANUAL-IMAGE.md for the tradeoffs and exact configuration. The default remains signed because a network updater without authenticated manifests would be unsafe.

Image distributors should also set BUILTIN_DASHBOARD_URL and DEFAULT_UPDATE_MANIFEST_URL in image-build.env. Both must be HTTPS. If the first-boot DASHBOARD_URL is left empty, a verified signed release can later change the dashboard URL; a non-empty first-boot URL intentionally remains a local farm pin and wins over channel updates.

For the exact disposable-Linux commands, runner resources, signing secrets and the gated Forgejo prerelease workflow, see Build and publish a Pi image. Run bash tools/print-builder-provisioning.sh to print the required host setup without changing the current machine, then run bash tools/check-builder-linux.sh "$PWD" for an aggregate non-destructive audit. The audit includes a short-lived, network-disabled privileged-container probe; it never pulls its probe image or installs/fixes anything automatically.

Test without a Raspberry Pi

There are three deliberately separate evidence levels:

  1. Host/static works on this Windows development machine with its existing Git Bash and Python. It validates the configuration parser, generated Chromium policy/navigation guard, scanner framing, service contract, shell syntax, and an assembled image-tree fixture:

    & 'C:\Program Files\Git\bin\bash.exe' -lc `
      'cd /c/path/to/printago-pi-kiosk && bash ./validate.sh'
    
  2. Built-image/offline + ARM64 userspace emulation needs a disposable Linux builder with privileged Docker, at least 20 GB free, loop-mount tools, and qemu-user-static. After build-image.sh succeeds:

    sudo bash tools/inspect-image-linux.sh dist/printago-kiosk-*.img.xz
    

    This checks the files actually installed in the image, systemd units, enabled-unit links, persistent profile, policies, secret removal, and runs ARM64 Bash/Python through QEMU user emulation.

  3. Best-effort system emulation uses QEMU's Raspberry Pi 3 model against a disposable copy of the image:

    sudo QEMU_BOOT_TIMEOUT_SECONDS=240 bash tools/qemu-boot-smoke.sh \
      dist/printago-kiosk-*.img.xz qemu-boot.log
    

    A pass proves an ARM64 kernel/systemd boot and a disposable in-guest probe of the persistent-profile, signed-updater, enabled-unit, sleep-mask, and managed file-dialog contracts. The QEMU launch deliberately masks the graphics-dependent kiosk service. QEMU does not model the production Pi 4/5 graphics, network, USB, scanner, or display stack closely enough to replace the physical checklist. The manual Forgejo workflow .forgejo/workflows/pi-kiosk-image.yml runs all three software levels on a self-hosted runner labeled printago-pi-builder. Its first step runs the read-only tools/check-builder-linux.sh capability audit. The final upload uses if: always() so a failed emulation attempt still returns its log and step outcomes for diagnosis.

If a workstation has no privileged Docker, Linux loop-mount tools, or QEMU, static validation still runs without modifying it. The minimum image-builder environment is a disposable Debian/Ubuntu VM with privileged Docker, 20 GB free, qemu-system-arm, qemu-user-static, xz-utils, and normal loop-mount utilities. That environment can produce and emulate the .img; real scanner and reboot evidence still requires one Pi and the actual scanner.

Configure before first boot

After flashing, mount the boot partition and copy config/kiosk-config.env.example as printago-kiosk.env:

DASHBOARD_URL=https://your-printago-dashboard.example/
SCANNER_MODE=auto
SERIAL_DEVICE=/dev/serial/by-id/replace-with-your-scanner
SERIAL_BAUD=9600
WIFI_SSID=YourWifi
WIFI_PASSWORD=replace-me
WIFI_COUNTRY=US

The accepted scanner modes are:

  • auto: keyboard-wedge scanners work normally; the serial bridge starts only when SERIAL_DEVICE exists.
  • hid-keyboard: keyboard-wedge scanner only.
  • serial: convert lines from SERIAL_DEVICE into keyboard input followed by Enter.
  • web: leave browser-owned device selection to the dashboard. The current dashboard adapters are WebHID POS and WebSerial; each explicit Connect HID POS scanner or Connect serial scanner click must remain the browser user gesture that opens the chooser. Chromium limits device requests to the configured dashboard origin and does not pre-grant devices. The WebUSB ask policy is future-facing containment only; it is not a claim that the current dashboard has a reviewed WebUSB scanner adapter.

The managed Chromium policy sets AllowFileSelectionDialogs=false. This keeps the dashboard's Scan QR image upload control from opening an unrestricted native file picker in the appliance. Live camera QR scanning, keyboard-wedge input, WebHID POS, and WebSerial remain available and require their own physical acceptance tests.

The serial-to-keyboard bridge deliberately targets the US keyboard layout so punctuation in QR payloads is deterministic. The image builder refuses a different KEYBOARD_KEYMAP. HID keyboard-wedge scanners use their own emitted key events as they do in normal Chromium.

Set DASHBOARD_URL to the farm's real dashboard URL before first boot. An empty value follows the signed channel-managed URL and finally the neutral example built into this public recipe. A non-empty HTTPS URL is a local/farm pin and always wins. The example domains in this repository are deliberately non-operational and never point at a contributor's farm. Changing the effective URL to a different origin keeps the Chromium profile, but the operator must sign in on that new origin because browser cookies are origin-scoped.

The first-boot service validates the URL and settings, creates a NetworkManager Wi-Fi profile, generates the Chromium policy/navigation guard, then deletes the boot-partition config so the Wi-Fi password is not left on a world-readable FAT filesystem. Runtime config contains no Wi-Fi password and is root-owned.

Flash and deploy (one page)

For a non-developer handoff, use the standalone Flash and deploy guide. The condensed steps are:

  1. Flash dist/printago-kiosk-*.img.xz with Raspberry Pi Imager or xzcat ... | sudo dd of=/dev/sdX bs=4M conv=fsync status=progress.
  2. Reinsert the card and mount its bootfs partition.
  3. Copy config/kiosk-config.env.example to bootfs/printago-kiosk.env and set the real HTTPS URL plus Wi-Fi/scanner settings for a farm-pinned deployment. To let the authenticated signed channel change the URL later, use config/kiosk-config.channel-managed.env.example and leave DASHBOARD_URL empty. Do not add shell syntax; this is parsed as data, not sourced.
  4. Insert the card in a Pi 4/5, connect the scanner, and power on. The first start can take several minutes while networking is applied.
  5. Sign in to Printago once. The dedicated Chromium profile persists that login across browser restarts and power cycles.
  6. To change settings later, place a new printago-kiosk.env on bootfs and run sudo systemctl restart printago-kiosk-firstboot.service, or run sudo printago-kiosk-configure /path/to/config locally, then reboot.

Signed runtime updates

The image checks a signed runtime channel with a systemd timer. It never rewrites the operating system or disk image unattended. A release is verified with a pinned public key and SHA-256, extracted into a new version directory, then activated by atomically switching /opt/printago-kiosk/current. If the kiosk service fails to return, the previous pointer and managed URL/channel state are restored. Local config, Wi-Fi settings, and the persistent Chromium profile are outside the release directory and are never replaced.

The update timer is fail-closed until /etc/printago-kiosk/update-public-key.pem exists. Official Forgejo workflow-built release images already contain the maintainer public key; do not copy or replace a key for those images. Only for an explicitly keyless manual/custom image, copy the matching maintainer key supplied with that release to the boot partition as printago-update-public-key.pem. First boot installs it and removes the FAT-partition copy. Never substitute a key from another release or farm. Private signing keys never belong in this repository or an image. See Signed updates.

OS and Chromium security updates

The signed runtime channel does not own Raspberry Pi OS. A separate bounded policy downloads Debian security updates and Raspberry Pi's signed Bookworm packages, including Chromium, then installs them only inside a configured maintenance window after local input is idle and no maintenance login is active. Generic APT upgrade timers are disabled so they cannot bypass that gate. A fresh boot-monotonic heartbeat must cover every current input device; stale, failed, or incomplete monitoring blocks maintenance. Recovery or an input-device hotplug starts a fresh idle interval. Input is monitored through continuously open event-device handles; the authorization heartbeat is invalidated before any coverage generation is replaced. The live resolved APT origin policy is revalidated under lock before every automatic download or install. Package installation is protected by a shutdown inhibitor.

APT never reboots automatically. The default REBOOT_POLICY=manual leaves a required reboot for an operator because the Pi cannot prove that remote farm activity is safe to interrupt. Operators may explicitly opt into an idle- and window-gated reboot policy. Browser profile, farm configuration, and the signed runtime channel/key are protected from the OS maintenance services. Chromium package changes trigger a gated supervised-browser restart and a readiness check, including when a timed-out or failed upgrade changed Chromium partway through; a conservative pre-upgrade marker and process-group shutdown keep timeout/error paths fail closed. A failed or empty Chromium package query also leaves that marker pending. OS install/restart/reboot and signed runtime activation share one operation lock. See OS and Chromium security maintenance.

Physical acceptance checklist

Run every item on the actual Pi and real dashboard before calling an image validated:

The checklist result is model-scoped. Retain a complete run for both Raspberry Pi 4 Model B and Raspberry Pi 5 before promoting the shared pi-stable automatic channel; a single-model run is candidate evidence only.

Use the evidence-oriented Raspberry Pi physical acceptance procedure for the exact reboot, login-persistence, network-recovery, scanner, and signed update-canary steps. The summary below is not a substitute for recording those results.

  • Cold boot reaches only the Printago dashboard, with no desktop, address bar, first-run prompt, crash bubble, or extra window.
  • Attempts to open a new window/tab are closed; top-level off-origin navigation returns to DASHBOARD_URL.
  • Right-click/long-press context menus and downloads are suppressed, and an external protocol link cannot launch another application.
  • With a physical keyboard attached, press Ctrl+Alt+F1 through F6: the kiosk stays on screen and no getty/login console appears (cage runs without VT switching).
  • Sign in, reboot twice, and confirm the session remains signed in.
  • Leave the kiosk idle for 30 minutes; display remains on and the Pi does not suspend.
  • Disconnect/reconnect Wi-Fi or Ethernet; the browser returns to the live dashboard without operator intervention.
  • Kill Chromium from an authorized management session; systemd restarts it in kiosk mode.
  • Scan the dashboard's normal scanner test code with a USB/Bluetooth HID keyboard-wedge scanner. The page receives exactly the same character and Enter key events as desktop Chrome, without clicking a scanner field.
  • In serial mode, scan through the configured /dev/serial/by-id/... device. The page receives the complete code once, followed by Enter.
  • In web mode, exercise the dashboard's current WebHID POS and WebSerial workflows and reboot to record each device's permission and reconnection behavior. Record WebUSB separately as not-supported or not-tested while no reviewed dashboard adapter exists; never turn that absence into a successful WebUSB claim.
  • Activate Scan QR image and confirm Chromium cancels the selection without opening a native file picker. Confirm Scan with camera and the intended hardware scanner path still work.
  • Test touch, scanner, live cameras, card flip, queue, and reconnect paths against the production-like dashboard URL.
  • Confirm OS installation defers during local input and an authorized maintenance login, then succeeds inside the chosen idle window. Confirm the default manual reboot policy never reboots on its own.

Automated checks in this repository validate parsing, policy generation, navigation-guard generation, serial framing/key mapping, service syntax where the host supports it, and shell syntax. They cannot prove USB electrical behavior, scanner firmware behavior, Pi graphics behavior, or session survival across a physical power cycle.

License

This repository is available under the MIT License.

Operations and diagnostics

journalctl -u printago-kiosk -u printago-kiosk-firstboot -b
journalctl -u printago-serial-scanner -b
systemctl status printago-kiosk printago-network-watchdog printago-kiosk-update.timer \
  printago-input-activity printago-os-security-download.timer \
  printago-os-security-install.timer printago-os-security-reboot.timer
chromium --version

The generated Chromium policy is /etc/chromium/policies/managed/printago-kiosk.json; the navigation extension is /opt/printago-kiosk/navigation-guard; and non-secret runtime settings are /etc/printago-kiosk/runtime.env.