From 6ab24977f17254fe309126aeb4bcad770387156e Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 1 Jul 2026 10:26:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20Installations-Skript=20+=20Anle?= =?UTF-8?q?itung=20f=C3=BCr=20natives=20YunoHost-SSO=20(jamulix.net)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alternative zum Authelia-Target (voice.jamulix.de): eigenständiges Ein-Maschinen-Setup, das YunoHosts Portal-Cookie (yunohost.portal) direkt nutzt statt Authelia. deploy/install_va_on_yunohost.sh automatisiert Code- Deploy, venv, systemd-Service, YunoHost-Domain und nginx-Reverse-Proxy inkl. WebSocket-Support; INSTALLATION_AUF_YUNOHOST_SERVER.md dokumentiert Ablauf, Sicherheitsmodell und offene manuelle Schritte. Co-Authored-By: Claude Sonnet 5 --- INSTALLATION_AUF_YUNOHOST_SERVER.md | 141 ++++++++++++++++++++ deploy/install_va_on_yunohost.sh | 195 ++++++++++++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 INSTALLATION_AUF_YUNOHOST_SERVER.md create mode 100755 deploy/install_va_on_yunohost.sh diff --git a/INSTALLATION_AUF_YUNOHOST_SERVER.md b/INSTALLATION_AUF_YUNOHOST_SERVER.md new file mode 100644 index 0000000..5a2d36e --- /dev/null +++ b/INSTALLATION_AUF_YUNOHOST_SERVER.md @@ -0,0 +1,141 @@ +# Installation auf einem YunoHost-Server (natives SSO, ohne Authelia) + +Alternative Installationsvariante zu `DEPLOYMENT.md` (jamulix.de, Authelia 2FA). +Hier läuft die Anmeldung stattdessen über **YunoHosts eigenes Portal-SSO** — kein +separat installiertes/gepflegtes Authelia nötig. Zielsystem in diesem Fall: +Debian-Server mit YunoHost, IP `82.153.225.151`, Domain `jamulix.net`. + +Repository: `https://kitux.de/forgejo/dschlueter/my_voice_assistant_v3_jamulix` + +## Architektur + +``` +Browser --HTTPS--> nginx (YunoHost, TLS + Portal-SSO) + | + v + 127.0.0.1:8003 (Gateway, systemd, User "voice") +``` + +Gateway und nginx laufen auf **derselben Maschine** — anders als beim bereits +produktiven `va.linix.de`-Aufbau (`deploy/README.md`), wo Gateway und +YunoHost-Reverse-Proxy auf zwei getrennten Rechnern im LAN sitzen. Das +vereinfacht die Firewall-Frage (kein LAN-Hop, kein offener Port nach außen) +und erlaubt zusätzliche Härtung (siehe unten). + +**Identität:** YunoHost reicht den eingeloggten Benutzernamen nicht als +HTTP-Header, sondern im **JWT-Cookie `yunohost.portal`** (Claim `user`) +durch — empirisch bestätigt bei der `va.linix.de`-Installation. Das Gateway +liest diesen Cookie direkt (`app/auth.py`, Einstellungen `TRUSTED_AUTH_COOKIE` +/ `TRUSTED_AUTH_COOKIE_CLAIM` / `TRUSTED_AUTH_JWT_SECRET`, siehe +`.env.example`). Ein zusätzlicher YunoHost-Header ist **nicht** nötig. + +## Voraussetzungen + +- SSH-Root-Zugriff auf den YunoHost-Server. +- DNS: A-Record der gewünschten Subdomain (z. B. `voice.jamulix.net`) zeigt + bereits auf `82.153.225.151`. +- Dein YunoHost-Benutzername (das Konto, mit dem du dich später einloggst), + z. B. `dschlueter`. +- Ein OpenRouter-API-Key (Cloud-Profil: STT/LLM/TTS laufen über OpenRouter, + keine lokale GPU/Modelle nötig). + +## Installation + +```bash +ssh root@82.153.225.151 +``` + +Skript auf den Server holen (z. B. per `scp` von diesem Repo, oder direkt aus +Forgejo, falls Netzzugriff besteht) und ausführen: + +```bash +scp deploy/install_va_on_yunohost.sh root@82.153.225.151:~/ +ssh root@82.153.225.151 +sudo bash ~/install_va_on_yunohost.sh voice.jamulix.net dschlueter +``` + +Das Skript (`deploy/install_va_on_yunohost.sh`) fragt einmalig nach dem +`OPENROUTER_API_KEY` (Enter = später manuell in +`/etc/voice-assistant/voice-assistant.env` nachtragen) und erledigt: + +1. System-Pakete (`git`, `python3-venv`, `python3-dev`, `build-essential`) + + Python-Versionscheck (Projekt braucht `>=3.11`). +2. System-User `voice` (falls nicht vorhanden). +3. `git clone` des Forgejo-Repos nach `/opt/voice-assistant` + (bei erneutem Lauf: `git pull --ff-only`). +4. Python-venv + Abhängigkeiten (nur Cloud-Profil — kein `faster-whisper`/ + `piper`/GPU-Kram nötig). +5. `/etc/voice-assistant/voice-assistant.env`: `HOST=127.0.0.1`, `PORT=8003`, + `VA_PROFILE=cloud`, `AUTH_ENABLED=true`, generiertes `ADMIN_API_KEY`, + `TRUSTED_AUTH_COOKIE=yunohost.portal`, `TRUSTED_AUTH_COOKIE_CLAIM=user`, + `TRUSTED_AUTH_JWT_SECRET` (aus `/etc/yunohost/.ssowat_cookie_secret` — + dadurch wird die Cookie-Signatur echt geprüft, nicht nur ungeprüft + gelesen), `TRUSTED_PROXY_IPS=127.0.0.1`, `ADMIN_USERS=`. + Existiert die Datei schon, wird sie **nicht** überschrieben. +6. systemd-Unit `voice-assistant.service` (`Restart=always`), aktiviert + + gestartet. +7. `yunohost domain add ` (übersprungen, falls schon registriert). +8. nginx-Snippet unter `/etc/nginx/conf.d/.d/voice-assistant.conf` + (überlebt `yunohost tools regen-conf nginx`, da app-eigener Pfad) mit + WebSocket-Upgrade-Headern für `/ws/voice` und `/ws/chat` (Mikrofon-Button). + +## Manuell danach (bewusst nicht automatisiert) + +```bash +dig +short voice.jamulix.net # zeigt auf 82.153.225.151? +yunohost domain cert-install voice.jamulix.net # Let's-Encrypt-Zertifikat +``` + +Danach einmal im Browser einloggen (setzt das `yunohost.portal`-Cookie): + +``` +https://voice.jamulix.net/yunohost/sso/ +``` + +und anschließend `https://voice.jamulix.net/` öffnen — die App sollte dich +automatisch als `dschlueter` erkennen (Admin-Bereich sichtbar). Fehlt der +`OPENROUTER_API_KEY`: in `/etc/voice-assistant/voice-assistant.env` +nachtragen, dann `systemctl restart voice-assistant`. + +## Sicherheitsmodell (Unterschied zu `va.linix.de`) + +Die Subdomain ist **nicht** über YunoHosts Permission-System gesperrt — es +gibt (noch) keinen automatischen Redirect zur Login-Seite bei Erstaufruf, wie +ihn ein echtes YunoHost-App-Package via SSOwat-Permission bekäme. Sicherheit +kommt stattdessen aus zwei Faktoren: + +1. **`TRUSTED_PROXY_IPS=127.0.0.1`** — die Identität aus dem Cookie wird nur + akzeptiert, wenn die Anfrage direkt vom lokalen nginx kommt (kein anderer + Rechner kann sich als "der Proxy" ausgeben). +2. **`TRUSTED_AUTH_JWT_SECRET`** — die Cookie-Signatur wird mit YunoHosts + echtem Portal-Secret geprüft (HS256). Ein gefälschter `yunohost.portal`- + Cookie eines Angreifers wird verworfen. + +Das ist strenger als bei `va.linix.de` (dort blieb das Secret mangels +Root-Zugriff auf dem Gateway-Rechner offen). Praktischer Nachteil: ohne +SSOwat-Redirect musst du dich **zuerst** manuell über `/yunohost/sso/` +einloggen, bevor `/` dich erkennt — es gibt keinen automatischen +"nicht eingeloggt → zur Login-Seite"-Sprung. + +## Troubleshooting + +| Symptom | Ursache | Fix | +|---|---|---| +| `yunohost: command not found` | Skript läuft auf dem falschen Rechner | Nur auf dem YunoHost-Server selbst ausführen | +| App erkennt dich nicht als eingeloggt | `yunohost.portal`-Cookie fehlt/falsche Domain | Erst `https:///yunohost/sso/` besuchen; Cookie-Domain im Browser-DevTools prüfen | +| 401 trotz Login | `TRUSTED_AUTH_JWT_SECRET` falsch/veraltet (Secret rotiert?) | `/etc/yunohost/.ssowat_cookie_secret` erneut auslesen und in die env eintragen | +| Mikrofon-Button reagiert nicht | WebSocket-Upgrade fehlt in nginx | `/etc/nginx/conf.d/.d/voice-assistant.conf` prüfen, `nginx -t && systemctl reload nginx` | +| Zertifikat schlägt fehl | DNS zeigt noch nicht auf den Server | `dig +short ` prüfen, ggf. TTL abwarten | +| Dienst startet nicht | Python < 3.11 oder fehlende venv-Pakete | `journalctl -u voice-assistant -f`; Python-Version prüfen (`python3 --version`) | + +Logs: `journalctl -u voice-assistant -f` + +## Ausblick (noch nicht umgesetzt) + +Geplant: Einbindung als `va.jamulix.net` über YunoHosts eigenes +**„Redirect"-App-Tool** statt des manuellen nginx-Snippets, inklusive einer +eigenen **„Alexis"-Kachel** im YunoHost-Dashboard/Portal. Das würde echtes +SSOwat-Permission-Handling (automatischer Login-Redirect, Portal-Kachel) +bringen, erfordert aber ein YunoHost-App-Package (`manifest.toml` + +Install-/Remove-/Upgrade-Skripte) statt des heutigen Ad-hoc-Skripts. Folgt in +einem späteren Schritt. diff --git a/deploy/install_va_on_yunohost.sh b/deploy/install_va_on_yunohost.sh new file mode 100755 index 0000000..fde20dd --- /dev/null +++ b/deploy/install_va_on_yunohost.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# Installiert den Voice-Assistant-Gateway auf einem YunoHost-Server und bindet +# die Identitaet ueber YunoHosts eigenes Portal-SSO an (Cookie "yunohost.portal"), +# analog zur bereits produktiven va.linix.de-Installation (siehe deploy/README.md), +# aber als Ein-Maschinen-Variante OHNE Authelia - nur natives YunoHost-Login. +# +# Voraussetzungen (manuell, VOR dem Ausfuehren): +# - SSH-Login auf dem YunoHost-Server als root (oder sudo-faehiger User). +# - DNS: A-Record der Subdomain zeigt bereits auf die Server-IP. +# - Du kennst deinen YunoHost-Benutzernamen (Authelia/LDAP-Konto in YunoHost), +# z. B. "dschlueter". +# +# Aufruf: +# sudo bash install_va_on_yunohost.sh +# +# Beispiel: +# sudo bash install_va_on_yunohost.sh voice.jamulix.net dschlueter +# +# Nach dem Lauf noch manuell noetig (siehe Ausgabe am Ende): +# - Zertifikat: yunohost domain cert-install +# - OPENROUTER_API_KEY nachtragen, falls beim Lauf leer gelassen. +# - Einmal ueber https:///yunohost/sso/ einloggen, dann die App oeffnen. + +set -euo pipefail + +DOMAIN="${1:?Nutzung: $0 }" +ADMIN_USERS="${2:?Nutzung: $0 }" + +REPO_URL="https://kitux.de/forgejo/dschlueter/my_voice_assistant_v3_jamulix.git" +APP_DIR="/opt/voice-assistant" +APP_USER="voice" +PORT="8003" +ENV_DIR="/etc/voice-assistant" +ENV_FILE="$ENV_DIR/voice-assistant.env" +DB_DIR="/var/lib/voice-assistant" +COOKIE_SECRET_FILE="/etc/yunohost/.ssowat_cookie_secret" + +# --- Vorbedingungen ---------------------------------------------------------- +if [ "$(id -u)" -ne 0 ]; then + echo "Bitte als root ausfuehren (sudo bash $0 ...)" >&2 + exit 1 +fi +command -v yunohost >/dev/null 2>&1 || { + echo "yunohost-CLI nicht gefunden - laeuft das hier wirklich auf dem YunoHost-Server?" >&2 + exit 1 +} + +echo "== 1/8: System-Pakete ==" +apt-get update -qq +apt-get install -y git python3-venv python3-pip python3-dev build-essential >/dev/null + +PYVER="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')" +if [ "$(printf '%s\n' "3.11" "$PYVER" | sort -V | head -n1)" != "3.11" ]; then + echo "WARNUNG: python3 ist $PYVER, das Projekt braucht >=3.11." >&2 + echo " Pruefe 'apt list -a python3.11' bzw. dein Debian-Release; danach das venv" >&2 + echo " in Schritt 4 manuell mit 'python3.11 -m venv .venv' anlegen." >&2 +fi + +echo "== 2/8: System-User '$APP_USER' ==" +id -u "$APP_USER" >/dev/null 2>&1 || useradd --system --create-home --shell /usr/sbin/nologin "$APP_USER" + +echo "== 3/8: Code holen ($REPO_URL) ==" +if [ -d "$APP_DIR/.git" ]; then + sudo -u "$APP_USER" git -C "$APP_DIR" pull --ff-only +else + mkdir -p "$APP_DIR" + chown "$APP_USER:$APP_USER" "$APP_DIR" + sudo -u "$APP_USER" git clone "$REPO_URL" "$APP_DIR" +fi + +echo "== 4/8: Python-venv + Abhaengigkeiten (Cloud-Profil, kein lokales STT/TTS) ==" +VENV_SETUP=/tmp/va_venv_setup.sh +cat > "$VENV_SETUP" <<'INNER' +#!/usr/bin/env bash +set -euo pipefail +cd "$1" +python3 -m venv .venv +.venv/bin/pip install -U pip +.venv/bin/pip install -e '.' +INNER +chmod +x "$VENV_SETUP" +sudo -u "$APP_USER" "$VENV_SETUP" "$APP_DIR" +rm -f "$VENV_SETUP" + +echo "== 5/8: Konfiguration ($ENV_FILE) ==" +mkdir -p "$ENV_DIR" "$DB_DIR" +chown "$APP_USER:$APP_USER" "$DB_DIR" + +if [ ! -f "$ENV_FILE" ]; then + ADMIN_API_KEY="$(openssl rand -hex 32)" + read -srp "OpenRouter API Key (OPENROUTER_API_KEY, Enter = spaeter manuell nachtragen): " OPENROUTER_API_KEY + echo + COOKIE_SECRET="" + if [ -r "$COOKIE_SECRET_FILE" ]; then + COOKIE_SECRET="$(cat "$COOKIE_SECRET_FILE")" + else + echo " WARNUNG: $COOKIE_SECRET_FILE nicht lesbar - TRUSTED_AUTH_JWT_SECRET bleibt leer" >&2 + echo " (Cookie wird dann ungeprueft gelesen, siehe deploy/README.md Abschnitt 4)." >&2 + fi + cat > "$ENV_FILE" < Quell-IP ist immer 127.0.0.1. +TRUSTED_AUTH_COOKIE=yunohost.portal +TRUSTED_AUTH_COOKIE_CLAIM=user +TRUSTED_AUTH_JWT_SECRET=$COOKIE_SECRET +TRUSTED_PROXY_IPS=127.0.0.1 +ADMIN_USERS=$ADMIN_USERS +SSO_LOGOUT_URL=https://$DOMAIN/yunohost/sso/?action=logout + +DB_PATH=$DB_DIR/voice-assistant.db +EOF + chown root:"$APP_USER" "$ENV_FILE" + chmod 640 "$ENV_FILE" +else + echo " $ENV_FILE existiert bereits - unveraendert gelassen." +fi + +echo "== 6/8: systemd-Service ==" +cat > /etc/systemd/system/voice-assistant.service </dev/null | grep -qxF " - $DOMAIN"; then + echo " Domain ist bereits registriert." +else + yunohost domain add "$DOMAIN" +fi + +echo "== 8/8: nginx-Reverse-Proxy ==" +mkdir -p "/etc/nginx/conf.d/$DOMAIN.d" +if [ ! -f /etc/nginx/conf.d/websocket-upgrade.conf ]; then + cat > /etc/nginx/conf.d/websocket-upgrade.conf <<'EOF' +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} +EOF +fi +cat > "/etc/nginx/conf.d/$DOMAIN.d/voice-assistant.conf" <