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..a22c3827 --- /dev/null +++ b/inventories/chaosknoten/host_vars/jitsi.yaml @@ -0,0 +1,33 @@ +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. + +# 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/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/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/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/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 @@ + diff --git a/roles/jitsi/README.md b/roles/jitsi/README.md new file mode 100644 index 00000000..57f5f8e6 --- /dev/null +++ b/roles/jitsi/README.md @@ -0,0 +1,64 @@ +# 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. + +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. + +## 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). +- `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 new file mode 100644 index 00000000..5bed993e --- /dev/null +++ b/roles/jitsi/defaults/main.yaml @@ -0,0 +1,9 @@ +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: "" +jitsi__web_files: [ ] 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..36ee25b6 --- /dev/null +++ b/roles/jitsi/meta/argument_specs.yaml @@ -0,0 +1,69 @@ +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: "" + 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/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..92b9da83 --- /dev/null +++ b/roles/jitsi/tasks/main.yaml @@ -0,0 +1,226 @@ +- 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 + +# 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: + 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..628b83bc --- /dev/null +++ b/roles/jitsi/templates/compose.override.yaml.j2 @@ -0,0 +1,47 @@ +# {{ 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__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 + 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__web_files or 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'] }}" 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