Files
daynivo-kiosk-os/docs/ARCHITECTURE.md
T
rothmanandClaude Opus 5.5 b0840d240e
CI / test-bookworm (push) Successful in 25s
CI / test-latest (push) Successful in 31s
Daynivo names for the files on the SD card and the command line tool
- daynivo-config.json, daynivo-authorized_keys and daynivo-network-reset
  on the boot partition; errors go to daynivo-config.error.txt.
- daynivo-kiosk-ctl, with voxip-kiosk-ctl as a symlink to it.
- The voxip-* names keep working, so existing SD cards and instructions
  stay valid; when both config files are present the Daynivo one is
  applied last.
- Internal names (services, /etc/voxip-kiosk, user voxip) are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:25:42 +01:00

204 lines
12 KiB
Markdown

# 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 (`daynivo-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, updates controleren en starten. |
| `voxip-kiosk-updater` | `root` (timer, of eigen transient unit via de helper) | Haalt de nieuwste release op (HTTPS, SHA-256), pakt hem uit in `/opt/daynivo-kiosk-os/<versie>` en draait `update.sh --no-pull`. Nooit een oudere of gelijke versie. |
| `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 `daynivo-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 `daynivo-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` / de updater) overschrijven
nooit bestaande configuratie. De updater draait in een eigen systemd-unit,
zodat het herstarten van de beheerinterface tijdens `install.sh` de update
niet afbreekt. De checksum komt van dezelfde server als de download: hij
vangt een kapotte download af, geen gehackte releaseserver.
### 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 `daynivo-authorized_keys` op de
bootpartitie (dan alleen sleutel-login).
- Strikte security-headers en CSP (`default-src 'self'`).
## Directorystructuur
```
daynivo-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 (daynivo-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
```