jitsi: add vault-backed jwt launcher
This commit is contained in:
parent
77ecf3229e
commit
a55203a909
@ -6,6 +6,10 @@ resources:
|
|||||||
- serviceaccount.yaml
|
- serviceaccount.yaml
|
||||||
- secretproviderclass.yaml
|
- secretproviderclass.yaml
|
||||||
- deployment.yaml
|
- deployment.yaml
|
||||||
|
- launcher-configmap.yaml
|
||||||
|
- launcher-deployment.yaml
|
||||||
|
- launcher-service.yaml
|
||||||
|
- launcher-ingress.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
- pvc.yaml
|
- pvc.yaml
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
|
|||||||
118
services/jitsi/launcher-configmap.yaml
Normal file
118
services/jitsi/launcher-configmap.yaml
Normal file
@ -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"""
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>Start a Jitsi room</h2>
|
||||||
|
{'<p style="color:red;">'+message+'</p>' if message else ''}
|
||||||
|
<form action="/launch" method="get">
|
||||||
|
<label>Room name:</label>
|
||||||
|
<input name="room" required autofocus />
|
||||||
|
<button type="submit">Launch</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
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")
|
||||||
52
services/jitsi/launcher-deployment.yaml
Normal file
52
services/jitsi/launcher-deployment.yaml
Normal file
@ -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
|
||||||
24
services/jitsi/launcher-ingress.yaml
Normal file
24
services/jitsi/launcher-ingress.yaml
Normal file
@ -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 }
|
||||||
12
services/jitsi/launcher-service.yaml
Normal file
12
services/jitsi/launcher-service.yaml
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user