Discovery zeigte: YunoHost reicht den Usernamen nicht als Header durch, sondern im signierten Cookie "yunohost.portal" (JWT, Claim "user"). Die Forward-Auth liest jetzt die Identitaet aus Header ODER Cookie - nur von der Proxy-Quell-IP akzeptiert. HS256-Signaturpruefung optional via TRUSTED_AUTH_JWT_SECRET (stdlib hmac, kein Dep). - config: trusted_auth_cookie / _cookie_claim / _jwt_secret - nginx-Vorlage + deploy/README: keine Identitaets-Header-Zeile mehr noetig; SSO-Schutz der Subdomain ist Pflicht (sonst Spoofing) - Tests: Cookie-Extraktion, Signaturpruefung, Proxy-IP-Trust Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
3.2 KiB
Markdown
67 lines
3.2 KiB
Markdown
# Remote-Betrieb über YunoHost (va.linix.de)
|
||
|
||
Web-UI + Mikrofon vom Handy/Browser, abgesichert per HTTPS und YunoHost-SSO.
|
||
Topologie: `https://va.linix.de` → nginx@YunoHost (TLS, SSO) → über LAN →
|
||
`http://<GPU-Box>:8003` (Gateway). Der interne LAN-Hop ist unverschlüsselt
|
||
(vertrautes LAN, wie bei Ollama).
|
||
|
||
> **Warum HTTPS Pflicht ist:** Browser geben das Mikrofon (`getUserMedia`) nur in
|
||
> einem „secure context" frei – also HTTPS oder `http://localhost`. Über einfaches
|
||
> `http://` vom Handy gibt es **kein** Mikrofon.
|
||
|
||
## 1. Gateway auf der GPU-Box
|
||
|
||
`deploy/voice-assistant.env.example` → `/etc/voice-assistant/voice-assistant.env` anpassen:
|
||
- `HOST` = **LAN-IP** der GPU-Box (nicht `0.0.0.0`), `PORT=8003`
|
||
- `AUTH_ENABLED=true`
|
||
- `TRUSTED_AUTH_HEADER` (nach Discovery, s. u.), `TRUSTED_PROXY_IPS` = LAN-IP von linix.de
|
||
- `ADMIN_USERS=atoor,dieterschlueter,dschlueter`, `SSO_LOGOUT_URL=…`
|
||
|
||
> Kein `--forwarded-allow-ips` setzen: Das Identitäts-Vertrauen prüft die
|
||
> **direkte Quell-IP** (= der Proxy). Würde uvicorn den Client aus
|
||
> `X-Forwarded-For` überschreiben, schlüge der Proxy-IP-Check fehl.
|
||
|
||
## 2. Drei Sicherheits-Pflichten
|
||
1. **Bind:** Gateway nur an die LAN-IP (Schritt 1).
|
||
2. **Firewall:** Port 8003 der GPU-Box **nur** von der linix.de-IP erlauben, z. B.:
|
||
```
|
||
sudo ufw allow from <linix.de_LAN_IP> to any port 8003 proto tcp
|
||
sudo ufw deny 8003
|
||
```
|
||
3. **Header überschreiben:** nginx setzt den Identitäts-Header selbst; Client-Eingaben
|
||
werden verworfen (siehe nginx-Conf). App-seitig zusätzlich der Proxy-IP-Check.
|
||
|
||
## 3. nginx auf YunoHost
|
||
`deploy/va.linix.de.nginx.conf` als Vorlage → auf dem YunoHost-Server unter
|
||
`/etc/nginx/conf.d/va.linix.de.d/assistant.conf` ablegen, `GPU_BOX_LAN_IP`
|
||
eintragen, die `map $http_upgrade …` einmalig im http{}-Kontext anlegen, dann
|
||
`nginx -t && systemctl reload nginx`. Subdomain `va.linix.de` in YunoHost
|
||
anlegen (Let's Encrypt) und per SSO schützen (nur erlaubte Tester/Gruppe).
|
||
|
||
## 4. Identität: YunoHost liefert sie im Cookie (nicht als Header)
|
||
Discovery (`/api/admin/request-headers?key=<ADMIN_API_KEY>` hinter dem SSO) zeigt:
|
||
YunoHost reicht den Usernamen **nicht** als eigenen Header durch, sondern im **JWT-Cookie
|
||
`yunohost.portal`** (Claim `user`). Das Gateway liest diesen Cookie direkt — daher:
|
||
|
||
```ini
|
||
# .env auf der GPU-Box:
|
||
TRUSTED_AUTH_COOKIE=yunohost.portal
|
||
TRUSTED_AUTH_COOKIE_CLAIM=user
|
||
TRUSTED_PROXY_IPS=<LAN-IP des YunoHost-Servers>
|
||
# optional (Härtung): HS256-Secret des Portals -> Signaturpruefung
|
||
# TRUSTED_AUTH_JWT_SECRET=...
|
||
```
|
||
|
||
In der nginx-Conf ist **keine** `proxy_set_header`-Identitätszeile nötig — Cookies
|
||
werden ohnehin durchgereicht.
|
||
|
||
> **Sicherheit:** Ohne `TRUSTED_AUTH_JWT_SECRET` wird die Cookie-Payload ungeprüft
|
||
> gelesen. Das ist nur sicher, weil (a) nur die Proxy-Quell-IP akzeptiert wird **und**
|
||
> (b) die Subdomain **per SSO geschützt** sein muss (dann lässt YunoHost nur validierte
|
||
> Cookies durch). Für Härtung das Portal-HS256-Secret in `TRUSTED_AUTH_JWT_SECRET`
|
||
> setzen → die Signatur wird dann selbst geprüft.
|
||
|
||
## 5. Test
|
||
- `https://va.linix.de/` lädt die UI, links „Angemeldet als <SSO-User>".
|
||
- Text-Chat funktioniert; **Mikrofon-Button** nimmt auf und spielt die Antwort ab.
|
||
- Admins (`ADMIN_USERS`) sehen den Admin-Bereich (Nutzerliste).
|