# Architectuur — Daynivo 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 (*Daynivo 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 Daynivo- 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 Daynivo-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 ```