feat(deploy): Installations-Skript + Anleitung für natives YunoHost-SSO (jamulix.net)

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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-07-01 10:26:12 +02:00
commit 6ab24977f1
2 changed files with 336 additions and 0 deletions

View file

@ -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=<dein Name>`.
Existiert die Datei schon, wird sie **nicht** überschrieben.
6. systemd-Unit `voice-assistant.service` (`Restart=always`), aktiviert +
gestartet.
7. `yunohost domain add <subdomain>` (übersprungen, falls schon registriert).
8. nginx-Snippet unter `/etc/nginx/conf.d/<subdomain>.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://<domain>/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/<domain>.d/voice-assistant.conf` prüfen, `nginx -t && systemctl reload nginx` |
| Zertifikat schlägt fehl | DNS zeigt noch nicht auf den Server | `dig +short <domain>` 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.

195
deploy/install_va_on_yunohost.sh Executable file
View file

@ -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 <subdomain> <yunohost-user[,user2,...]>
#
# 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 <subdomain>
# - OPENROUTER_API_KEY nachtragen, falls beim Lauf leer gelassen.
# - Einmal ueber https://<domain>/yunohost/sso/ einloggen, dann die App oeffnen.
set -euo pipefail
DOMAIN="${1:?Nutzung: $0 <subdomain, z.B. voice.jamulix.net> <yunohost-user[,user2,...]>}"
ADMIN_USERS="${2:?Nutzung: $0 <subdomain> <yunohost-user[,user2,...]>}"
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" <<EOF
# Erzeugt von install_va_on_yunohost.sh am $(date -Is)
HOST=127.0.0.1
PORT=$PORT
OPENROUTER_API_KEY=$OPENROUTER_API_KEY
VA_PROFILE=cloud
AUTH_ENABLED=true
ADMIN_API_KEY=$ADMIN_API_KEY
# Identitaet: natives YunoHost-Portal-SSO (Cookie "yunohost.portal"), KEIN Authelia.
# nginx und Gateway laufen auf derselben Maschine -> 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 <<EOF
[Unit]
Description=Voice Assistant Gateway
After=network.target
[Service]
Type=simple
User=$APP_USER
WorkingDirectory=$APP_DIR
EnvironmentFile=$ENV_FILE
ExecStart=$APP_DIR/.venv/bin/uvicorn app.main:app --host \${HOST} --port \${PORT}
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now voice-assistant.service
echo "== 7/8: YunoHost-Domain '$DOMAIN' =="
if yunohost domain list 2>/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" <<EOF
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
# WebSocket-Upgrade (zwingend fuer /ws/voice, /ws/chat - Mikrofon-Button):
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Identitaet: KEIN eigener Header noetig, das Cookie "yunohost.portal" wird
# ohnehin durchgereicht; das Gateway liest/prueft es selbst (siehe .env).
}
EOF
nginx -t && systemctl reload nginx
echo
echo "=================================================================="
echo "Fertig. Manuell noch offen:"
echo " 1) DNS pruefen: dig +short $DOMAIN (muss auf diese Server-IP zeigen)"
echo " 2) Zertifikat: yunohost domain cert-install $DOMAIN"
echo " 3) Login testen: https://$DOMAIN/yunohost/sso/ (einloggen als $ADMIN_USERS),"
echo " danach https://$DOMAIN/ oeffnen - sollte dich erkennen."
echo " 4) Falls OPENROUTER_API_KEY leer gelassen wurde: in $ENV_FILE nachtragen,"
echo " dann: systemctl restart voice-assistant"
echo " Logs: journalctl -u voice-assistant -f"
echo "=================================================================="