diff --git a/services/jitsi/kustomization.yaml b/services/jitsi/kustomization.yaml index cfa5622..805a967 100644 --- a/services/jitsi/kustomization.yaml +++ b/services/jitsi/kustomization.yaml @@ -6,6 +6,10 @@ resources: - serviceaccount.yaml - secretproviderclass.yaml - deployment.yaml + - launcher-configmap.yaml + - launcher-deployment.yaml + - launcher-service.yaml + - launcher-ingress.yaml - service.yaml - pvc.yaml - ingress.yaml diff --git a/services/jitsi/launcher-configmap.yaml b/services/jitsi/launcher-configmap.yaml new file mode 100644 index 0000000..c36f167 --- /dev/null +++ b/services/jitsi/launcher-configmap.yaml @@ -0,0 +1,118 @@ +# services/jitsi/launcher-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: jitsi-launcher + namespace: jitsi +data: + app.py: | + import base64 + import hashlib + import hmac + import json + import os + import time + from fastapi import FastAPI, HTTPException, Request + from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse + + ISSUER = os.getenv("JWT_ISSUER", "https://sso.bstein.dev/realms/atlas") + AUDIENCE = os.getenv("JWT_AUDIENCE", "jitsi") + APP_ID = os.getenv("JWT_APP_ID", "jitsi") + PUBLIC_URL = os.getenv("PUBLIC_URL", "https://meet.bstein.dev") + SECRET_FILE = os.getenv("JWT_SECRET_FILE", "/var/lib/jitsi-jwt/jwt") + ALLOWED_GROUPS = {g for g in os.getenv("ALLOWED_GROUPS", "").split(",") if g} + TOKEN_TTL = int(os.getenv("JWT_TTL_SECONDS", "600")) + + app = FastAPI() + + + def _b64url(data: bytes) -> bytes: + return base64.urlsafe_b64encode(data).rstrip(b"=") + + + def _read_secret() -> bytes: + raw = open(SECRET_FILE, "rb").read().strip() + try: + return bytes.fromhex(raw.decode()) + except ValueError: + return raw + + + def _sign(room: str, user: str, groups: list[str]) -> str: + now = int(time.time()) + header = {"alg": "HS256", "typ": "JWT"} + payload = { + "iss": ISSUER, + "aud": AUDIENCE, + "sub": "meet.jitsi", + "room": room, + "exp": now + TOKEN_TTL, + "nbf": now - 10, + "context": { + "user": { + "name": user, + "email": user, + "affiliation": "owner", + "groups": groups, + } + }, + "app_id": APP_ID, + } + secret = _read_secret() + signing_input = b".".join( + [ + _b64url(json.dumps(header, separators=(",", ":")).encode()), + _b64url(json.dumps(payload, separators=(",", ":")).encode()), + ] + ) + sig = _b64url(hmac.new(secret, signing_input, hashlib.sha256).digest()) + return b".".join([signing_input, sig]).decode() + + + def _render_form(message: str = "") -> HTMLResponse: + body = f""" + +
+'+message+'
' if message else ''} + + + + """ + return HTMLResponse(body) + + + def _extract_groups(request: Request) -> set[str]: + raw = request.headers.get("x-auth-request-groups", "") + # Traefik forwardAuth returns comma-separated groups + return {g.strip() for g in raw.split(",") if g.strip()} + + + @app.get("/launch") + async def launch(request: Request, room: str | None = None): + user = request.headers.get("x-auth-request-email") or request.headers.get( + "x-auth-request-user", "" + ) + groups = _extract_groups(request) + if ALLOWED_GROUPS and not (groups & ALLOWED_GROUPS): + raise HTTPException(status_code=403, detail="forbidden") + if not room: + return _render_form() + room = room.strip() + if not room or "/" in room or ".." in room: + raise HTTPException(status_code=400, detail="invalid room") + token = _sign(room, user or "moderator", sorted(groups)) + join_url = f"{PUBLIC_URL}/{room}#config.jwt={token}" + accept = request.headers.get("accept", "") + if "text/html" in accept: + return RedirectResponse(join_url, status_code=302) + return JSONResponse({"room": room, "join_url": join_url, "token": token}) + + + @app.get("/") + async def root(): + return RedirectResponse("/launch") diff --git a/services/jitsi/launcher-deployment.yaml b/services/jitsi/launcher-deployment.yaml new file mode 100644 index 0000000..3d207c7 --- /dev/null +++ b/services/jitsi/launcher-deployment.yaml @@ -0,0 +1,52 @@ +# services/jitsi/launcher-deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jitsi-launcher + namespace: jitsi +spec: + replicas: 1 + selector: + matchLabels: { app: jitsi-launcher } + template: + metadata: + labels: { app: jitsi-launcher } + spec: + serviceAccountName: jitsi + nodeSelector: + kubernetes.io/hostname: titan-22 + kubernetes.io/arch: amd64 + containers: + - name: launcher + image: ghcr.io/tiangolo/uvicorn-gunicorn-fastapi:python3.11-slim + imagePullPolicy: IfNotPresent + env: + - { name: JWT_SECRET_FILE, value: "/var/lib/jitsi-jwt/jwt" } + - { name: JWT_ISSUER, value: "https://sso.bstein.dev/realms/atlas" } + - { name: JWT_AUDIENCE, value: "jitsi" } + - { name: JWT_APP_ID, value: "jitsi" } + - { name: PUBLIC_URL, value: "https://meet.bstein.dev" } + - { name: ALLOWED_GROUPS, value: "admin,jitsi-moderator" } + - { name: JWT_TTL_SECONDS, value: "600" } + ports: + - { name: http, containerPort: 80 } + volumeMounts: + - { name: app, mountPath: /app/main.py, subPath: app.py } + - { name: jwt, mountPath: /var/lib/jitsi-jwt, readOnly: true } + readinessProbe: + httpGet: + path: /launch + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: app + configMap: + name: jitsi-launcher + defaultMode: 0444 + - name: jwt + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: jitsi-jwt diff --git a/services/jitsi/launcher-ingress.yaml b/services/jitsi/launcher-ingress.yaml new file mode 100644 index 0000000..c9f1f55 --- /dev/null +++ b/services/jitsi/launcher-ingress.yaml @@ -0,0 +1,24 @@ +# services/jitsi/launcher-ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: jitsi-launcher + namespace: jitsi + annotations: + cert-manager.io/cluster-issuer: letsencrypt + traefik.ingress.kubernetes.io/router.middlewares: sso-oauth2-proxy-forward-auth@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: [ "meet.bstein.dev" ] + secretName: jitsi-meet-tls + rules: + - host: meet.bstein.dev + http: + paths: + - path: /launch + pathType: Prefix + backend: + service: + name: jitsi-launcher + port: { number: 80 } diff --git a/services/jitsi/launcher-service.yaml b/services/jitsi/launcher-service.yaml new file mode 100644 index 0000000..3ed7f5a --- /dev/null +++ b/services/jitsi/launcher-service.yaml @@ -0,0 +1,12 @@ +# services/jitsi/launcher-service.yaml +apiVersion: v1 +kind: Service +metadata: + name: jitsi-launcher + namespace: jitsi +spec: + selector: { app: jitsi-launcher } + ports: + - name: http + port: 80 + targetPort: 80