VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
CI / test-bookworm (push) Failing after 11s
CI / test-latest (push) Successful in 1m27s

Chromium kiosk without a desktop for the Raspberry Pi 3, a Flask web
interface on port 8080 (kiosk, system and network management), a
restricted root helper, network changes with automatic rollback, a
healthcheck timer, install/update/uninstall scripts and a pi-gen image
build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rothmanandClaude Opus 5.5 committed 2026-11-04 21:38:49 +01:00
commit a33c4822bf
96 files changed
+7535

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
# Architectuur — VoxIP Kiosk OS
Dit document is het architectuurvoorstel waarop de implementatie in deze
repository gebaseerd is. Het beschrijft de onderdelen, hun rechten, hoe ze
samenwerken en waarom bepaalde keuzes zijn gemaakt.
## Doel en randvoorwaarden
- Raspberry Pi 3 Model B/B+ (1 GB RAM), Raspberry Pi OS Lite 32-bit
(Bookworm of Trixie), 24/7 in bedrijf.
- Eén HTTPS-pagina fullscreen in Chromium, zonder desktopomgeving.
- Lokale webbeheerinterface (*VoxIP Kiosk Manager*) op poort 8080.
- Geen klantspecifieke code: alles wat per klant verschilt staat in
`/etc/voxip-kiosk/config.json` en kan bij uitrol via de bootpartitie
worden meegegeven (`voxip-config.json`).
## Overzicht
```
┌──────────────────────── Raspberry Pi ────────────────────────┐
│ │
HDMI ◄────────┤ voxip-kiosk-x.service Xorg :0 op vt7 (user kiosk) │
│ ▲ │
│ │ DISPLAY=:0 │
│ voxip-kiosk-browser.service Chromium --kiosk (user kiosk) │
│ ▲ CDP 127.0.0.1:9222 │
│ │ ┌─────────────────────────────┐ │
│ ├──┤ voxip-kiosk-agent.service │ navigatie, offline-│
│ │ │ (user kiosk) │ pagina, verversen, │
│ │ └─────────────────────────────┘ hang-detectie │
│ │ │
LAN :8080 ───►│ voxip-kiosk-manager.service Flask + gunicorn (user voxip) │
│ │ sudo -n (één commando) │
│ ▼ │
│ voxip-kiosk-helper root, vaste werkwoorden, │
│ strikte invoervalidatie │
│ │
│ voxip-kiosk-healthcheck.timer elke minuut, root │
│ voxip-network-guard.service netwerk-rollback bij boot │
│ voxip-boot-import.service config/SSH-sleutels vanaf │
│ de bootpartitie │
└──────────────────────────────────────────────────────────────┘
```
### Processen en rechten
| Onderdeel | Gebruiker | Taak |
|---|---|---|
| `voxip-kiosk-x` | `kiosk` (via Xorg.wrap) | Minimale X-server op vt7, zonder cursor, zonder TCP. |
| `voxip-kiosk-browser` | `kiosk` | Past rotatie/resolutie/DPMS toe (xrandr/xset), ruimt een vastgelopen profiel op en start Chromium. |
| `voxip-kiosk-agent` | `kiosk` | Bestuurt Chromium via het DevTools Protocol op localhost: offline-pagina, URL-wissel zonder herstart, periodiek verversen, hang-detectie. |
| `voxip-kiosk-manager` | `voxip` | Flask-webinterface (gunicorn, 1 worker, 4 threads). |
| `voxip-kiosk-helper` | `root` (via sudo) | Enige geprivilegieerde ingang: reboot, uitschakelen, Chromium herstarten, hostname, wifi-land, wifi-scan, netwerkconfiguratie + rollback. |
| `voxip-kiosk-healthcheck` | `root` | Bewaakt alle services en herstelt ze, met logging. |
De webinterface draait **nooit** als root. De sudoers-regel staat
uitsluitend `/usr/lib/voxip-kiosk/voxip-kiosk-helper` toe. De helper:
- draait met `python3 -I` (geen `PYTHONPATH`, geen user site-packages);
- kent alleen een vaste lijst werkwoorden; interne werkwoorden (zoals
`network-activate`) weigert hij als hij via sudo wordt aangeroepen;
- valideert iedere parameter opnieuw (de webinterface is niet vertrouwd);
- start alle subprocessen met een argumentenlijst, nooit via een shell;
- schrijft netwerkprofielen zelf als NetworkManager-keyfile (0600), zodat
wachtwoorden nooit op een commandoregel verschijnen.
### Waarom X11 + twee services
Xorg en Chromium zijn aparte services. Daardoor is *Chromium herstarten*
snel (X blijft draaien) en geeft het exitcode van Chromium direct aan
systemd door. `voxip-kiosk-browser` heeft `Requires=` en `PartOf=` op
`voxip-kiosk-x`: valt X weg, dan start de browser opnieuw mee.
### Chromium besturen via CDP
Chromium luistert met `--remote-debugging-address=127.0.0.1
--remote-debugging-port=9222`, dus alleen lokaal. De agent en de
webinterface gebruiken dit om:
- de pagina te herladen zonder de browser te herstarten;
- naar een nieuwe URL te navigeren na een configuratiewijziging;
- een screenshot te maken;
- te detecteren of de renderer nog reageert (`Runtime.evaluate`).
### Offline-gedrag
Chromium start altijd op een lokale startpagina
(`/usr/share/voxip-kiosk/www/start.html`). De agent controleert met een
HTTPS-verzoek (mét certificaatvalidatie) of de doel-URL bereikbaar is en
navigeert pas dan. De offline-pagina verschijnt alleen als de pagina niet
**geladen** kan worden (startpagina, Chromium-foutpagina). Valt het
netwerk weg terwijl een pagina al draait, dan blijft die pagina staan.
Zo blijven applicaties met een eigen offline-cache (zoals InfoFlow-
displays) gewoon hun laatste inhoud tonen. Automatisch verversen wordt
overgeslagen zolang de doel-URL onbereikbaar is.
### Initiële configuratie
Zolang er geen beheerderswachtwoord is:
1. maakt de webinterface een eenmalige **setupcode** aan;
2. toont de kiosk op het HDMI-scherm een setuppagina met die code en de
adressen van de beheerinterface;
3. kan een wachtwoord alleen ingesteld worden met die code.
Wie fysiek bij het scherm staat (of via SSH `voxip-kiosk-ctl setup-code`
draait) kan het apparaat dus claimen, iemand anders op het netwerk niet.
### Netwerkwijzigingen met rollback
1. De helper maakt een snapshot van `/etc/NetworkManager/system-connections`
en van de actieve verbindingen.
2. Hij schrijft het nieuwe profiel (`voxip-eth0` / `voxip-wlan0`, prioriteit
100) en plant een rollback-timer (`systemd-run`, 180 s).
3. Activatie gebeurt asynchroon, zodat het HTTP-antwoord nog aankomt.
4. De beheerder bevestigt binnen 180 s (eventueel via het nieuwe adres),
anders zet de timer de snapshot terug.
5. Valt de stroom uit tijdens die periode, dan zet
`voxip-network-guard.service` de snapshot terug bij het opstarten, nog
vóór NetworkManager start.
6. Noodherstel: een leeg bestand `voxip-network-reset` op de bootpartitie
verwijdert bij de volgende boot alle VoxIP-netwerkprofielen (terug naar
DHCP).
### Configuratie
- `/etc/voxip-kiosk/config.json`: eigenaar `voxip`, groep `kiosk`, 0640.
Wordt atomair opgeslagen (tijdelijk bestand, `fsync`, `rename`, `fsync`
van de map) met een `.bak` van de vorige versie. Ongeldige waarden vallen
bij het laden terug op de standaard, zodat de kiosk altijd start.
Onbekende sleutels blijven bewaard (vooruit-/achteruitcompatibel).
- `/etc/voxip-kiosk/admin.json`: Argon2id-hash, 0600, alleen `voxip`.
- Updates (`install.sh --upgrade` / `update.sh`) overschrijven nooit
bestaande configuratie.
### Betrouwbaarheid (24/7)
- Alle services `Restart=` met `StartLimitBurst`; de healthcheck
logt een *start-limit-hit* als fout en probeert na 5 minuten opnieuw.
Er zijn dus geen stille herstartloops en geen permanente uitval.
- Hardware-watchdog (`RuntimeWatchdogSec=15s`): een vastgelopen kernel
zorgt voor een automatische reboot.
- Chromium-profiel: profielvergrendelingen en de crash-vlag worden voor
iedere start opgeruimd, zodat er na stroomuitval geen dialoog verschijnt.
- HTTP-cache van Chromium in RAM (`/run`), het profiel zelf (cookies,
localStorage/IndexedDB) blijft standaard persistent zodat offline-caches
van webapps een reboot overleven (`browser_profile: "ephemeral"` zet ook
dat in RAM).
- journald maximaal 32 MB en synchroniseert eens per 15 minuten;
`/tmp` als tmpfs; zram in plaats van swap op de SD-kaart.
- Logrotatie voor het audit-log van de beheerinterface.
### Beveiliging
- Argon2id (`argon2-cffi`, 64 MiB, t=3, p=2), geen standaardwachtwoord.
- Sessiecookie: `HttpOnly`, `SameSite=Strict`, `Secure` bij HTTPS, 60 min
inactiviteit, ongeldig na een wachtwoordwijziging (sessie-epoch).
- CSRF-token op iedere POST, plus controle van de `Origin`-header.
- Login-throttling per IP (exponentieel tot 15 min) en globaal.
- Beheerinterface weigert standaard verzoeken van publieke IP-adressen
(toegestaan: privé, link-local, loopback, CGNAT/VPN 100.64.0.0/10).
- Optioneel HTTPS (zelfondertekend certificaat, automatisch aangemaakt).
- SSH staat in de image standaard uit en wordt alleen ingeschakeld met
credentials uit Raspberry Pi Imager of `voxip-authorized_keys` op de
bootpartitie (dan alleen sleutel-login).
- Strikte security-headers en CSP (`default-src 'self'`).
## Directorystructuur
```
voxip-kiosk-os/
├── README.md bouw-, flash-, installatie-instructies
├── VERSION
├── Makefile test / lint / image
├── install.sh installatie op bestaande Pi OS + gebruikt door image-build
├── update.sh update met backup van configuratie
├── uninstall.sh verwijderen (--purge voor alles)
├── bin/ dunne launchers → /usr/lib/voxip-kiosk/
├── src/voxip_kiosk/ Python-package (alle logica)
│ ├── config.py laden/valideren/atomair opslaan
│ ├── validation.py gedeelde invoervalidatie
│ ├── cdp.py Chrome DevTools Protocol-client
│ ├── browser.py Chromium-launcher (xrandr, xset, profiel)
│ ├── agent.py kiosk-agent
│ ├── healthcheck.py healthcheck
│ ├── helper.py root-helper
│ ├── netconfig.py NetworkManager-keyfiles, nmcli-parsing
│ ├── netinfo.py netwerkstatus (zonder rechten)
│ ├── sysinfo.py CPU, temperatuur, geheugen, opslag, IP
│ ├── services.py systemd- en journal-status
│ ├── ctl.py CLI voor SSH-beheer (voxip-kiosk-ctl)
│ └── web/ Flask-app, templates, statische bestanden
├── www/ lokale kioskpagina's (start, offline, setup)
├── systemd/ unit-bestanden
├── config/ standaardconfig, sudoers, journald, logrotate, …
├── image/ pi-gen image-builder (stage-voxip)
├── tests/ pytest-suite
└── docs/ architectuur, hardwaretest
```
+125
View File
@@ -0,0 +1,125 @@
# Testprocedure op Raspberry Pi 3-hardware
De geautomatiseerde tests (`make test`, `make test-bookworm`) dekken de
configuratie, validatie, de webinterface, de helper (inclusief
netwerk-rollback), de agent en de healthcheck. Wat alleen op echte hardware
te controleren is, staat hieronder. Voer dit uit voor iedere nieuwe release
en voor elk nieuw type scherm.
**Benodigd:** Raspberry Pi 3 Model B of B+, officiële voeding (5,1 V/2,5 A),
nieuwe microSD-kaart van 16 GB of groter, HDMI-monitor, ethernetkabel,
een wifi-netwerk, een laptop op hetzelfde netwerk.
Noteer per stap: ✅ / ❌ + opmerking, de imageversie en het schermtype.
## 1. Flashen en eerste boot
- [ ] Schrijf de `.img.xz` met Raspberry Pi Imager **zonder** OS-aanpassingen.
- [ ] Eerste boot: de rootpartitie wordt vergroot (`df -h /` toont later
ongeveer de kaartgrootte). De Pi herstart zichzelf eenmaal.
- [ ] Binnen 90 s na stroom aan verschijnt het setupscherm met de setupcode,
`http://kiosk.local:8080` en het IP-adres. Er is geen muiscursor en
er zijn geen dialoogvensters.
- [ ] SSH is uit (`ssh voxipadmin@kiosk.local` wordt geweigerd), en er is
geen bruikbaar standaardwachtwoord.
## 2. Initiële configuratie
- [ ] `http://kiosk.local:8080` en `http://<ip>:8080` tonen de setuppagina.
- [ ] Een verkeerde setupcode wordt geweigerd. Na 5 pogingen volgt een wachttijd.
- [ ] Een zwak wachtwoord (`admin12345`) wordt geweigerd.
- [ ] Met de juiste code en een sterk wachtwoord: dashboard. Het scherm
schakelt binnen 10 s naar de kiosk-URL.
- [ ] De setupcode werkt daarna niet meer (`/setup` stuurt door).
## 3. Kiosk
- [ ] De pagina is fullscreen, zonder adresbalk, scrollbalk-pop-ups,
vertaalbalk of "Chromium is niet correct afgesloten".
- [ ] Na 30 minuten is het scherm niet gedimd of zwart geworden.
- [ ] **Pagina herladen** (dashboard): de pagina herlaadt binnen 2 s, zonder
zwart scherm (Chromium herstart niet).
- [ ] **Chromium herstarten**: kort zwart scherm, binnen 15 s weer de pagina.
- [ ] Een andere URL instellen (Kiosk → Instellingen): binnen 10 s getoond,
zonder herstart van Chromium (de teller *Herstarts* blijft gelijk).
- [ ] Een `http://`-URL wordt geweigerd.
- [ ] Verversinterval op *Elke minuut*: de pagina herlaadt ongeveer elke minuut
(zie Logboek → Kiosk-agent).
- [ ] Screenshot op de Kiosk-pagina toont wat er op het scherm staat.
- [ ] InfoFlow-URL met `?token=`: het token is overal gemaskeerd
(dashboard, `voxip-kiosk-ctl status`, `journalctl -u voxip-kiosk-agent`).
## 4. Herstel
- [ ] `sudo pkill -9 chromium`: binnen 15 s is de pagina terug. Het logboek
toont de herstart.
- [ ] `sudo pkill -9 Xorg`: X en Chromium komen binnen 20 s terug.
- [ ] Crashloop: `sudo voxip-kiosk-ctl set-url https://localhost:1/` en
daarna 7× `sudo pkill -9 chromium` binnen 2 minuten. De service komt
in de fouttoestand (start-limit). De healthcheck logt een fout en start
hem na ongeveer 5 minuten opnieuw. Er is geen eindeloze stille lus.
Zet de URL daarna terug.
- [ ] Vastgelopen pagina: open via SSH-tunnel DevTools of zet een testpagina met
`while(true){}` in. Na ±15 s herlaadt de agent, na ±30 s herstart Chromium.
- [ ] `sudo systemctl stop voxip-kiosk-agent`: de healthcheck start hem binnen 2 minuten.
- [ ] De webinterface blijft werken terwijl Chromium gestopt is
(`sudo systemctl stop voxip-kiosk-browser`).
## 5. Netwerk
- [ ] Netwerkkabel eruit terwijl de pagina draait: de pagina **blijft
staan** (een InfoFlow-display toont zijn laatste inhoud). Het
dashboard toont *Offline*.
- [ ] Kabel weer erin: het dashboard toont binnen 1 minuut *Online*.
- [ ] Opstarten zonder netwerk: offlinepagina met klok. Kabel erin:
automatisch naar de kiosk-URL.
- [ ] Wifi scannen toont netwerken. Wifi instellen (met kabel nog erin):
wlan0 krijgt een adres. Bevestigen werkt.
- [ ] Statisch IP op eth0 in een ander adres van hetzelfde subnet:
beheer bereikbaar op het nieuwe adres, bevestigen daar.
- [ ] **Rollback:** stel een statisch IP in een verkeerd subnet in en bevestig
niet. Na 3 minuten is het oude adres weer bereikbaar.
- [ ] **Rollback na stroomuitval:** herhaal, trek binnen 3 minuten de stroom
eruit. Na het opstarten zijn de oude instellingen actief.
- [ ] **Noodreset:** stel iets onbereikbaars in en bevestig het. Zet
`voxip-network-reset` op de bootpartitie en start: weer DHCP.
## 6. Systeem
- [ ] Rotatie 90° en 270° (met *direct toepassen*): beeld gedraaid, pagina
vult het scherm.
- [ ] Resolutie 1280x720: het scherm schakelt om. Een niet-ondersteunde resolutie
valt terug op automatisch.
- [ ] *Scherm altijd aan* uit, 1 minuut: na een minuut gaat het scherm uit
(DPMS). Zet het weer aan.
- [ ] Hostname wijzigen naar `test-kiosk`: `http://test-kiosk.local:8080` werkt.
- [ ] HTTPS aan: doorgestuurd naar `https://…:8080`. Na bevestiging van het
certificaat werkt inloggen.
- [ ] **Veilig uitschakelen**: de groene LED stopt met knipperen. De Pi komt pas terug na stroom eruit/erin.
- [ ] **Raspberry Pi herstarten**: na ±60 s weer kiosk + beheer.
## 7. Betrouwbaarheid (24/7)
- [ ] 10× stroom eruit tijdens normaal bedrijf (willekeurige momenten): steeds
zonder ingrijpen terug in de kiosk, zonder "herstellen"-melding.
- [ ] 72 uur continu draaien met verversinterval 5 min. Daarna: geheugen
< 80% (dashboard), geen onderspanning, `journalctl -p err -b` zonder
terugkerende fouten, `voxip-kiosk-ctl status` alles *Actief*.
- [ ] SD-kaartschrijfacties: `cat /proc/diskstats | grep mmcblk0` op t=0 en
na 1 uur stationair. Het verschil in geschreven sectoren (kolom 10) is
minder dan 50 MB per uur.
- [ ] Hardware-watchdog actief: `systemctl show -p RuntimeWatchdogUSec` toont `15s`.
## 8. Uitrol en updates
- [ ] Nieuwe kaart met `voxip-config.json` (url, device_name, hostname,
rotation) en `voxip-authorized_keys` op de bootpartitie: bij de eerste
boot toegepast, hernoemd naar `.applied`, SSH alleen met sleutel.
- [ ] Ongeldige `voxip-config.json` (http-URL): `.rejected` en
`voxip-config.error.txt` met de reden. De kiosk start met de standaardconfiguratie.
- [ ] `sudo ./update.sh` vanuit een nieuwere checkout: versie in het dashboard
verhoogd, configuratie en wachtwoord ongewijzigd, back-up in `/var/backups`.
- [ ] `sudo ./uninstall.sh --purge` op een testkaart: geen VoxIP-services of
-bestanden meer (`systemctl list-unit-files | grep voxip` is leeg).
- [ ] Installatie op een bestaande Raspberry Pi OS Lite (Bookworm) met de
InfoFlow-kiosk erop: URL overgenomen, `display-kiosk.service` uitgeschakeld.