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.

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

A named Prosody-internal moderator gates who can start a meeting. AUTH_TYPE=internal with one account (jitsi_moderator_user, password in KeePass — a human credential, not in the automation vault). ENABLE_GUESTS=1 lets anonymous users into the lobby, but only the moderator can create/moderate a room. The role creates the account only when absent and never rewrites an existing password, so a hand rotation is not silently undone by the next converge.

Not SSO, deliberately: this box has no route to the identity core — the same rule as tvheadend and ntfy. The nordeck OIDC→JWT adapter was retired (jitsi_oidc_adapter_enabled: false); it is Keycloak-only and could not front Authelia without the origin Caddy impersonating Keycloak. The flag is kept rather than deleted because it also gates the cleanup tasks that tear the adapter off any host still running it.

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, vault_jitsi_oauth_client_secret, vault_jitsi_jicofo_component_secret, vault_jitsi_jicofo_auth_password, vault_jitsi_jvb_auth_password.
  • 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
Register the OIDC client via the Authelia play (playbooks/site.yml -l sso). Publish the public meet.<domain> A record (nsupdate — runbook 48 §DNS). Outward-facing — operator-gated.