diff --git a/.forgejo/workflows/deploy-docs.yaml b/.forgejo/workflows/deploy-docs.yaml index 6e0acd6d..ed31e899 100644 --- a/.forgejo/workflows/deploy-docs.yaml +++ b/.forgejo/workflows/deploy-docs.yaml @@ -11,7 +11,7 @@ jobs: build: runs-on: docker container: - image: docker.io/library/python:3.14-trixie + image: docker.io/library/python:3.15-trixie steps: - name: Pipeline info run: | diff --git a/.forgejo/workflows/lint.yaml b/.forgejo/workflows/lint.yaml index 5c1b82ac..6a31829c 100644 --- a/.forgejo/workflows/lint.yaml +++ b/.forgejo/workflows/lint.yaml @@ -24,7 +24,7 @@ jobs: # work in our environmnet. # Rather manually setup python (pip) before instead. - name: Run ansible-lint - uses: https://github.com/ansible/ansible-lint@v26.6.0 + uses: https://github.com/ansible/ansible-lint@v26.10.1 with: setup_python: "false" requirements_file: "requirements.yml" diff --git a/inventories/chaosknoten/host_vars/cloud.yaml b/inventories/chaosknoten/host_vars/cloud.yaml index 8f299b2d..a441ac84 100644 --- a/inventories/chaosknoten/host_vars/cloud.yaml +++ b/inventories/chaosknoten/host_vars/cloud.yaml @@ -1,7 +1,7 @@ # renovate: datasource=docker depName=git.hamburg.ccc.de/ccchh/oci-images/nextcloud nextcloud__version: 34 # renovate: datasource=docker depName=docker.io/library/postgres -nextcloud__postgres_version: 15.18 +nextcloud__postgres_version: 15.19 nextcloud__fqdn: cloud.hamburg.ccc.de nextcloud__data_dir: /data/nextcloud nextcloud__extra_configuration: "{{ lookup('ansible.builtin.template', 'resources/chaosknoten/cloud/nextcloud/extra_configuration.config.php.j2') }}" 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/host_vars/netbox.yaml b/inventories/chaosknoten/host_vars/netbox.yaml index 312b1d70..50bf76b2 100644 --- a/inventories/chaosknoten/host_vars/netbox.yaml +++ b/inventories/chaosknoten/host_vars/netbox.yaml @@ -1,5 +1,5 @@ # renovate: datasource=github-releases depName=netbox packageName=netbox-community/netbox -netbox__version: "v4.6.4" +netbox__version: "v4.7.3" netbox__config: "{{ lookup('ansible.builtin.template', 'resources/chaosknoten/netbox/netbox/configuration.py.j2') }}" netbox__custom_pipeline_oidc_group_and_role_mapping: true diff --git a/inventories/chaosknoten/host_vars/pad.yaml b/inventories/chaosknoten/host_vars/pad.yaml index 0c9b29ea..c2fa834f 100644 --- a/inventories/chaosknoten/host_vars/pad.yaml +++ b/inventories/chaosknoten/host_vars/pad.yaml @@ -13,4 +13,4 @@ nginx__configurations: content: "{{ lookup('ansible.builtin.file', 'resources/chaosknoten/pad/nginx/pad.hamburg.ccc.de.conf') }}" # renovate: datasource=docker depName=quay.io/hedgedoc/hedgedoc -host_pad__docker_compose__hedgedoc_version: 1.11.1 +host_pad__docker_compose__hedgedoc_version: 1.12.0 diff --git a/inventories/chaosknoten/hosts.yaml b/inventories/chaosknoten/hosts.yaml index 5c29a4e8..8880311e 100644 --- a/inventories/chaosknoten/hosts.yaml +++ b/inventories/chaosknoten/hosts.yaml @@ -146,6 +146,7 @@ systemd_networkd_hosts: router: nftables_hosts: hosts: + jitsi: router: docker_compose_hosts: hosts: @@ -263,6 +264,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/ccchoir/docker_compose/compose.yaml.j2 b/resources/chaosknoten/ccchoir/docker_compose/compose.yaml.j2 index 72d898fa..00cac00e 100644 --- a/resources/chaosknoten/ccchoir/docker_compose/compose.yaml.j2 +++ b/resources/chaosknoten/ccchoir/docker_compose/compose.yaml.j2 @@ -16,24 +16,24 @@ services: backend: restart: unless-stopped - app: - image: docker.io/library/wordpress:7-php8.2 - environment: - - "WORDPRESS_DB_HOST=database" - - "WORDPRESS_DB_NAME=wordpress" - - "WORDPRESS_DB_USER=wordpress" - - "WORDPRESS_TABLE_PREFIX=wp_" - - "WORDPRESS_DB_PASSWORD={{ secret__wordpress_db_password }}" - volumes: - - wordpress:/var/www/html/wp-content - ports: - - "127.0.0.1:3000:80" - networks: - backend: - frontend: - restart: unless-stopped - depends_on: - - database +# app: +# image: docker.io/library/wordpress:7-php8.2 +# environment: +# - "WORDPRESS_DB_HOST=database" +# - "WORDPRESS_DB_NAME=wordpress" +# - "WORDPRESS_DB_USER=wordpress" +# - "WORDPRESS_TABLE_PREFIX=wp_" +# - "WORDPRESS_DB_PASSWORD={{ secret__wordpress_db_password }}" +# volumes: +# - wordpress:/var/www/html/wp-content +# ports: +# - "127.0.0.1:3000:80" +# networks: +# backend: +# frontend: +# restart: unless-stopped +# depends_on: +# - database volumes: database: {} diff --git a/resources/chaosknoten/draupnir/docker_compose/compose.yaml b/resources/chaosknoten/draupnir/docker_compose/compose.yaml index 42bf8fda..e346c6bb 100644 --- a/resources/chaosknoten/draupnir/docker_compose/compose.yaml +++ b/resources/chaosknoten/draupnir/docker_compose/compose.yaml @@ -3,7 +3,7 @@ services: draupnir: - image: ghcr.io/the-draupnir-project/draupnir:v3.1.0 + image: ghcr.io/the-draupnir-project/draupnir:v3.2.0 command: [ "bot", "--draupnir-config", "/etc/draupnir/production.yaml" ] restart: unless-stopped volumes: diff --git a/resources/chaosknoten/grafana/docker_compose/compose.yaml.j2 b/resources/chaosknoten/grafana/docker_compose/compose.yaml.j2 index 05d1c7e5..9931b7c6 100644 --- a/resources/chaosknoten/grafana/docker_compose/compose.yaml.j2 +++ b/resources/chaosknoten/grafana/docker_compose/compose.yaml.j2 @@ -2,7 +2,7 @@ services: prometheus: - image: docker.io/prom/prometheus:v3.13.1 + image: docker.io/prom/prometheus:v3.15.0 container_name: prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' @@ -19,7 +19,7 @@ services: - prom_data:/prometheus alertmanager: - image: docker.io/prom/alertmanager:v0.33.1 + image: docker.io/prom/alertmanager:v0.34.1 container_name: alertmanager command: - '--config.file=/etc/alertmanager/alertmanager.yaml' @@ -32,7 +32,7 @@ services: - alertmanager_data:/alertmanager grafana: - image: docker.io/grafana/grafana:13.1.0 + image: docker.io/grafana/grafana:13.2.4 container_name: grafana ports: - 3000:3000 @@ -46,7 +46,7 @@ services: - graf_data:/var/lib/grafana pve-exporter: - image: docker.io/prompve/prometheus-pve-exporter:3.9.0 + image: docker.io/prompve/prometheus-pve-exporter:3.10.1 container_name: pve-exporter ports: - 9221:9221 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/resources/chaosknoten/ntfy/docker_compose/compose.yaml.j2 b/resources/chaosknoten/ntfy/docker_compose/compose.yaml.j2 index a982bf3a..fbd42fa6 100644 --- a/resources/chaosknoten/ntfy/docker_compose/compose.yaml.j2 +++ b/resources/chaosknoten/ntfy/docker_compose/compose.yaml.j2 @@ -1,7 +1,7 @@ --- services: ntfy: - image: docker.io/binwiederhier/ntfy:v2.26.0 + image: docker.io/binwiederhier/ntfy:v2.32.0 container_name: ntfy command: - serve diff --git a/resources/chaosknoten/pretalx/docker_compose/compose.yaml.j2 b/resources/chaosknoten/pretalx/docker_compose/compose.yaml.j2 index 6b1683cc..5e3ff3e6 100644 --- a/resources/chaosknoten/pretalx/docker_compose/compose.yaml.j2 +++ b/resources/chaosknoten/pretalx/docker_compose/compose.yaml.j2 @@ -15,7 +15,7 @@ services: - pretalx_net redis: - image: docker.io/library/redis:8.8.0 + image: docker.io/library/redis:8.10.2 restart: unless-stopped volumes: - redis:/data diff --git a/resources/chaosknoten/tickets/docker_compose/compose.yaml.j2 b/resources/chaosknoten/tickets/docker_compose/compose.yaml.j2 index 28cef213..eb92c851 100644 --- a/resources/chaosknoten/tickets/docker_compose/compose.yaml.j2 +++ b/resources/chaosknoten/tickets/docker_compose/compose.yaml.j2 @@ -13,7 +13,7 @@ services: restart: unless-stopped redis: - image: docker.io/library/redis:8.8.0 + image: docker.io/library/redis:8.10.2 ports: - "6379:6379" volumes: diff --git a/resources/external/status/docker_compose/compose.yaml.j2 b/resources/external/status/docker_compose/compose.yaml.j2 index 638ebbe2..5329b644 100644 --- a/resources/external/status/docker_compose/compose.yaml.j2 +++ b/resources/external/status/docker_compose/compose.yaml.j2 @@ -4,7 +4,7 @@ services: database: - image: docker.io/library/postgres:18.4 + image: docker.io/library/postgres:18.6 restart: always volumes: - ./database:/var/lib/postgresql @@ -16,7 +16,7 @@ services: - gatus gatus: - image: ghcr.io/twin/gatus:v5.36.0 + image: ghcr.io/twin/gatus:v5.37.0 restart: always ports: - "8080:8080" diff --git a/roles/jitsi/README.md b/roles/jitsi/README.md new file mode 100644 index 00000000..b3392c6a --- /dev/null +++ b/roles/jitsi/README.md @@ -0,0 +1,70 @@ +# 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. + IPv4 addresses are put into `JVB_ADVERTISE_IPS`, which docker-jitsi-meet maps to the IPv4 address of the JVB container. + IPv6 addresses are mapped to the IPv6 address of the JVB container in `custom-jvb.conf` instead, for which the `meet.jitsi` network gets a fixed IPv6 subnet and the JVB a fixed IPv6 address. + Defaults to `[ ]`. +- `jitsi__ipv6_subnet`: The IPv6 subnet of the `meet.jitsi` network, if IPv6 addresses are advertised. + Defaults to `fd4a:7c1e:93b5:1::/64`. +- `jitsi__jvb_ipv6_address`: The IPv6 address of the JVB container within `jitsi__ipv6_subnet`. + Defaults to `fd4a:7c1e:93b5:1::10`. +- `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..8532b275 --- /dev/null +++ b/roles/jitsi/defaults/main.yaml @@ -0,0 +1,11 @@ +jitsi__timezone: Europe/Berlin +jitsi__jvb_advertise_ips: [ ] +jitsi__ipv6_subnet: fd4a:7c1e:93b5:1::/64 +jitsi__jvb_ipv6_address: fd4a:7c1e:93b5:1::10 +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..6f0eb2e3 --- /dev/null +++ b/roles/jitsi/handlers/main.yaml @@ -0,0 +1,29 @@ +- 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 jvb + community.docker.docker_compose_v2: + project_src: "{{ jitsi__base_dir }}" + project_name: jitsi + files: "{{ jitsi__compose_files }}" + services: + - jvb + 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..e2df54fe --- /dev/null +++ b/roles/jitsi/meta/argument_specs.yaml @@ -0,0 +1,77 @@ +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__ipv6_subnet: + type: str + required: false + default: fd4a:7c1e:93b5:1::/64 + jitsi__jvb_ipv6_address: + type: str + required: false + default: fd4a:7c1e:93b5:1::10 + 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..4846ad5a --- /dev/null +++ b/roles/jitsi/tasks/main.yaml @@ -0,0 +1,245 @@ +- 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: deploy custom JVB configuration + ansible.builtin.template: + src: custom-jvb.conf.j2 + dest: "{{ jitsi__config_dir }}/jvb/custom-jvb.conf" + mode: "0644" + owner: root + group: root + become: true + when: jitsi__jvb_advertise_ipv6s | length > 0 + notify: restart jitsi jvb + +- name: remove unconfigured custom JVB configuration + ansible.builtin.file: + path: "{{ jitsi__config_dir }}/jvb/custom-jvb.conf" + state: absent + become: true + when: jitsi__jvb_advertise_ipv6s | length == 0 + notify: restart jitsi jvb + +- 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..2cefd134 --- /dev/null +++ b/roles/jitsi/templates/compose.override.yaml.j2 @@ -0,0 +1,59 @@ +# {{ 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 +{% if jitsi__jvb_advertise_ipv6s | length > 0 %} + # A fixed subnet, so the JVB can have a fixed IPv6 address to map the public ones to. + ipam: + config: + - subnet: {{ jitsi__ipv6_subnet }} +{% endif %} + +services: +{% if jitsi__jvb_advertise_ipv6s | length > 0 %} + jvb: + networks: + meet.jitsi: + ipv6_address: {{ jitsi__jvb_ipv6_address }} +{% endif %} +{% 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__jvb_advertise_ipv6s or jitsi__web_files or jitsi__turn or jitsi__prometheus_exporter) %} + {} +{% endif %} diff --git a/roles/jitsi/templates/custom-jvb.conf.j2 b/roles/jitsi/templates/custom-jvb.conf.j2 new file mode 100644 index 00000000..050511a9 --- /dev/null +++ b/roles/jitsi/templates/custom-jvb.conf.j2 @@ -0,0 +1,11 @@ +# {{ ansible_managed }} +# Included at the end of the generated jvb.conf. +# Map the advertised IPv6 addresses to the pinned IPv6 address of the JVB, in addition to +# the mappings of the IPv4 addresses generated from JVB_ADVERTISE_IPS. +{% for ip in jitsi__jvb_advertise_ipv6s %} +ice4j.harvest.mapping.static-mappings += { + local-address = "{{ jitsi__jvb_ipv6_address }}" + public-address = "{{ ip }}" + name = "ipv6-{{ loop.index0 }}" +} +{% endfor %} diff --git a/roles/jitsi/templates/jitsi.env.j2 b/roles/jitsi/templates/jitsi.env.j2 new file mode 100644 index 00000000..f5d8a447 --- /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_ipv4s | length > 0 %} +JVB_ADVERTISE_IPS={{ jitsi__jvb_advertise_ipv4s | 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..993aa412 --- /dev/null +++ b/roles/jitsi/vars/main.yaml @@ -0,0 +1,8 @@ +jitsi__base_dir: /opt/jitsi +jitsi__config_dir: "{{ jitsi__base_dir }}/config" +jitsi__release_dir: "{{ jitsi__base_dir }}/releases/docker-jitsi-meet-{{ jitsi__version }}" +# docker-jitsi-meet maps all JVB_ADVERTISE_IPS to the container's IPv4 address, so IPv6 +# addresses are mapped to the pinned IPv6 address of the JVB in custom-jvb.conf instead. +jitsi__jvb_advertise_ipv4s: "{{ jitsi__jvb_advertise_ips | reject('search', ':') | list }}" +jitsi__jvb_advertise_ipv6s: "{{ jitsi__jvb_advertise_ips | select('search', ':') | list }}" +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