Skip to content

role: jitsi

Self-hosted Jitsi Meet — a public video plane, off the origin/edge planes (see runbooks/48-jitsi-meet.md and CLAUDE.md). INTERIM: runs as a pve VM holding its own public IP on its own NIC (wan in the vms.conf VLAN column — untagged on the WAN bridge, exactly like the edge VMs), standing in for a future off-prem vendor edge3. jitsi_public_ip is the box's own address, so it is ansible_host too.

It was first deployed behind an OPNsense WAN VIP with NAT to a DMZ address; that was undone, because inbound NAT applies only on WAN, so internal clients hit the firewall's DNS-rebind guard instead of Jitsi. Don't publish a service on a firewall VIP.

What it is

docker-jitsi-meet mapped onto podman quadlets: web, prosody, jicofo, jvb, on a private meet network. It runs on its own public IP with its own UDP/10000 media port, and terminates its own TLS. Its only link to the home lab is a WireGuard tunnel used solely to pull blessed packagesAllowedIPs is two /32s (the origin's tunnel address and the RPM mirror). No origin Caddy, no RFC2136 to the home Bind primary, no media over the tunnel.

Images: ghcr.io/jitsi/* (jitsi_image_registry), pinned stable-11248. Host ports 80/443/10000 are unchanged; the container listens on 8000/8443 because >=11146 runs as uid 1000 and cannot bind privileged ports — it still terminates TLS itself. That uid also needs three persistent volumes it hard-exits without: storage/web (/storage, acme.sh + certs), storage/prosody (/var/lib/prosody, the account store) and tmp/web-crontabs (/var/spool/cron/crontabs). See runbook 48 §Adopting.

Why it breaks two edge invariants (on purpose)

  • Terminates its own TLS (Let's Encrypt HTTP-01) — the passthrough edges hold no certs, but Jitsi is an app host, not a dumb relay.
  • Serves a public UDP media port — the SFU needs it; the TCP-SNI edges can't carry it.

Both are acceptable because E2EE is on by default (jitsi_e2ee_default): media frames are end-to-end encrypted (insertable streams), so even this box's JVB — and therefore the vendor — cannot read them. Terminating transport TLS only exposes signaling/metadata.

Auth

JWT (since 2026-08-15): any IPA user can start a meeting; this box only verifies. AUTH_TYPE=jwt — prosody's mod_auth_token checks HS256 tokens minted by the meet_issuer role on sso (meet-start.${domain}, Tier-2 behind Authelia forward-auth). The two halves share exactly one thing, vault_jitsi_jwt_secret; the signed, room-scoped, short-lived token crosses the boundary in the user's own browser redirect. TOKEN_AUTH_URL points the UI's "I am the host" at the issuer (the client substitutes {room}). ENABLE_GUESTS=1 still lets anonymous users into the lobby; a valid token joins authenticated and jicofo grants it moderator. See runbook 48 §Auth.

The no-route-to-the-identity-core rule still holds: the box asks nobody who anyone is — identity is proven lab-side where Authelia already fronts every Tier-2 app. The nordeck OIDC→JWT adapter stays retired (jitsi_oidc_adapter_enabled: false — that was an OIDC relying party on this box, Keycloak-only; the flag is kept because it gates the cleanup tasks that tear the adapter off any host still running it). Superseded 2026-08-15: the per-person Prosody-internal roster (jitsi_moderators + one KeePass password per human) — the role now reports leftover internal accounts with their deluser command instead of managing them; they are the rollback credential through the soak window.

Inputs

  • jitsi_public_ip — the VPS's real public IP (set in group_vars/all/zz-local.yml). Feeds JVB_ADVERTISE_IPS, the inventory ansible_host, and the meet A-record.
  • Vault secrets (per-play in playbooks/jitsi.yml): vault_jitsi_jwt_secret (shared with meet_issuer on sso — issuer signs, prosody verifies), vault_jitsi_jicofo_component_secret, vault_jitsi_jicofo_auth_password, vault_jitsi_jvb_auth_password. (vault_jitsi_oauth_client_secret, which fed the retired OIDC-adapter design, was pruned from the vault 2026-08-15 — these four are the complete set.)
  • Firewall (group_vars/jitsi): 80/tcp, 443/tcp, 10000/udp (+ optional 4443/tcp).
  • jitsi_jvb_max_memory / jitsi_jicofo_max_memory — JVM heap caps, derived from the guest's ansible_memtotal_mb (40% / 20%). Not optional tuning: jvb.sh and jicofo.sh both default to -Xmx3072m when unset, i.e. 6 GB of heap requested on a 4 GB no-swap guest. See runbook 48 "Provision".

Not IPA-enrolled

Off-prem-shaped public box (lab_ipa_enroll: false), like the edges — gets the common baseline (SSH-CA login, fail2ban, host firewall) + this role's auditd. Its own firewall is the boundary: nothing else filters this box, so 22/tcp is source-scoped via host_firewall_source_ports and lockout recovery is the Proxmox console.

It IS on central obs. Metrics and logs ship over the same WireGuard tunnel as the package pulls, with per-host attribution via Alloy's host label. That works because the obs /32 is in jitsi_peer_allowed_ips alongside the origin's tunnel address and the mirror — the edges are wired the same way (edge_peer_allowed_ips + origin_tunnel_masq_dests).

⚠️ Remove that obs /32 and meet goes silently unmonitored — it does NOT fail loudly. Alloy pushes, so with no route to obs prometheus.remote_write retries forever with context deadline exceeded while the unit stays active. A running-but-mute agent is indistinguishable from a healthy one, so "Systemd unit failed" cannot see it. That was the real state until 2026-08-01: meet had never shipped a sample, and the moment it did, obs immediately surfaced a cloud-final.service failure that had been invisible since first boot. fleet-agent-not-shipping (runbook 27) now catches a host that stops shipping — but a host that never shipped has no series to age, so when adding a tunnel-attached box, confirm a metric actually ARRIVES; is-active is not evidence.

Deploy

ansible-playbook -i inventory.ini playbooks/jitsi.yml
The Authelia side of auth lives in the sso converge (meet_issuer role + the meet-start.${domain} forward-auth domain + apps-proxy vhost), not here — converge sso and apps-proxy before flipping this box to jwt, or "I am the host" points at a name that is not there yet. Publish the public meet.<domain> A record (nsupdate — runbook 48 §DNS). Outward-facing — operator-gated.