VoxIP Kiosk OS 1.0.0: Raspberry Pi kiosk with web management
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:
commit
a33c4822bf
96 files changed
+7535
No files matched your search
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user