- Python 85.8%
- Shell 14.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| config | ||
| docs | ||
| monitoring | ||
| pi-gen/stage-printago | ||
| tests | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| artifact-manifest.json | ||
| build-image.sh | ||
| CURRENT-RELEASE-STATUS.md | ||
| image-build.env.example | ||
| LICENSE | ||
| MIGRATION.md | ||
| NOTICE | ||
| README.md | ||
| SECURITY.md | ||
| validate.sh | ||
Printago Raspberry Pi kiosk image
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
.imgis only a delivered artifact afterbuild-image.shfinishes 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:
-
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' -
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. Afterbuild-image.shsucceeds:sudo bash tools/inspect-image-linux.sh dist/printago-kiosk-*.img.xzThis 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.
-
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.logA 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.ymlruns all three software levels on a self-hosted runner labeledprintago-pi-builder. Its first step runs the read-onlytools/check-builder-linux.shcapability audit. The final upload usesif: 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 whenSERIAL_DEVICEexists.hid-keyboard: keyboard-wedge scanner only.serial: convert lines fromSERIAL_DEVICEinto 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:
- Flash
dist/printago-kiosk-*.img.xzwith Raspberry Pi Imager orxzcat ... | sudo dd of=/dev/sdX bs=4M conv=fsync status=progress. - Reinsert the card and mount its
bootfspartition. - Copy
config/kiosk-config.env.exampletobootfs/printago-kiosk.envand 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, useconfig/kiosk-config.channel-managed.env.exampleand leaveDASHBOARD_URLempty. Do not add shell syntax; this is parsed as data, not sourced. - 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.
- Sign in to Printago once. The dedicated Chromium profile persists that login across browser restarts and power cycles.
- To change settings later, place a new
printago-kiosk.envon bootfs and runsudo systemctl restart printago-kiosk-firstboot.service, or runsudo printago-kiosk-configure /path/to/configlocally, 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
serialmode, scan through the configured/dev/serial/by-id/...device. The page receives the complete code once, followed by Enter. - In
webmode, exercise the dashboard's current WebHID POS and WebSerial workflows and reboot to record each device's permission and reconnection behavior. Record WebUSB separately asnot-supportedornot-testedwhile 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.