From dbac5ede16a4a88465b08765dccfa85c73cd5c08 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Thu, 8 Oct 2026 10:23:17 +0200 Subject: [PATCH 1/4] Add a role for Jitsi in Docker Compose We need this role to follow the recommended deployment approach from https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker/ This role enables us to keep our config and customizations separate from releases of the Compose setup the Jitsi project publishs. --- playbooks/deploy.yaml | 7 + roles/jitsi/README.md | 57 ++++++ roles/jitsi/defaults/main.yaml | 8 + roles/jitsi/handlers/main.yaml | 19 ++ roles/jitsi/meta/argument_specs.yaml | 57 ++++++ roles/jitsi/meta/main.yaml | 4 + roles/jitsi/tasks/main.yaml | 186 ++++++++++++++++++ .../jitsi/templates/compose.override.yaml.j2 | 40 ++++ roles/jitsi/templates/jitsi.env.j2 | 46 +++++ roles/jitsi/templates/turnserver.conf.j2 | 34 ++++ roles/jitsi/vars/main.yaml | 4 + 11 files changed, 462 insertions(+) create mode 100644 roles/jitsi/README.md create mode 100644 roles/jitsi/defaults/main.yaml create mode 100644 roles/jitsi/handlers/main.yaml create mode 100644 roles/jitsi/meta/argument_specs.yaml create mode 100644 roles/jitsi/meta/main.yaml create mode 100644 roles/jitsi/tasks/main.yaml create mode 100644 roles/jitsi/templates/compose.override.yaml.j2 create mode 100644 roles/jitsi/templates/jitsi.env.j2 create mode 100644 roles/jitsi/templates/turnserver.conf.j2 create mode 100644 roles/jitsi/vars/main.yaml diff --git a/playbooks/deploy.yaml b/playbooks/deploy.yaml index 2a1498ac..fd86de91 100644 --- a/playbooks/deploy.yaml +++ b/playbooks/deploy.yaml @@ -110,6 +110,13 @@ tags: - docker_compose +- name: Ensure Jitsi deployment on jitsi_hosts + hosts: jitsi_hosts + roles: + - jitsi + tags: + - jitsi + - name: Ensure NGINX deployment on nginx_hosts hosts: nginx_hosts:!public_reverse_proxy_hosts roles: diff --git a/roles/jitsi/README.md b/roles/jitsi/README.md new file mode 100644 index 00000000..bd3db9bb --- /dev/null +++ b/roles/jitsi/README.md @@ -0,0 +1,57 @@ +# Role `jitsi` + +A role for deploying [Jitsi Meet](https://jitsi.org/) using the official [docker-jitsi-meet](https://github.com/jitsi/docker-jitsi-meet) release. + +The given release is downloaded and unpacked unmodified to `/opt/jitsi/releases/` and `/opt/jitsi/release` is pointed to it. +The `.env` file at `/opt/jitsi/.env` is fully managed by this role; `env.example` and `gen-passwords.sh` of the release are not used. +The `.env` also sets `COMPOSE_PROJECT_NAME` and `COMPOSE_FILE`, so `docker compose` can be run by hand in `/opt/jitsi`. +The Jitsi configuration directory (`CONFIG`) is `/opt/jitsi/config`, with persistent state in `/opt/jitsi/config/storage/`. +The image version (`JITSI_IMAGE_VERSION`) always equals the release version, so upgrading means bumping `jitsi__version`. +Releases other than the configured one are removed. + +TLS certificates are obtained by the web container itself using Let's Encrypt, so ports 80/tcp and 443/tcp as well as 10000/udp (media) must be reachable. + +Optionally, a [coturn](https://github.com/coturn/coturn) TURN server and the [Prometheus Jitsi Meet exporter](https://github.com/systemli/prometheus-jitsi-meet-exporter) are deployed as additional services of the same Compose project, defined in `/opt/jitsi/compose.override.yaml`. +coturn uses the certificate obtained by the web container and is restarted weekly to pick up renewals. +It needs 3478/udp+tcp, 5349/tcp and the relay port range 49152-65535/udp to be reachable. +The exporter scrapes the JVB statistics and listens on port 9888. + +## Supported Distributions + +Should work on Debian-based distributions. + +## Required Arguments + +- `jitsi__version`: The docker-jitsi-meet release to deploy, e.g. `stable-11248`. +- `jitsi__domain`: The domain Jitsi is served on. Used for `PUBLIC_URL` and the Let's Encrypt certificate. +- `jitsi__letsencrypt_email`: The e-mail address for the Let's Encrypt account. +- `jitsi__jicofo_auth_password`: XMPP password for Jicofo. Generate using `openssl rand -hex 16`. +- `jitsi__jvb_auth_password`: XMPP password for the JVB. Generate using `openssl rand -hex 16`. + +## Optional Arguments + +- `jitsi__timezone`: The time zone for the containers. + Defaults to `Europe/Berlin`. +- `jitsi__jvb_advertise_ips`: List of IP addresses the JVB should advertise for media (`JVB_ADVERTISE_IPS`). + Defaults to `[ ]`. +- `jitsi__whiteboard`: Whether to deploy the Excalidraw whiteboard backend (`whiteboard.yml`). + Defaults to `false`. +- `jitsi__turn`: Whether to deploy coturn and configure Jitsi to use it. + Defaults to `false`. +- `jitsi__turn_secret`: The shared secret between Prosody and coturn. Required if `jitsi__turn` is `true`. + Generate using `openssl rand -hex 32`. +- `jitsi__prometheus_exporter`: Whether to deploy the Prometheus Jitsi Meet exporter on port 9888. + Defaults to `false`. +- `jitsi__env`: Dictionary of additional variables to put into the `.env` file. + See `env.example` of the release and the handbook for available variables. + Defaults to `{ }`. +- `jitsi__custom_config_js`: Content of `custom-config.js`, which gets appended to the generated `config.js`. + Defaults to `""` (no file). +- `jitsi__custom_interface_config_js`: Content of `custom-interface_config.js`, which gets appended to the generated `interface_config.js`. + Defaults to `""` (no file). + +## Links & Resources + +- +- +- diff --git a/roles/jitsi/defaults/main.yaml b/roles/jitsi/defaults/main.yaml new file mode 100644 index 00000000..9782b636 --- /dev/null +++ b/roles/jitsi/defaults/main.yaml @@ -0,0 +1,8 @@ +jitsi__timezone: Europe/Berlin +jitsi__jvb_advertise_ips: [ ] +jitsi__whiteboard: false +jitsi__turn: false +jitsi__prometheus_exporter: false +jitsi__env: { } +jitsi__custom_config_js: "" +jitsi__custom_interface_config_js: "" diff --git a/roles/jitsi/handlers/main.yaml b/roles/jitsi/handlers/main.yaml new file mode 100644 index 00000000..7bde7f02 --- /dev/null +++ b/roles/jitsi/handlers/main.yaml @@ -0,0 +1,19 @@ +- name: restart jitsi web + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + services: + - web + state: restarted + become: true + +- name: restart jitsi coturn + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + services: + - coturn + state: restarted + become: true diff --git a/roles/jitsi/meta/argument_specs.yaml b/roles/jitsi/meta/argument_specs.yaml new file mode 100644 index 00000000..2c65ca54 --- /dev/null +++ b/roles/jitsi/meta/argument_specs.yaml @@ -0,0 +1,57 @@ +argument_specs: + main: + options: + jitsi__version: + type: str + required: true + jitsi__domain: + type: str + required: true + jitsi__letsencrypt_email: + type: str + required: true + jitsi__jicofo_auth_password: + type: str + required: true + no_log: true + jitsi__jvb_auth_password: + type: str + required: true + no_log: true + jitsi__timezone: + type: str + required: false + default: Europe/Berlin + jitsi__jvb_advertise_ips: + type: list + elements: str + required: false + default: [ ] + jitsi__whiteboard: + type: bool + required: false + default: false + jitsi__turn: + type: bool + required: false + default: false + jitsi__turn_secret: + type: str + required: false + no_log: true + jitsi__prometheus_exporter: + type: bool + required: false + default: false + jitsi__env: + type: dict + required: false + default: { } + jitsi__custom_config_js: + type: str + required: false + default: "" + jitsi__custom_interface_config_js: + type: str + required: false + default: "" diff --git a/roles/jitsi/meta/main.yaml b/roles/jitsi/meta/main.yaml new file mode 100644 index 00000000..1fbb2364 --- /dev/null +++ b/roles/jitsi/meta/main.yaml @@ -0,0 +1,4 @@ +--- +allow_duplicates: false +dependencies: + - role: docker diff --git a/roles/jitsi/tasks/main.yaml b/roles/jitsi/tasks/main.yaml new file mode 100644 index 00000000..a7beaaa4 --- /dev/null +++ b/roles/jitsi/tasks/main.yaml @@ -0,0 +1,186 @@ +- name: make sure a TURN secret is given, if TURN is enabled + ansible.builtin.assert: + that: + - jitsi__turn_secret is defined + - jitsi__turn_secret | length > 0 + quiet: true + when: jitsi__turn + +- name: make sure the Jitsi base directories exist + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: "0755" + owner: root + group: root + become: true + loop: + - "{{ jitsi__base_dir }}" + - "{{ jitsi__base_dir }}/releases" + +- name: make sure the docker-jitsi-meet release is present + ansible.builtin.unarchive: + src: "https://github.com/jitsi/docker-jitsi-meet/archive/refs/tags/{{ jitsi__version }}.tar.gz" + remote_src: true + dest: "{{ jitsi__base_dir }}/releases" + creates: "{{ jitsi__release_dir }}/docker-compose.yml" + owner: root + group: root + become: true + +- name: point the `release` symlink to the configured release + ansible.builtin.file: + src: "{{ jitsi__release_dir }}" + dest: "{{ jitsi__base_dir }}/release" + state: link + owner: root + group: root + become: true + +# Configuration read by the containers at startup; managed by Ansible. +- name: make sure the configuration input directories exist + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/{{ item }}" + state: directory + mode: "0755" + owner: root + group: root + become: true + loop: + - "" + - web + - prosody + - prosody/config + - prosody/prosody-plugins-custom + - jicofo + - jvb + - storage + - tmp + +# State written by the containers, which run as uid/gid 1000. +# The certificate directory is created here, as Docker would otherwise create it owned by +# root for the coturn bind mount, before the web container gets to write the certificate. +- name: make sure the storage and tmp directories exist and are writable by the containers + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/{{ item }}" + state: directory + mode: "0750" + owner: "1000" + group: "1000" + become: true + loop: + - storage/prosody + - storage/web + - storage/web/acme-certs + - "storage/web/acme-certs/{{ jitsi__domain }}" + - storage/transcripts + - tmp/web-crontabs + - tmp/web-load-test + +- name: deploy custom web configuration + ansible.builtin.copy: + content: "{{ item.content }}" + dest: "{{ jitsi__config_dir }}/web/{{ item.name }}" + mode: "0644" + owner: root + group: root + become: true + when: item.content != "" + loop: + - name: custom-config.js + content: "{{ jitsi__custom_config_js }}" + - name: custom-interface_config.js + content: "{{ jitsi__custom_interface_config_js }}" + loop_control: + label: "{{ item.name }}" + notify: restart jitsi web + +- name: remove unconfigured custom web configuration + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/web/{{ item.name }}" + state: absent + become: true + when: item.content == "" + loop: + - name: custom-config.js + content: "{{ jitsi__custom_config_js }}" + - name: custom-interface_config.js + content: "{{ jitsi__custom_interface_config_js }}" + loop_control: + label: "{{ item.name }}" + notify: restart jitsi web + +- name: make sure the coturn directory exists + ansible.builtin.file: + path: "{{ jitsi__base_dir }}/coturn" + state: "{{ 'directory' if jitsi__turn else 'absent' }}" + mode: "0755" + owner: root + group: root + become: true + +# Readable by uid 1000, which coturn runs as. +- name: deploy the coturn configuration + ansible.builtin.template: + src: turnserver.conf.j2 + dest: "{{ jitsi__base_dir }}/coturn/turnserver.conf" + mode: "0640" + owner: root + group: "1000" + become: true + when: jitsi__turn + notify: restart jitsi coturn + +# coturn doesn't notice the certificate renewed by the web container on its own. +- name: ensure the weekly coturn restart cron job is present + ansible.builtin.cron: + name: "jitsi coturn restart for certificate renewal" + special_time: weekly + job: "cd {{ jitsi__base_dir }} && docker compose restart coturn >/dev/null" + state: "{{ 'present' if jitsi__turn else 'absent' }}" + become: true + +# Changes to these make `docker compose up` below recreate the affected containers. +- name: deploy the compose override file + ansible.builtin.template: + src: compose.override.yaml.j2 + dest: "{{ jitsi__base_dir }}/compose.override.yaml" + mode: "0644" + owner: root + group: root + become: true + +- name: deploy the .env file + ansible.builtin.template: + src: jitsi.env.j2 + dest: "{{ jitsi__base_dir }}/.env" + mode: "0600" + owner: root + group: root + become: true + +- name: docker compose up + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + state: present + pull: missing + remove_orphans: true + become: true + +- name: find old releases + ansible.builtin.find: + paths: "{{ jitsi__base_dir }}/releases" + file_type: directory + excludes: "docker-jitsi-meet-{{ jitsi__version }}" + register: jitsi__old_releases + +- name: remove old releases + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + become: true + loop: "{{ jitsi__old_releases.files }}" + loop_control: + label: "{{ item.path }}" diff --git a/roles/jitsi/templates/compose.override.yaml.j2 b/roles/jitsi/templates/compose.override.yaml.j2 new file mode 100644 index 00000000..05842569 --- /dev/null +++ b/roles/jitsi/templates/compose.override.yaml.j2 @@ -0,0 +1,40 @@ +# {{ ansible_managed }} +# Services in addition to the ones of the docker-jitsi-meet release. + +# Docker Compose explicitly disables IPv6 for networks not enabling it, overriding the +# daemon's default-network-opts. +networks: + meet.jitsi: + enable_ipv6: true + +services: +{% if jitsi__turn %} + coturn: + image: docker.io/coturn/coturn:4.18.0 + restart: unless-stopped + # Recommended by coturn, as Docker performs badly with large port ranges. + network_mode: host + # Needs to be able to read the certificate obtained by the web container. + user: "1000:1000" + command: ["-c", "/etc/coturn/turnserver.conf"] + volumes: + - {{ jitsi__base_dir }}/coturn/turnserver.conf:/etc/coturn/turnserver.conf:ro + - {{ jitsi__config_dir }}/storage/web/acme-certs/{{ jitsi__domain }}:/certs:ro + tmpfs: + - /var/lib/coturn +{% endif %} +{% if jitsi__prometheus_exporter %} + prometheus-jitsi-meet-exporter: + image: docker.io/systemli/prometheus-jitsi-meet-exporter:1.4.2 + restart: unless-stopped + command: ["-videobridge-url", "http://jvb:8080/colibri/stats"] + ports: + - "9888:9888" + depends_on: + - jvb + networks: + meet.jitsi: +{% endif %} +{% if not (jitsi__turn or jitsi__prometheus_exporter) %} + {} +{% endif %} diff --git a/roles/jitsi/templates/jitsi.env.j2 b/roles/jitsi/templates/jitsi.env.j2 new file mode 100644 index 00000000..f09b0414 --- /dev/null +++ b/roles/jitsi/templates/jitsi.env.j2 @@ -0,0 +1,46 @@ +# {{ ansible_managed }} +# Only the settings differing from the defaults are set here. +# Reference: {{ jitsi__base_dir }}/release/env.example and +# https://jitsi.github.io/handbook/docs/devops-guide/devops-guide-docker + +# Allow running `docker compose` by hand in {{ jitsi__base_dir }}. +COMPOSE_PROJECT_NAME=jitsi +COMPOSE_FILE={{ jitsi__compose_files | join(':') }} + +CONFIG={{ jitsi__config_dir }} +JITSI_IMAGE_VERSION={{ jitsi__version }} +TZ={{ jitsi__timezone }} +PUBLIC_URL=https://{{ jitsi__domain }} + +HTTP_PORT=80 +HTTPS_PORT=443 +ENABLE_HTTP_REDIRECT=1 +ENABLE_LETSENCRYPT=1 +LETSENCRYPT_DOMAIN={{ jitsi__domain }} +LETSENCRYPT_EMAIL={{ jitsi__letsencrypt_email }} +LETSENCRYPT_ACME_SERVER=letsencrypt +{% if jitsi__jvb_advertise_ips | length > 0 %} +JVB_ADVERTISE_IPS={{ jitsi__jvb_advertise_ips | join(',') }} +{% endif %} +{% if jitsi__whiteboard %} + +WHITEBOARD_COLLAB_SERVER_URL_BASE=http://whiteboard.meet.jitsi +{% endif %} +{% if jitsi__turn %} + +TURN_CREDENTIALS={{ jitsi__turn_secret }} +TURN_HOST={{ jitsi__domain }} +TURN_PORT=3478 +TURN_TRANSPORT=udp,tcp +TURNS_HOST={{ jitsi__domain }} +TURNS_PORT=5349 +{% endif %} +{% if jitsi__env | length > 0 %} + +{% for key, value in jitsi__env.items() %} +{{ key }}={{ value }} +{% endfor %} +{% endif %} + +JICOFO_AUTH_PASSWORD={{ jitsi__jicofo_auth_password }} +JVB_AUTH_PASSWORD={{ jitsi__jvb_auth_password }} diff --git a/roles/jitsi/templates/turnserver.conf.j2 b/roles/jitsi/templates/turnserver.conf.j2 new file mode 100644 index 00000000..28770f81 --- /dev/null +++ b/roles/jitsi/templates/turnserver.conf.j2 @@ -0,0 +1,34 @@ +# {{ ansible_managed }} +listening-port=3478 +tls-listening-port=5349 +realm={{ jitsi__domain }} +use-auth-secret +static-auth-secret={{ jitsi__turn_secret }} +cert=/certs/fullchain.pem +pkey=/certs/key.pem +no-tlsv1 +no-tlsv1_1 +no-cli +no-multicast-peers +no-loopback-peers +log-file=stdout +# Don't relay to private and otherwise special addresses. +denied-peer-ip=0.0.0.0-0.255.255.255 +denied-peer-ip=10.0.0.0-10.255.255.255 +denied-peer-ip=100.64.0.0-100.127.255.255 +denied-peer-ip=127.0.0.0-127.255.255.255 +denied-peer-ip=169.254.0.0-169.254.255.255 +denied-peer-ip=172.16.0.0-172.31.255.255 +denied-peer-ip=192.0.0.0-192.0.0.255 +denied-peer-ip=192.0.2.0-192.0.2.255 +denied-peer-ip=192.88.99.0-192.88.99.255 +denied-peer-ip=192.168.0.0-192.168.255.255 +denied-peer-ip=198.18.0.0-198.19.255.255 +denied-peer-ip=198.51.100.0-198.51.100.255 +denied-peer-ip=203.0.113.0-203.0.113.255 +denied-peer-ip=240.0.0.0-255.255.255.255 +denied-peer-ip=::1 +denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff +denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 +denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff +denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff diff --git a/roles/jitsi/vars/main.yaml b/roles/jitsi/vars/main.yaml new file mode 100644 index 00000000..21fe76b6 --- /dev/null +++ b/roles/jitsi/vars/main.yaml @@ -0,0 +1,4 @@ +jitsi__base_dir: /opt/jitsi +jitsi__config_dir: "{{ jitsi__base_dir }}/config" +jitsi__release_dir: "{{ jitsi__base_dir }}/releases/docker-jitsi-meet-{{ jitsi__version }}" +jitsi__compose_files: "{{ ['release/docker-compose.yml'] + (['release/whiteboard.yml'] if jitsi__whiteboard else []) + ['compose.override.yaml'] }}" -- 2.54.0 From 86e793c62420c02538fe9bcb428159b17a521f70 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Thu, 8 Oct 2026 10:24:38 +0200 Subject: [PATCH 2/4] Use jitsi role to configure jitsi The host has been manually cleaned up (removed old packages and config) and this config has been deployed already. --- .../chaosknoten/host_vars/jitsi.sops.yaml | 7 +- inventories/chaosknoten/host_vars/jitsi.yaml | 13 ++++ inventories/chaosknoten/hosts.yaml | 4 ++ .../chaosknoten/jitsi/nftables/nftables.conf | 65 +++++++++++++++++++ roles/nftables/tasks/main.yaml | 7 ++ 5 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 inventories/chaosknoten/host_vars/jitsi.yaml create mode 100644 resources/chaosknoten/jitsi/nftables/nftables.conf diff --git a/inventories/chaosknoten/host_vars/jitsi.sops.yaml b/inventories/chaosknoten/host_vars/jitsi.sops.yaml index ce335073..adae05aa 100644 --- a/inventories/chaosknoten/host_vars/jitsi.sops.yaml +++ b/inventories/chaosknoten/host_vars/jitsi.sops.yaml @@ -1,4 +1,7 @@ ansible_pull__age_private_key: ENC[AES256_GCM,data:8PCkZx76R5aWPITn5MCZCMckXvaLfVvnDgO70XmF/eeH5xLVOVx+EC3DZjmHxEoW7Kn4LAJTR4mWl/xP+p++elRvTMcHltV+vBQ=,iv:jKq07NLM84MSGU4CCyUS9ODmwiUMhlDONkKJ7P9P2DM=,tag:95yBZlwYdSNeX2O4t/h9kw==,type:str] +jitsi__jicofo_auth_password: ENC[AES256_GCM,data:a7y36RJUR4OBtVNzgmqlGEsg1hNRGgsuwG2YFJrDlMI=,iv:mnGlKOXtNltNe3wY34BnVCJ9CtLQ7aksr4xXbrTOnJg=,tag:j1N7MUTJG8K0JW/a6YESFg==,type:str] +jitsi__jvb_auth_password: ENC[AES256_GCM,data:xUFiIXAzY4pEpwgL/5LPBlhkx73RX6VFT3QJlG5QbFo=,iv:/XqENztrrpj1h3lBhEtJJo78lCrxrMWxRszNjSwx+KQ=,tag:+LRYmv9Tvt0muR47QTd6HA==,type:str] +jitsi__turn_secret: ENC[AES256_GCM,data:UPev9ND9Mf2gGK28huQuuxRyUZvPPCmFL9rU1f0zndSPPBMspSsnlgtMy0+VY/CsGEIEcWsy3eYDPcAW/EKFtA==,iv:ew0MKP5zdQvqfla2y7xkcXLm3qSMlf5sucn8Qu3G3Zs=,tag:dqe239LD+wPVqLQFF+nz2A==,type:str] sops: age: - enc: | @@ -30,8 +33,8 @@ sops: QzBBdJ6TiDwJ1rwwM211smfIbcYhWX6C/EfitI5XoIW8aS4ln1g1Ww== -----END AGE ENCRYPTED FILE----- recipient: age187zdk64205lx7r83a4m26pfa8hq6khc8sf83k266jasngeq08v3sfqswfg - lastmodified: "2026-07-29T20:26:44Z" - mac: ENC[AES256_GCM,data:dHiB768SBppBrjNoSpgDvriCgrSNzIkpSYzm9REx4otbnvNjHYPhP9OdsZ6zTcoFoX+41/zE13r4EoAz4F++jdJidv1+7DMFvVUKMuFNzp2VjDfB1ZNzzyM+px7l83L5eNhgNPp26mweOVMEz92NgB916KMY+5e3uUeSR+iIdho=,iv:7k8rugBXULjPSOZukQiufNn9Vs5uWVhcvFKIhD1o7so=,tag:IE+2uhzGS2xJJgFHuc94CA==,type:str] + lastmodified: "2026-10-08T07:56:43Z" + mac: ENC[AES256_GCM,data:CgD19xHN5nEg9TMQq/c4Xpv1LH0LfcXrkfBMUcbJiIn4FsSzQetLYxueeDXUVOujOb9ZSEin1nR+OZwa/nuwN9mTg37c0XZ6HV0UvNNEIrN/o4doUCQa2jLmbX8etxeNhF7oWJ84OgQIhPLvLwp3Z0A/+kPKbmtz3znQNgKoHQY=,iv:6Dt7z55az2SJovJ7ozG35Q99a08kOrKl/uQCUVcbq8c=,tag:M6p0ZJzoFnWgspnGknq7SQ==,type:str] pgp: - created_at: "2026-09-24T19:54:13Z" enc: |- diff --git a/inventories/chaosknoten/host_vars/jitsi.yaml b/inventories/chaosknoten/host_vars/jitsi.yaml new file mode 100644 index 00000000..3673cbf9 --- /dev/null +++ b/inventories/chaosknoten/host_vars/jitsi.yaml @@ -0,0 +1,13 @@ +nftables__config: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/nftables/nftables.conf') }}" + +# renovate: datasource=github-releases depName=jitsi/docker-jitsi-meet versioning=regex:^stable-(?\d+)$ +jitsi__version: stable-11248 +jitsi__domain: jitsi.hamburg.ccc.de +jitsi__letsencrypt_email: le-admin@hamburg.ccc.de +jitsi__jvb_advertise_ips: + - 212.12.51.139 + - 2a00:14b0:f000:23:51:139:0:1 +jitsi__whiteboard: true +jitsi__turn: true +jitsi__prometheus_exporter: true +# jitsi__jicofo_auth_password, jitsi__jvb_auth_password and jitsi__turn_secret are defined in jitsi.sops.yaml. diff --git a/inventories/chaosknoten/hosts.yaml b/inventories/chaosknoten/hosts.yaml index 7d641ae3..fad62d3c 100644 --- a/inventories/chaosknoten/hosts.yaml +++ b/inventories/chaosknoten/hosts.yaml @@ -141,6 +141,7 @@ systemd_networkd_hosts: router: nftables_hosts: hosts: + jitsi: router: docker_compose_hosts: hosts: @@ -258,6 +259,9 @@ wiki_hosts: netbox_hosts: hosts: netbox: +jitsi_hosts: + hosts: + jitsi: proxmox_vm_template_hosts: hosts: chaosknoten: diff --git a/resources/chaosknoten/jitsi/nftables/nftables.conf b/resources/chaosknoten/jitsi/nftables/nftables.conf new file mode 100644 index 00000000..0c239986 --- /dev/null +++ b/resources/chaosknoten/jitsi/nftables/nftables.conf @@ -0,0 +1,65 @@ +#!/usr/sbin/nft -f + +# Docker manages its own tables (via iptables-nft), so don't use "flush ruleset" here, +# but only replace our own table. This also means that a reload is fine, but stopping +# or restarting nftables.service (which flushes the whole ruleset) requires restarting +# Docker afterwards. +# Traffic to ports published by Docker containers (web, JVB, exporter) gets DNATed in +# prerouting and therefore passes the forward hook instead of the input hook. + +## Variables + +define if_wan = "ens18" + +# Prometheus on grafana (IPv4 via the NAT of router) +define prometheus_v4 = { 212.12.48.123 } +define prometheus_v6 = { 2a00:14b0:42:102::e } + +## Rules + +table inet host_filter +delete table inet host_filter + +table inet host_filter { + # Runs before Docker's DNAT, so this also applies to ports published by containers. + chain prerouting { + type filter hook prerouting priority dstnat - 10; policy accept; + + iifname $if_wan tcp dport 9888 ip saddr $prometheus_v4 accept comment "jitsi exporter (container)" + iifname $if_wan tcp dport 9888 ip6 saddr $prometheus_v6 accept comment "jitsi exporter (container)" + iifname $if_wan tcp dport 9888 drop + } + + chain input { + type filter hook input priority filter; policy drop; + + iifname "lo" accept + ct state invalid drop + ct state established,related accept + + # ICMPv6 is needed for neighbour discovery etc. + meta l4proto ipv6-icmp accept + meta l4proto icmp accept + + # Docker networks + iifname { "docker0", "br-*" } accept + + tcp dport 22 accept comment "ssh" + + # Ports published by containers normally get DNATed and never reach the input + # hook, but they do when handled by docker-proxy (e.g. for IPv6 without IPv6 + # enabled on the Docker network). + tcp dport { 80, 443 } accept comment "jitsi web (container)" + udp dport 10000 accept comment "jitsi jvb (container)" + + # coturn (runs in the host network namespace) + udp dport 3478 accept comment "turn" + tcp dport 3478 accept comment "turn" + tcp dport 5349 accept comment "turns" + udp dport 5349 accept comment "turns (dtls)" + udp dport 49152-65535 accept comment "turn relay ports" + + tcp dport { 9100, 9888 } ip saddr $prometheus_v4 accept comment "node exporter, jitsi exporter (container)" + tcp dport { 9100, 9888 } ip6 saddr $prometheus_v6 accept comment "node exporter, jitsi exporter (container)" + } +} diff --git a/roles/nftables/tasks/main.yaml b/roles/nftables/tasks/main.yaml index 6a72800f..ec5daaf2 100644 --- a/roles/nftables/tasks/main.yaml +++ b/roles/nftables/tasks/main.yaml @@ -13,3 +13,10 @@ group: root become: true notify: Reload nftables service + +- name: ensure nftables service is enabled and started + ansible.builtin.systemd_service: + name: nftables + enabled: true + state: started + become: true -- 2.54.0 From 610a1faca82edc3f3df5b819952e8bcdc28b1c5d Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Thu, 8 Oct 2026 10:50:23 +0200 Subject: [PATCH 3/4] Allow UI customization --- roles/jitsi/README.md | 7 ++++ roles/jitsi/defaults/main.yaml | 1 + roles/jitsi/meta/argument_specs.yaml | 12 ++++++ roles/jitsi/tasks/main.yaml | 40 +++++++++++++++++++ .../jitsi/templates/compose.override.yaml.j2 | 9 ++++- 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/roles/jitsi/README.md b/roles/jitsi/README.md index bd3db9bb..57f5f8e6 100644 --- a/roles/jitsi/README.md +++ b/roles/jitsi/README.md @@ -16,6 +16,9 @@ coturn uses the certificate obtained by the web container and is restarted weekl It needs 3478/udp+tcp, 5349/tcp and the relay port range 49152-65535/udp to be reachable. The exporter scrapes the JVB statistics and listens on port 9888. +Files can be added to or replaced in the Jitsi Meet web root (`/usr/share/jitsi-meet` in the web container) using `jitsi__web_files`, e.g. for branding. +They are deployed to `/opt/jitsi/web-files/` and bind-mounted into the web container. + ## Supported Distributions Should work on Debian-based distributions. @@ -49,6 +52,10 @@ Should work on Debian-based distributions. Defaults to `""` (no file). - `jitsi__custom_interface_config_js`: Content of `custom-interface_config.js`, which gets appended to the generated `interface_config.js`. Defaults to `""` (no file). +- `jitsi__web_files`: List of files to mount into the Jitsi Meet web root. + Defaults to `[ ]`. +- `jitsi__web_files.*.path`: Path relative to the web root, e.g. `images/watermark.svg` or `static/welcomePageAdditionalContent.html`. +- `jitsi__web_files.*.content`: The content of the file. ## Links & Resources diff --git a/roles/jitsi/defaults/main.yaml b/roles/jitsi/defaults/main.yaml index 9782b636..5bed993e 100644 --- a/roles/jitsi/defaults/main.yaml +++ b/roles/jitsi/defaults/main.yaml @@ -6,3 +6,4 @@ jitsi__prometheus_exporter: false jitsi__env: { } jitsi__custom_config_js: "" jitsi__custom_interface_config_js: "" +jitsi__web_files: [ ] diff --git a/roles/jitsi/meta/argument_specs.yaml b/roles/jitsi/meta/argument_specs.yaml index 2c65ca54..36ee25b6 100644 --- a/roles/jitsi/meta/argument_specs.yaml +++ b/roles/jitsi/meta/argument_specs.yaml @@ -55,3 +55,15 @@ argument_specs: type: str required: false default: "" + jitsi__web_files: + type: list + elements: dict + required: false + default: [ ] + options: + path: + type: str + required: true + content: + type: str + required: true diff --git a/roles/jitsi/tasks/main.yaml b/roles/jitsi/tasks/main.yaml index a7beaaa4..92b9da83 100644 --- a/roles/jitsi/tasks/main.yaml +++ b/roles/jitsi/tasks/main.yaml @@ -140,6 +140,46 @@ state: "{{ 'present' if jitsi__turn else 'absent' }}" become: true +# Files mounted into the web container, replacing or adding to the files of Jitsi Meet. +- name: make sure the directories for the web files exist + ansible.builtin.file: + path: "{{ jitsi__base_dir }}/web-files/{{ item }}" + state: directory + mode: "0755" + owner: root + group: root + become: true + loop: "{{ [''] + (jitsi__web_files | map(attribute='path') | map('dirname') | unique | reject('equalto', '') | list) }}" + +- name: deploy the web files + ansible.builtin.copy: + content: "{{ item.content }}" + dest: "{{ jitsi__base_dir }}/web-files/{{ item.path }}" + mode: "0644" + owner: root + group: root + become: true + loop: "{{ jitsi__web_files }}" + loop_control: + label: "{{ item.path }}" + # Single file bind mounts keep pointing to the replaced file until the container is restarted. + notify: restart jitsi web + +- name: find web files to remove + ansible.builtin.find: + paths: "{{ jitsi__base_dir }}/web-files" + recurse: true + register: jitsi__web_files_found + +- name: remove web files, which should be removed + ansible.builtin.file: + path: "{{ item }}" + state: absent + become: true + loop: "{{ jitsi__web_files_found.files | map(attribute='path') | reject('in', jitsi__web_files_paths) | list }}" + vars: + jitsi__web_files_paths: "{{ jitsi__web_files | map(attribute='path') | map('regex_replace', '^', jitsi__base_dir ~ '/web-files/') | list }}" + # Changes to these make `docker compose up` below recreate the affected containers. - name: deploy the compose override file ansible.builtin.template: diff --git a/roles/jitsi/templates/compose.override.yaml.j2 b/roles/jitsi/templates/compose.override.yaml.j2 index 05842569..628b83bc 100644 --- a/roles/jitsi/templates/compose.override.yaml.j2 +++ b/roles/jitsi/templates/compose.override.yaml.j2 @@ -8,6 +8,13 @@ networks: enable_ipv6: true services: +{% if jitsi__web_files | length > 0 %} + web: + volumes: +{% for file in jitsi__web_files %} + - {{ jitsi__base_dir }}/web-files/{{ file.path }}:/usr/share/jitsi-meet/{{ file.path }}:ro +{% endfor %} +{% endif %} {% if jitsi__turn %} coturn: image: docker.io/coturn/coturn:4.18.0 @@ -35,6 +42,6 @@ services: networks: meet.jitsi: {% endif %} -{% if not (jitsi__turn or jitsi__prometheus_exporter) %} +{% if not (jitsi__web_files or jitsi__turn or jitsi__prometheus_exporter) %} {} {% endif %} -- 2.54.0 From 8bf5690275ad566e9014a9d9bbd4d364ec9c5513 Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Thu, 8 Oct 2026 10:50:48 +0200 Subject: [PATCH 4/4] Customize with our logos --- inventories/chaosknoten/host_vars/jitsi.yaml | 20 +++++++++++++++++++ .../chaosknoten/jitsi/web/background.svg | 18 +++++++++++++++++ resources/chaosknoten/jitsi/web/branding.json | 5 +++++ .../chaosknoten/jitsi/web/ccchh-logo.svg | 5 +++++ .../chaosknoten/jitsi/web/round-logo.svg | 7 +++++++ .../web/welcomePageAdditionalContent.html | 13 ++++++++++++ 6 files changed, 68 insertions(+) create mode 100644 resources/chaosknoten/jitsi/web/background.svg create mode 100644 resources/chaosknoten/jitsi/web/branding.json create mode 100644 resources/chaosknoten/jitsi/web/ccchh-logo.svg create mode 100644 resources/chaosknoten/jitsi/web/round-logo.svg create mode 100644 resources/chaosknoten/jitsi/web/welcomePageAdditionalContent.html diff --git a/inventories/chaosknoten/host_vars/jitsi.yaml b/inventories/chaosknoten/host_vars/jitsi.yaml index 3673cbf9..a22c3827 100644 --- a/inventories/chaosknoten/host_vars/jitsi.yaml +++ b/inventories/chaosknoten/host_vars/jitsi.yaml @@ -11,3 +11,23 @@ jitsi__whiteboard: true jitsi__turn: true jitsi__prometheus_exporter: true # jitsi__jicofo_auth_password, jitsi__jvb_auth_password and jitsi__turn_secret are defined in jitsi.sops.yaml. + +# Branding +jitsi__env: + DYNAMIC_BRANDING_URL: https://jitsi.hamburg.ccc.de/static/ccchh-branding.json +jitsi__custom_interface_config_js: | + // Replace the footer with app store links with our own in welcomePageAdditionalContent.html. + interfaceConfig.DISPLAY_WELCOME_FOOTER = false; + interfaceConfig.DISPLAY_WELCOME_PAGE_CONTENT = true; + interfaceConfig.JITSI_WATERMARK_LINK = 'https://hamburg.ccc.de/'; +jitsi__web_files: + - path: images/watermark.svg + content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/web/ccchh-logo.svg') }}" + - path: images/favicon.svg + content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/web/round-logo.svg') }}" + - path: images/ccchh-background.svg + content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/web/background.svg') }}" + - path: static/ccchh-branding.json + content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/web/branding.json') }}" + - path: static/welcomePageAdditionalContent.html + content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/jitsi/web/welcomePageAdditionalContent.html') }}" diff --git a/resources/chaosknoten/jitsi/web/background.svg b/resources/chaosknoten/jitsi/web/background.svg new file mode 100644 index 00000000..15f8531a --- /dev/null +++ b/resources/chaosknoten/jitsi/web/background.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/resources/chaosknoten/jitsi/web/branding.json b/resources/chaosknoten/jitsi/web/branding.json new file mode 100644 index 00000000..acb28508 --- /dev/null +++ b/resources/chaosknoten/jitsi/web/branding.json @@ -0,0 +1,5 @@ +{ + "logoImageUrl": "https://jitsi.hamburg.ccc.de/images/watermark.svg", + "logoClickUrl": "https://hamburg.ccc.de/", + "backgroundImageUrl": "https://jitsi.hamburg.ccc.de/images/ccchh-background.svg" +} diff --git a/resources/chaosknoten/jitsi/web/ccchh-logo.svg b/resources/chaosknoten/jitsi/web/ccchh-logo.svg new file mode 100644 index 00000000..84abd70e --- /dev/null +++ b/resources/chaosknoten/jitsi/web/ccchh-logo.svg @@ -0,0 +1,5 @@ + + +Chaos Computer Club Hamburg Logo + + diff --git a/resources/chaosknoten/jitsi/web/round-logo.svg b/resources/chaosknoten/jitsi/web/round-logo.svg new file mode 100644 index 00000000..9d139ddc --- /dev/null +++ b/resources/chaosknoten/jitsi/web/round-logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/chaosknoten/jitsi/web/welcomePageAdditionalContent.html b/resources/chaosknoten/jitsi/web/welcomePageAdditionalContent.html new file mode 100644 index 00000000..982f09ef --- /dev/null +++ b/resources/chaosknoten/jitsi/web/welcomePageAdditionalContent.html @@ -0,0 +1,13 @@ + -- 2.54.0