From 6efce0c8f10c959802f52124ae8d71b5b636a37e Mon Sep 17 00:00:00 2001 From: Dennis Paradzinski Date: Mon, 11 May 2026 12:55:29 +0200 Subject: [PATCH] chore: initial Jitsi-Meet k3s scaffold for darkemberserver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted Jitsi instance at meet.it.financeflow.de — avoids the meet.jit.si moderator-auth wall. Four components (web/prosody/jicofo/jvb) as raw k3s manifests, same deploy pattern as Embertime (Gitea Actions + kubectl apply + KUBECONFIG_B64 secret). JVB uses hostNetwork + UDP 10000 for media — requires router forward. Component passwords live in a kubectl-applied Secret (not in git); generate-secrets.sh produces a fresh manifest. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitea/workflows/deploy.yml | 66 +++++++++++++++++++++++++++++++ .gitignore | 8 ++++ CLAUDE.md | 53 +++++++++++++++++++++++++ infra/k3s/00-namespace.yaml | 4 ++ infra/k3s/10-config.yaml | 46 +++++++++++++++++++++ infra/k3s/20-secrets.yaml.example | 20 ++++++++++ infra/k3s/30-prosody.yaml | 47 ++++++++++++++++++++++ infra/k3s/40-jicofo.yaml | 26 ++++++++++++ infra/k3s/50-web.yaml | 40 +++++++++++++++++++ infra/k3s/60-jvb.yaml | 47 ++++++++++++++++++++++ infra/k3s/70-ingress.yaml | 30 ++++++++++++++ scripts/generate-secrets.sh | 34 ++++++++++++++++ 12 files changed, 421 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 infra/k3s/00-namespace.yaml create mode 100644 infra/k3s/10-config.yaml create mode 100644 infra/k3s/20-secrets.yaml.example create mode 100644 infra/k3s/30-prosody.yaml create mode 100644 infra/k3s/40-jicofo.yaml create mode 100644 infra/k3s/50-web.yaml create mode 100644 infra/k3s/60-jvb.yaml create mode 100644 infra/k3s/70-ingress.yaml create mode 100755 scripts/generate-secrets.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..464b493 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,66 @@ +name: deploy + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + with: + version: latest + + - name: Configure kubectl from secret + run: | + mkdir -p "$HOME/.kube" + echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > "$HOME/.kube/config" + chmod 600 "$HOME/.kube/config" + kubectl config current-context + + # __PUBLIC_IP__ placeholder in jvb manifest needs the actual public + # IP of darkember. Inject from a repo secret so the manifest stays + # generic in git. + - name: Patch JVB public IP + run: | + test -n "${{ secrets.DARKEMBER_PUBLIC_IP }}" || (echo "secret DARKEMBER_PUBLIC_IP missing" && exit 1) + sed -i "s|__PUBLIC_IP__|${{ secrets.DARKEMBER_PUBLIC_IP }}|g" infra/k3s/60-jvb.yaml + + - name: Apply manifests + # 20-secrets.yaml is intentionally NOT applied — secret must be + # created out-of-band (see scripts/generate-secrets.sh) so we + # don't overwrite real values with placeholders. + run: | + kubectl apply -f infra/k3s/00-namespace.yaml + kubectl apply -f infra/k3s/10-config.yaml + kubectl apply -f infra/k3s/30-prosody.yaml + kubectl apply -f infra/k3s/40-jicofo.yaml + kubectl apply -f infra/k3s/50-web.yaml + kubectl apply -f infra/k3s/60-jvb.yaml + kubectl apply -f infra/k3s/70-ingress.yaml + + - name: Wait for rollout + run: | + kubectl -n jitsi rollout status deployment/prosody --timeout=3m + kubectl -n jitsi rollout status deployment/jicofo --timeout=3m + kubectl -n jitsi rollout status deployment/jitsi-web --timeout=3m + kubectl -n jitsi rollout status deployment/jvb --timeout=3m + + - name: Smoke-check + run: | + for i in 1 2 3 4 5; do + if curl -fsS -o /dev/null -w "%{http_code}\n" https://meet.it.financeflow.de/ | grep -q "200\|301\|302"; then + echo "meet.it.financeflow.de is up" + exit 0 + fi + sleep 5 + done + echo "smoke-check failed" + exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b0debb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Real secrets — must live out-of-band, not in git +infra/k3s/20-secrets.yaml +infra/k3s/*-secrets.yaml +*.secret + +# Tools +.DS_Store +node_modules diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fbc8a4f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# jitsi-meet — Projekt-Kontext für Claude-Sessions + +Selbst-gehostete Jitsi-Meet-Instanz auf darkemberserver via k3s. Same-Cluster-Deploy wie Embertime/Gitea/Headscale. Repo unter `~/Developer/jitsi-meet/`, Public-URL `https://meet.it.financeflow.de`. + +Zweck: privater Online-Meeting-Service ohne Google-/GitHub-Login-Zwang (das öffentliche meet.jit.si verlangt seit 2024/25 Moderator-Auth). Wird unter anderem von Embertime als Default-Meeting-URL referenziert. + +## Architektur + +Vier Container nach offiziellem `jitsi/docker-jitsi-meet`-Pattern: + +- **web** (`jitsi/web`): Nginx + Meet-Frontend, exposed via Ingress :443 +- **prosody** (`jitsi/prosody`): XMPP-Server, cluster-intern +- **jicofo** (`jitsi/jicofo`): Focus/Conference-Manager, cluster-intern +- **jvb** (`jitsi/jvb`): Videobridge — `hostNetwork: true` damit UDP 10000 direkt auf Node-Interface bindet (alternativ Sub-LoadBalancer, aber unzuverlässig) + +Inter-Component-Auth über shared XMPP-Component-Secrets (im Kubernetes-Secret, nicht im Repo). + +## Wichtigste Dateien + +- `infra/k3s/00-namespace.yaml` — Namespace `jitsi` +- `infra/k3s/10-config.yaml` — ConfigMap mit nicht-sensiblen Env-Vars (XMPP-Domains, PUBLIC_URL, ...) +- `infra/k3s/20-secrets.yaml.example` — Vorlage; **echte Secrets manuell erzeugen** via `scripts/generate-secrets.sh`, dann `kubectl apply -f infra/k3s/20-secrets.yaml` einmalig out-of-band +- `infra/k3s/30-prosody.yaml`, `40-jicofo.yaml`, `50-web.yaml`, `60-jvb.yaml` — Component-Deployments + Services +- `infra/k3s/70-ingress.yaml` — Ingress mit cert-manager (letsencrypt-prod ClusterIssuer) +- `.gitea/workflows/deploy.yml` — `kubectl apply -f infra/k3s/...` auf Push nach main +- `scripts/generate-secrets.sh` — generiert random Component-Passwords + schreibt `20-secrets.yaml` + +## Setup-Schritte (einmalig) + +1. **DNS**: A-Record `meet.it.financeflow.de` → öffentliche IP von darkemberserver +2. **Firewall/Router**: UDP-Port **10000** auf darkember-Node forwarden (kritisch! ohne das funktioniert nur Audio, kein Video) +3. **Secrets generieren + applyen**: + ```bash + ./scripts/generate-secrets.sh > infra/k3s/20-secrets.yaml + # DOCKER_HOST_ADDRESS in 60-jvb.yaml auf die Public-IP von darkember setzen + kubectl apply -f infra/k3s/20-secrets.yaml + ``` +4. **Gitea-Repo + Secret KUBECONFIG_B64**: Repo bei Gitea anlegen, KUBECONFIG_B64 als Repo-Secret hinterlegen (gleicher Wert wie bei Embertime), push. + +Danach: Pipeline rollt aus, in ~3 Min steht `https://meet.it.financeflow.de`. + +## Was NICHT machen + +- Keine Authentifizierung aktivieren — der Witz ist, dass der Service ohne Login funktioniert. `ENABLE_AUTH=0` ist Pflicht. +- Keinen Jibri (Recording) ohne expliziten Auftrag — braucht extra Pod mit Chrome + viel mehr Ressourcen +- Kein Helm — bleibt beim raw-Manifest-Pattern wie Embertime +- Nicht `replicas > 1` für prosody/jicofo — beide sind stateful Single-Instance +- JVB skaliert prinzipiell horizontal, aber für ein Single-Cluster reicht 1 Replica + +## Verwandt im Obsidian-Vault + +- `feedback_git_workflow.md` — Klone-Pfad, Gitea-SSH-Setup +- Embertime-CLAUDE.md (`~/Developer/embertime/CLAUDE.md`) — gleicher Deploy-Pattern, gleiches KUBECONFIG-Secret diff --git a/infra/k3s/00-namespace.yaml b/infra/k3s/00-namespace.yaml new file mode 100644 index 0000000..bcb3964 --- /dev/null +++ b/infra/k3s/00-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: jitsi diff --git a/infra/k3s/10-config.yaml b/infra/k3s/10-config.yaml new file mode 100644 index 0000000..06259e8 --- /dev/null +++ b/infra/k3s/10-config.yaml @@ -0,0 +1,46 @@ +# Non-sensitive env shared across all jitsi components. Sensitive values +# (XMPP component passwords) live in the 20-secrets.yaml that is NOT +# committed to git — see scripts/generate-secrets.sh. +apiVersion: v1 +kind: ConfigMap +metadata: + name: jitsi-config + namespace: jitsi +data: + # === Public-facing config === + PUBLIC_URL: "https://meet.it.financeflow.de" + TZ: "Europe/Berlin" + + # === XMPP topology (internal hostnames — do not change without + # updating all 4 components in lockstep) === + XMPP_DOMAIN: "meet.jitsi" + XMPP_AUTH_DOMAIN: "auth.meet.jitsi" + XMPP_INTERNAL_MUC_DOMAIN: "internal-muc.meet.jitsi" + XMPP_MUC_DOMAIN: "muc.meet.jitsi" + XMPP_GUEST_DOMAIN: "guest.meet.jitsi" + XMPP_RECORDER_DOMAIN: "recorder.meet.jitsi" + XMPP_SERVER: "prosody.jitsi.svc.cluster.local" + XMPP_BOSH_URL_BASE: "http://prosody.jitsi.svc.cluster.local:5280" + + # === Auth disabled — anyone with the URL can start a room. That's the + # whole point of self-hosting (avoids the meet.jit.si Google/GitHub + # moderator gate). === + ENABLE_AUTH: "0" + ENABLE_GUESTS: "0" + + # === TLS handled by ingress cert-manager, not by the jitsi/web container === + ENABLE_LETSENCRYPT: "0" + ENABLE_HTTP_REDIRECT: "0" + DISABLE_HTTPS: "1" + + # === Videobridge brewery (where jicofo finds JVBs over XMPP) === + JVB_BREWERY_MUC: "jvbbrewery" + + # === STUN — default Jitsi-hosted STUN servers; ok for getting started === + JVB_STUN_SERVERS: "meet-jit-si-turnrelay.jitsi.net:443" + + # === Minor UX tweaks === + ENABLE_PREJOIN_PAGE: "1" + ENABLE_WELCOME_PAGE: "1" + ENABLE_TRANSCRIPTIONS: "0" + ENABLE_RECORDING: "0" diff --git a/infra/k3s/20-secrets.yaml.example b/infra/k3s/20-secrets.yaml.example new file mode 100644 index 0000000..c6963ec --- /dev/null +++ b/infra/k3s/20-secrets.yaml.example @@ -0,0 +1,20 @@ +# TEMPLATE — do NOT commit a filled-in copy. Generate the real secret +# via scripts/generate-secrets.sh and apply once out-of-band: +# +# ./scripts/generate-secrets.sh > infra/k3s/20-secrets.yaml +# kubectl apply -f infra/k3s/20-secrets.yaml +# +# Component passwords are only used cluster-internally between +# prosody/jicofo/jvb — clients (browsers) never see them. +apiVersion: v1 +kind: Secret +metadata: + name: jitsi-secrets + namespace: jitsi +type: Opaque +stringData: + JICOFO_COMPONENT_SECRET: "REPLACE_WITH_32_RANDOM_CHARS" + JICOFO_AUTH_USER: "focus" + JICOFO_AUTH_PASSWORD: "REPLACE_WITH_32_RANDOM_CHARS" + JVB_AUTH_USER: "jvb" + JVB_AUTH_PASSWORD: "REPLACE_WITH_32_RANDOM_CHARS" diff --git a/infra/k3s/30-prosody.yaml b/infra/k3s/30-prosody.yaml new file mode 100644 index 0000000..4a36a92 --- /dev/null +++ b/infra/k3s/30-prosody.yaml @@ -0,0 +1,47 @@ +# XMPP server — cluster-internal only. Holds the conference state and +# brokers messages between web (client) ↔ jicofo ↔ jvb. Single-replica +# stateful service; do NOT scale. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prosody + namespace: jitsi +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: { app: prosody } + template: + metadata: + labels: { app: prosody } + spec: + containers: + - name: prosody + image: jitsi/prosody:stable + envFrom: + - configMapRef: { name: jitsi-config } + - secretRef: { name: jitsi-secrets } + ports: + - { name: c2s, containerPort: 5222 } + - { name: comp, containerPort: 5347 } + - { name: bosh, containerPort: 5280 } + readinessProbe: + tcpSocket: { port: 5222 } + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } +--- +apiVersion: v1 +kind: Service +metadata: + name: prosody + namespace: jitsi +spec: + selector: { app: prosody } + ports: + - { name: c2s, port: 5222, targetPort: 5222 } + - { name: comp, port: 5347, targetPort: 5347 } + - { name: bosh, port: 5280, targetPort: 5280 } diff --git a/infra/k3s/40-jicofo.yaml b/infra/k3s/40-jicofo.yaml new file mode 100644 index 0000000..8200087 --- /dev/null +++ b/infra/k3s/40-jicofo.yaml @@ -0,0 +1,26 @@ +# Focus/conference manager. Talks to prosody as a component, signs +# users into rooms, picks JVBs from the brewery for video routing. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jicofo + namespace: jitsi +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: { app: jicofo } + template: + metadata: + labels: { app: jicofo } + spec: + containers: + - name: jicofo + image: jitsi/jicofo:stable + envFrom: + - configMapRef: { name: jitsi-config } + - secretRef: { name: jitsi-secrets } + resources: + requests: { cpu: 50m, memory: 256Mi } + limits: { cpu: 500m, memory: 1Gi } diff --git a/infra/k3s/50-web.yaml b/infra/k3s/50-web.yaml new file mode 100644 index 0000000..199f842 --- /dev/null +++ b/infra/k3s/50-web.yaml @@ -0,0 +1,40 @@ +# Static Meet UI + nginx that proxies XMPP-over-BOSH back to prosody. +# Exposed via Ingress in 70-ingress.yaml. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jitsi-web + namespace: jitsi +spec: + replicas: 1 + selector: + matchLabels: { app: jitsi-web } + template: + metadata: + labels: { app: jitsi-web } + spec: + containers: + - name: web + image: jitsi/web:stable + envFrom: + - configMapRef: { name: jitsi-config } + - secretRef: { name: jitsi-secrets } + ports: + - { name: http, containerPort: 80 } + readinessProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + periodSeconds: 5 + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } +--- +apiVersion: v1 +kind: Service +metadata: + name: jitsi-web + namespace: jitsi +spec: + selector: { app: jitsi-web } + ports: + - { name: http, port: 80, targetPort: 80 } diff --git a/infra/k3s/60-jvb.yaml b/infra/k3s/60-jvb.yaml new file mode 100644 index 0000000..9d6d1f8 --- /dev/null +++ b/infra/k3s/60-jvb.yaml @@ -0,0 +1,47 @@ +# Videobridge — routes the actual media streams. Two critical bits: +# +# 1) hostNetwork: true — JVB binds UDP 10000 directly on the node's +# external interface. The router/firewall must forward UDP 10000 +# from the public IP to that node, otherwise browsers can't reach +# it and you get audio-only fallback. +# +# 2) DOCKER_HOST_ADDRESS — the public IP/hostname browsers will use +# to reach the bridge. Replace `__PUBLIC_IP__` with darkember's +# public IP before applying, or override via kustomize/sed in the +# deploy step. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jvb + namespace: jitsi +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: { app: jvb } + template: + metadata: + labels: { app: jvb } + spec: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + containers: + - name: jvb + image: jitsi/jvb:stable + envFrom: + - configMapRef: { name: jitsi-config } + - secretRef: { name: jitsi-secrets } + env: + - name: DOCKER_HOST_ADDRESS + value: "__PUBLIC_IP__" + - name: JVB_PORT + value: "10000" + ports: + - name: media + containerPort: 10000 + hostPort: 10000 + protocol: UDP + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: 2, memory: 2Gi } diff --git a/infra/k3s/70-ingress.yaml b/infra/k3s/70-ingress.yaml new file mode 100644 index 0000000..99b211d --- /dev/null +++ b/infra/k3s/70-ingress.yaml @@ -0,0 +1,30 @@ +# TLS termination via cert-manager — same `letsencrypt-prod` ClusterIssuer +# as embertime/gitea. Traffic forwarded to jitsi-web on :80. JVB media +# (UDP 10000) goes around the ingress entirely (hostNetwork on the node). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: jitsi + namespace: jitsi + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/proxy-body-size: "0" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - meet.it.financeflow.de + secretName: jitsi-tls + rules: + - host: meet.it.financeflow.de + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: jitsi-web + port: + number: 80 diff --git a/scripts/generate-secrets.sh b/scripts/generate-secrets.sh new file mode 100755 index 0000000..baf53a7 --- /dev/null +++ b/scripts/generate-secrets.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Generates a 20-secrets.yaml with fresh random component passwords. +# Usage: +# ./scripts/generate-secrets.sh > infra/k3s/20-secrets.yaml +# kubectl apply -f infra/k3s/20-secrets.yaml +# +# Re-running rotates the passwords — every component then needs to be +# restarted (kubectl rollout restart) so they pick up the new env. + +set -eu + +# 24 random bytes → 32 base64 chars, stripped of slashes/+ for safety in +# env vars + URLs. Avoids the SIGPIPE issue with `tr | head` under +# pipefail. +rand() { openssl rand -hex 16; } # 32 hex chars = 16 bytes entropy, plenty for component auth + +JICOFO_COMPONENT_SECRET=$(rand) +JICOFO_AUTH_PASSWORD=$(rand) +JVB_AUTH_PASSWORD=$(rand) + +cat <