Step 0: Write your threat model
This guide is practical, but the details depend on your risks. Start by answering these core questions to define your boundaries:
- What happens if they get USB access?
- What happens if they power-cycle repeatedly?
- Do you need offline-first behavior?
- What data must never be stored locally?
Step 1: Use a clean disk layout
The mistake most teams make is making “everything writable”. Instead, separate the immutable OS from controlled persistence.
Partition A: root (immutable)
Mount read-only. All OS binaries, configs, and services live here.
Partition B: persistence
Only logs, device identity, approved app cache, and update metadata.
Rule of thumb: if it’s not explicitly required to persist, it should reset on reboot.
Step 2: Implement OverlayFS
OverlayFS lets you mount the base OS as read-only, while writes go into an upper layer (on your persistence partition or tmpfs). On reboot you can wipe the upper layer and return to a clean state.
# Example paths (adjust to your distro and partitions)
sudo mkdir -p /persist/overlay/upper
sudo mkdir -p /persist/overlay/work
# Optional: keep app cache separate so you can wipe it safely
sudo mkdir -p /persist/app-cache# lowerdir = immutable root filesystem content
# upperdir = writable changes
# workdir = required by OverlayFS
mount -t overlay overlay \
-o lowerdir=/sysroot,upperdir=/persist/overlay/upper,workdir=/persist/overlay/work \
/newrootStep 3: Decide what’s allowed to persist
Your kiosk becomes stable when persistence is intentional. Be extremely strict with your allowed list.
- Device identity (a device ID, public key, provisioning token)
- Crash logs and health telemetry
- Approved app cache (limited size, easy to wipe)
- Update metadata (current version, rollback state)
Step 4: Lock down the runtime session
Run your app under a dedicated low-privilege user. Disable unused TTYs, interactive shells, USB, Bluetooth, and external storage where possible. Use a kiosk mode browser with no window manager escape routes.
Step 5: Make boot deterministic
Your app should come up the same way every time, and restart if it crashes. Use systemd to enforce ProtectSystem=strict and explicitly whitelist ReadWritePaths.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/persist /var/logStep 6: Add a watchdog
Watchdogs are boring — and that’s why they’re great. If your app freezes, the hardware watchdog (or software equivalent) resets the device automatically.
Step 7: Update rollbacks
Start with signed artifacts and staged rollouts. Move towards A/B partitions or known-good snapshots so a botched update simply reboots into the previous version.
Step 8: Validate like a kiosk
Run this checklist before rollout to catch field-failures early.
- → Power-cut test: Unplug during a write, reboot, verify clean recovery.
- → Network-loss test: Disconnect for 20 mins, ensure app stability.
- → Crash test: Kill the app repeatedly, watch systemd restart it.
- → Persistence test: Reboot 10 times, verify no "mystery state" drift.
- → Tamper test: Plug in a keyboard/USB, verify escapes are blocked.
Frequently Asked Questions
Does read-only root mean the kiosk can’t store anything?↓
No. The goal is controlled persistence. Keep a small writable partition for device identity, logs, and approved cache. Everything else resets on reboot.
Should the OverlayFS upper layer live on disk or tmpfs?↓
If you want “always clean on reboot”, tmpfs is great. If you need controlled persistence across reboots, store it on the persistence partition — but keep it minimal and easy to wipe.
What’s the fastest win if we’re shipping soon?↓
Start with: systemd restart policies + locked-down user + controlled persistence. Then add read-only root + watchdog as the next hardening layer.