200 lines
11 KiB
Markdown
200 lines
11 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 (`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
|
|
|
|
```
|
|
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 (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
|
|
```
|