diff --git a/README.md b/README.md index b84158ba3..60e2cd3b2 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The homeserver is the backbone of your Matrix system. Choose one from the follow | [Synapse](https://github.com/element-hq/synapse) | ✅ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network | [Link](docs/configuring-playbook-synapse.md) | | [Conduit](https://conduit.rs) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Conduit is a lightweight open-source server implementation of the Matrix Specification with a focus on easy setup and low system requirements | [Link](docs/configuring-playbook-conduit.md) | | [continuwuity](https://continuwuity.org) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. | [Link](docs/configuring-playbook-continuwuity.md) | +| [Tuwunel](https://matrix-construct.github.io/tuwunel/) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Tuwunel is the official successor to conduwuit. | [Link](docs/configuring-playbook-tuwunel.md) | | [Dendrite](https://github.com/element-hq/dendrite) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Dendrite is a second-generation Matrix homeserver written in Go, an alternative to Synapse. | [Link](docs/configuring-playbook-dendrite.md) | ### Clients diff --git a/docs/configuring-playbook-tuwunel.md b/docs/configuring-playbook-tuwunel.md new file mode 100644 index 000000000..0932f552b --- /dev/null +++ b/docs/configuring-playbook-tuwunel.md @@ -0,0 +1,234 @@ + + +# Configuring Tuwunel (optional) + +The playbook can install and configure the [Tuwunel](https://matrix-construct.github.io/tuwunel/) Matrix homeserver for you. + +Tuwunel is a featureful homeserver written entirely in Rust, intended as a scalable, low-cost, enterprise-ready alternative to Synapse that fully implements the [Matrix specification](https://spec.matrix.org/latest/) for all but the most niche uses. It is the official successor to [conduwuit](configuring-playbook-conduwuit.md), is now sponsored by the government of Switzerland 🇨🇭 (where it is currently deployed for citizens), and is used by a number of organisations with a vested interest in its continued development. See the project's [documentation](https://matrix-construct.github.io/tuwunel/) for further background. + +By default, the playbook installs [Synapse](https://github.com/element-hq/synapse) as it's the only full-featured Matrix server at the moment. If that's okay, you can skip this document. + +> [!WARNING] +> - **You can't switch an existing Matrix server's implementation** (e.g. Synapse → Tuwunel). Proceed below only if you're OK with starting over, or you're dealing with a server on a new domain name which hasn't participated in the Matrix federation yet. The one exception is migrating from conduwuit; see [Migrating from conduwuit](#migrating-from-conduwuit). +> - **Homeserver implementations other than Synapse may not be fully functional** with every part of this playbook. Make yourself familiar with the trade-offs before proceeding. + +## Adjusting the playbook configuration + +To use Tuwunel, set the following on `inventory/host_vars/matrix.example.com/vars.yml`: + +```yaml +matrix_homeserver_implementation: tuwunel + +# Open the registration endpoint long enough to create your first user. +# After signing up, set this back to false. +matrix_tuwunel_config_allow_registration: true + +# A registration token to protect the endpoint from abuse. +# Generate one with `pwgen -s 64 1` or similar. +matrix_tuwunel_config_registration_token: '' +``` + +The first user account that registers becomes a server admin and is automatically invited to the admin room. See [Creating the first user account](#creating-the-first-user-account) below for the bootstrap procedure. + +## Wiring done for you + +When `matrix_homeserver_implementation: tuwunel` is set, the playbook automatically integrates Tuwunel with the rest of your stack: + +- **Federation.** Toggled by `matrix_homeserver_federation_enabled`. The federation virtual host (port 8448 in the default setup) is wired up via Traefik labels. +- **Well-known.** `matrix_tuwunel_config_well_known_client` is set to your public homeserver URL whenever SSL is enabled. Matrix clients use this for delegated-domain server discovery; identity-provider entries below can also omit their `callback_url`, since Tuwunel derives `/_matrix/client/unstable/login/sso/callback/` automatically. +- **Element Call / MatrixRTC.** When the [LiveKit JWT service](configuring-playbook-matrix-rtc.md) is enabled, Tuwunel publishes its public URL through `.well-known/matrix/client` per [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143). +- **Legacy calls (TURN).** When [Coturn](configuring-playbook-turn.md) is enabled, its URIs and shared secret (or username/password, depending on `coturn_authentication_method`) are wired automatically. + +## Extending the configuration + +Tuwunel exposes a large configuration surface. The role surfaces commonly used options as Ansible variables under `matrix_tuwunel_config_*`. See [`roles/custom/matrix-tuwunel/defaults/main.yml`](../roles/custom/matrix-tuwunel/defaults/main.yml) for the complete list, and [`roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2`](../roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2) for the rendered configuration. + +For options that aren't surfaced as a dedicated variable, [environment variables](https://matrix-construct.github.io/tuwunel/configuration.html#environment-variables) are the recommended override mechanism. They take priority over the rendered TOML, are scoped to the running container, and require no template patching: + +```yaml +matrix_tuwunel_environment_variables_extension: | + TUWUNEL_REQUEST_TIMEOUT=60 + TUWUNEL_DNS_CACHE_SIZE=131072 +``` + +Keys nested under a TOML section use `__` (double underscore) to descend, e.g. `TUWUNEL_WELL_KNOWN__SERVER`. User-named sections become path segments too: `TUWUNEL_STORAGE_PROVIDER__ARCHIVE__S3__URL` overrides the `url` field of the `archive` storage provider in the example below. + +If you need wholesale control of the configuration file, copy [`roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2`](../roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2) into your inventory and point `matrix_tuwunel_template_tuwunel_config` at your copy. + +The container image published as `:latest` is built with `io_uring`, `jemalloc`, LDAP, blurhashing, URL preview, sentry telemetry, and zstd compression all enabled, so most opt-in features are simply a configuration toggle away. + +### Identity providers (OAuth2 / OIDC) + +Configure one or more `[[global.identity_provider]]` entries via a list. Each entry maps directly to Tuwunel's [identity-provider fields](https://matrix-construct.github.io/tuwunel/authentication/providers.html); only the fields you set are emitted. GitHub, GitLab, and Google have built-in `issuer_url` defaults so a `client_id` plus `client_secret` is enough; for any other `brand` (Apple, Facebook, Keycloak, MAS, Twitter, etc.) you must supply `issuer_url` explicitly: + +```yaml +matrix_tuwunel_config_identity_providers: + - brand: keycloak + client_id: matrix + client_secret: '' + issuer_url: https://sso.example.com/realms/matrix + callback_url: https://matrix.example.com/_matrix/client/unstable/login/sso/callback/matrix + trusted: true + - brand: github + client_id: '' + client_secret: '' +``` + +Self-hosted providers must supply both `client_id` and `issuer_url`. Set `trusted: true` only on providers you operate yourself; trusting a public provider (GitHub, Google, etc.) is an account-takeover risk. + +### LDAP + +Tuwunel can authenticate `m.login.password` requests against an LDAP directory and, in search-then-bind mode, keep admin status in sync with directory membership. The shipped image already includes the `ldap` build feature. + +```yaml +matrix_tuwunel_config_ldap_enabled: true +matrix_tuwunel_config_ldap_uri: ldaps://ldap.example.com:636 +matrix_tuwunel_config_ldap_base_dn: ou=users,dc=example,dc=org +matrix_tuwunel_config_ldap_bind_dn: cn=ldap-reader,dc=example,dc=org +matrix_tuwunel_config_ldap_bind_password_file: /etc/tuwunel/ldap.pw +matrix_tuwunel_config_ldap_filter: '(&(objectClass=person)(memberOf=cn=matrix,ou=groups,dc=example,dc=org))' +``` + +> [!NOTE] +> `bind_password_file` is read **inside the container**. The role bind-mounts `/matrix/tuwunel/config` to `/etc/tuwunel` (read-only) and `/matrix/tuwunel/data` to `/var/lib/tuwunel`. To make the file available at the path above, drop it on the host at `/matrix/tuwunel/config/ldap.pw` (owned by `matrix:matrix`) before running the playbook; the role does not template secret files for you. + +For direct-bind, anonymous-search, and admin-sync details, see [LDAP authentication](https://matrix-construct.github.io/tuwunel/authentication/ldap.html). + +### JWT login + +Tuwunel can accept signed JSON Web Tokens both as a login flow and as a User-Interactive Authentication step: + +```yaml +matrix_tuwunel_config_jwt_enabled: true +matrix_tuwunel_config_jwt_key: '' +matrix_tuwunel_config_jwt_format: HMAC # one of HMAC, B64HMAC, ECDSA, EDDSA +matrix_tuwunel_config_jwt_algorithm: HS256 +matrix_tuwunel_config_jwt_audience: ['matrix'] +matrix_tuwunel_config_jwt_issuer: ['https://issuer.example.com'] +``` + +The defaults match Synapse's `experimental_features.jwt_config` semantics, so a key + algorithm port should authenticate the same set of tokens. See [Enterprise JWT](https://matrix-construct.github.io/tuwunel/authentication/jwt.html) for the full reference, including the asymmetric (ECDSA / EdDSA) formats and the operator-controlled UIAA override flow. + +### Media storage providers + +Each entry becomes a `[global.storage_provider..]` block. `kind` is `local` or `s3`; the remaining keys map directly to the fields documented in [Storage providers](https://matrix-construct.github.io/tuwunel/media/storage.html): + +```yaml +matrix_tuwunel_config_storage_providers: + - id: primary + kind: local + base_path: /var/lib/tuwunel/media + + - id: archive + kind: s3 + url: s3://my-bucket/media + region: us-east-1 + key: AKIA... + secret: '' + multipart_threshold: 100 MiB +``` + +The S3 backend ships with native multipart upload, so no goofys/rclone sidecar is required. MinIO, Cloudflare R2, and DigitalOcean Spaces all work; set `endpoint` and `use_vhost_request: false` as appropriate. + +> [!NOTE] +> Local provider paths must live under `/var/lib/tuwunel` (the container's data mount, persisted on the host at `/matrix/tuwunel/data`), or you must mount the target directory into the container yourself via `matrix_tuwunel_container_extra_arguments`. The container otherwise runs read-only. + +### RocksDB and cache tuning + +Tuwunel embeds RocksDB. The defaults (`rocksdb_compression_algo: zstd`) suit most deployments. For high-throughput servers you may want to enable direct I/O, raise parallelism, and bump the cache modifier: + +```yaml +matrix_tuwunel_config_rocksdb_direct_io: true +matrix_tuwunel_config_rocksdb_parallelism_threads: 8 +matrix_tuwunel_config_cache_capacity_modifier: 2.0 +matrix_tuwunel_config_database_backup_path: /var/lib/tuwunel/backups +``` + +If you run on ZFS, the [Tuwunel maintenance guide](https://matrix-construct.github.io/tuwunel/maintenance.html#zfs) lists the dataset properties (`recordsize`, `primarycache`, `compression`, `atime`, `logbias`) and config flags (`rocksdb_direct_io`, `rocksdb_allow_fallocate`) you need to adjust to avoid severe write amplification. + +To enable Sentry crash reporting, set `matrix_tuwunel_config_sentry_enabled: true`. + +### Federation gating + +Tuwunel accepts regular-expression patterns at every level of remote-server filtering: + +```yaml +matrix_tuwunel_config_forbidden_remote_server_names: + - 'bad\.example\.com$' +matrix_tuwunel_config_forbidden_remote_room_directory_server_names: + - 'spam\.example\.com$' +matrix_tuwunel_config_prevent_media_downloads_from: + - 'heavy\.example\.com$' +``` + +Tuwunel additionally implements [MSC4284 policy servers](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) for room-level federation gating; that lives in room state and needs no playbook configuration. + +### Default room version + +The role sets `default_room_version: '12'`, so newly created rooms default to Matrix [room version 12](https://github.com/matrix-org/matrix-spec-proposals/pull/4289) ("Hydra"). Override `matrix_tuwunel_config_default_room_version` if you need an earlier version for client compatibility. + +## Creating the first user account + +Unlike Synapse and Dendrite, Tuwunel does not register users from the command line or via the playbook. On first startup it logs a one-time-use registration token to its journal: + +```sh +# Adjust the duration if necessary or remove the --since argument. +journalctl -u matrix-tuwunel.service --since="10 minutes ago" +``` + +Use the token to create your first account from any client that supports token-gated registration (e.g. [Element Web](configuring-playbook-client-element-web.md)). The account is auto-promoted to admin and invited to the admin room together with the `@conduit:` server bot. The bot keeps the legacy `conduit` localpart due to the project's lineage from Conduit. + +## Configuring bridges and appservices + +The playbook does not auto-register appservices for Tuwunel. After your bridge has produced its `registration.yaml` (e.g. `/matrix/mautrix-signal/bridge/registration.yaml`), register it manually by sending the contents to the admin room, prefixed with `!admin appservices register` and wrapped in a fenced code block: + + !admin appservices register + ``` + id: signal + url: http://matrix-mautrix-signal:29328 + as_token: + hs_token: + sender_localpart: _bot_signalbot + rate_limited: false + namespaces: + users: + - exclusive: true + regex: '^@signal_.+:example\.org$' + - exclusive: true + regex: '^@signalbot:example\.org$' + aliases: + - exclusive: true + regex: '^#signal_.+:example\.org$' + ``` + +Registrations stored this way are persisted in the database and survive restarts. Re-running the command with the same `id` replaces the existing entry. See [Application services](https://matrix-construct.github.io/tuwunel/appservices.html) for the full reference and admin commands. + +## Migrating from conduwuit + +Tuwunel is a "binary swap" for conduwuit; it reads conduwuit's RocksDB layout directly, so migration is a data move, not an export/import. + +1. Set `matrix_homeserver_implementation: tuwunel` on `vars.yml` and remove any `matrix_conduwuit_*` overrides. +2. Run a full installation so that the new service is created and the old one removed (e.g. `just setup-all`). +3. Run `just run-tags tuwunel-migrate-from-conduwuit`. + +The migration stops `matrix-conduwuit.service`, copies `/matrix/conduwuit` into `/matrix/tuwunel`, renames the config file, and starts `matrix-tuwunel.service`. The freshly generated tuwunel data directory is preserved alongside as `/matrix/tuwunel_old` until you remove it manually. + +> [!CAUTION] +> Migrating from any other Conduit derivative (Conduit itself, Continuwuity, or any other fork) is **not supported** and will corrupt your database. All Conduit forks share the same linear database version with no awareness of each other; switching between them produces unrecoverable damage. See the [upstream migration table](https://matrix-construct.github.io/tuwunel/#migrating-to-tuwunel). + +## Troubleshooting + +As with all other services, the logs are available via [systemd-journald](https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html): + +```sh +journalctl -fu matrix-tuwunel +``` + +Logging verbosity is controlled by `matrix_tuwunel_config_log` in [`tracing-subscriber` env-filter syntax](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html). The default (`info,state_res=warn`) is reasonable for production; for debugging, try `debug` or scope it tighter, e.g. `info,tuwunel_service::sending=debug`. + +For RocksDB-level issues, online backups, and offline backup procedures, see the [Tuwunel maintenance guide](https://matrix-construct.github.io/tuwunel/maintenance.html). For protocol-compliance state across MSCs, the spec, and Complement, the project's [compliance dashboard](https://matrix-construct.github.io/tuwunel/development/compliance.html) is the authoritative tracker. diff --git a/docs/configuring-playbook.md b/docs/configuring-playbook.md index ee4a13731..7beb3309a 100644 --- a/docs/configuring-playbook.md +++ b/docs/configuring-playbook.md @@ -53,6 +53,8 @@ For a more custom setup, see the [Other configuration options](#other-configurat - [Configuring continuwuity](configuring-playbook-continuwuity.md), if you've switched to the [continuwuity](https://continuwuity.org) homeserver implementation + - [Configuring Tuwunel](configuring-playbook-tuwunel.md), if you've switched to the [Tuwunel](https://matrix-construct.github.io/tuwunel/) homeserver implementation + - [Configuring Dendrite](configuring-playbook-dendrite.md), if you've switched to the [Dendrite](https://matrix-org.github.io/dendrite) homeserver implementation - Server components: diff --git a/docs/container-images.md b/docs/container-images.md index 7c356cf07..c9b17c223 100644 --- a/docs/container-images.md +++ b/docs/container-images.md @@ -28,6 +28,7 @@ We try to stick to official images (provided by their respective projects) as mu | [Synapse](configuring-playbook-synapse.md) | [element-hq/synapse](https://ghcr.io/element-hq/synapse) | ✅ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network | | [Conduit](configuring-playbook-conduit.md) | [matrixconduit/matrix-conduit](https://hub.docker.com/r/matrixconduit/matrix-conduit) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Conduit is a lightweight open-source server implementation of the Matrix Specification with a focus on easy setup and low system requirements | | [continuwuity](configuring-playbook-continuwuity.md) | [continuwuation/continuwuity](https://forgejo.ellis.link/continuwuation/continuwuity) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. continuwuity is a continuation of conduwuit. | +| [Tuwunel](configuring-playbook-tuwunel.md) | [matrix-construct/tuwunel](https://ghcr.io/matrix-construct/tuwunel) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Tuwunel is the official successor to conduwuit. | | [Dendrite](configuring-playbook-dendrite.md) | [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith/) | ❌ | Storing your data and managing your presence in the [Matrix](http://matrix.org/) network. Dendrite is a second-generation Matrix homeserver written in Go, an alternative to Synapse. | ## Clients diff --git a/docs/howto-srv-server-delegation.md b/docs/howto-srv-server-delegation.md index 5e0055c7e..251169b21 100644 --- a/docs/howto-srv-server-delegation.md +++ b/docs/howto-srv-server-delegation.md @@ -26,7 +26,7 @@ The up-to-date list can be accessed on [traefik's documentation](https://doc.tra **Note**: the changes below instruct you how to do this for a basic Synapse installation. You will need to adapt the variable name and the content of the labels: -- if you're using another homeserver implementation (e.g. [Conduit](./configuring-playbook-conduit.md), [continuwuity](./configuring-playbook-continuwuity.md) or [Dendrite](./configuring-playbook-dendrite.md)) +- if you're using another homeserver implementation (e.g. [Conduit](./configuring-playbook-conduit.md), [continuwuity](./configuring-playbook-continuwuity.md), [Tuwunel](./configuring-playbook-tuwunel.md) or [Dendrite](./configuring-playbook-dendrite.md)) - if you're using [Synapse with workers enabled](./configuring-playbook-synapse.md#load-balancing-with-workers) (`matrix_synapse_workers_enabled: true`). In that case, it's actually the `matrix-synapse-reverse-proxy-companion` service which has Traefik labels attached Also, all instructions below are from an older version of the playbook and may not work anymore. diff --git a/group_vars/matrix_servers b/group_vars/matrix_servers index c6be2292e..753a56172 100755 --- a/group_vars/matrix_servers +++ b/group_vars/matrix_servers @@ -631,6 +631,7 @@ devture_systemd_service_manager_services_list_auto: | 'restart_necessary': ( (matrix_conduit_restart_necessary | bool) if matrix_homeserver_implementation == 'conduit' else (matrix_continuwuity_restart_necessary | bool) if matrix_homeserver_implementation == 'continuwuity' + else (matrix_tuwunel_restart_necessary | bool) if matrix_homeserver_implementation == 'tuwunel' else (matrix_dendrite_restart_necessary | bool) if matrix_homeserver_implementation == 'dendrite' else true ), @@ -1008,6 +1009,7 @@ matrix_homeserver_container_client_api_endpoint: |- 'dendrite': ('matrix-dendrite:' + matrix_dendrite_http_bind_port | default('8008') | string), 'conduit': ('matrix-conduit:' + matrix_conduit_port_number | default('8008') | string), 'continuwuity': ('matrix-continuwuity:' + matrix_continuwuity_config_port_number | default('8008') | string), + 'tuwunel': ('matrix-tuwunel:' + matrix_tuwunel_config_port_number | default('8008') | string), }[matrix_homeserver_implementation] }} @@ -1018,6 +1020,7 @@ matrix_homeserver_container_federation_api_endpoint: |- 'dendrite': ('matrix-dendrite:' + matrix_dendrite_http_bind_port | default('8008') | string), 'conduit': ('matrix-conduit:' + matrix_conduit_port_number | default('8008') | string), 'continuwuity': ('matrix-continuwuity:' + matrix_continuwuity_config_port_number | default('8008') | string), + 'tuwunel': ('matrix-tuwunel:' + matrix_tuwunel_config_port_number | default('8008') | string), }[matrix_homeserver_implementation] }} @@ -5558,6 +5561,7 @@ grafana_default_home_dashboard_path: |- 'dendrite': ('/etc/grafana/dashboards/node-exporter-full.json' if prometheus_node_exporter_enabled else ''), 'conduit': ('/etc/grafana/dashboards/node-exporter-full.json' if prometheus_node_exporter_enabled else ''), 'continuwuity': ('/etc/grafana/dashboards/node-exporter-full.json' if prometheus_node_exporter_enabled else ''), + 'tuwunel': ('/etc/grafana/dashboards/node-exporter-full.json' if prometheus_node_exporter_enabled else ''), }[matrix_homeserver_implementation] }} @@ -5618,6 +5622,7 @@ matrix_registration_shared_secret: |- 'dendrite': matrix_dendrite_client_api_registration_shared_secret | default (''), 'conduit': '', 'continuwuity': '', + 'tuwunel': '', }[matrix_homeserver_implementation] }} @@ -5843,6 +5848,67 @@ matrix_continuwuity_systemd_wanted_services_list_auto: | ###################################################################### +###################################################################### +# +# matrix-tuwunel +# +###################################################################### + +matrix_tuwunel_enabled: "{{ matrix_homeserver_implementation == 'tuwunel' }}" + +matrix_tuwunel_hostname: "{{ matrix_server_fqn_matrix }}" + +matrix_tuwunel_config_allow_federation: "{{ matrix_homeserver_federation_enabled }}" + +matrix_tuwunel_config_well_known_client: "{{ matrix_homeserver_url if matrix_playbook_ssl_enabled else '' }}" + +matrix_tuwunel_container_image_registry_prefix_upstream: "{{ matrix_container_global_registry_prefix_override if matrix_container_global_registry_prefix_override else matrix_tuwunel_container_image_registry_prefix_upstream_default }}" + +matrix_tuwunel_container_network: "{{ matrix_homeserver_container_network }}" + +matrix_tuwunel_container_additional_networks_auto: | + {{ + ( + ([matrix_playbook_reverse_proxyable_services_additional_network] if matrix_tuwunel_container_labels_traefik_enabled and matrix_playbook_reverse_proxyable_services_additional_network else []) + ) | unique + }} + +matrix_tuwunel_container_labels_traefik_enabled: "{{ matrix_playbook_reverse_proxy_type in ['playbook-managed-traefik', 'other-traefik-container'] and not matrix_synapse_workers_enabled }}" +matrix_tuwunel_container_labels_traefik_docker_network: "{{ matrix_playbook_reverse_proxyable_services_additional_network }}" +matrix_tuwunel_container_labels_traefik_entrypoints: "{{ traefik_entrypoint_primary }}" +matrix_tuwunel_container_labels_traefik_tls_certResolver: "{{ traefik_certResolver_primary }}" + +matrix_tuwunel_container_labels_public_client_root_redirection_enabled: "{{ matrix_tuwunel_container_labels_public_client_root_redirection_url != '' }}" +matrix_tuwunel_container_labels_public_client_root_redirection_url: "{{ (('https://' if matrix_playbook_ssl_enabled else 'http://') + matrix_server_fqn_element) if matrix_client_element_enabled else '' }}" + +matrix_tuwunel_container_labels_public_federation_api_traefik_hostname: "{{ matrix_server_fqn_matrix_federation }}" +matrix_tuwunel_container_labels_public_federation_api_traefik_entrypoints: "{{ matrix_federation_traefik_entrypoint_name }}" +matrix_tuwunel_container_labels_public_federation_api_traefik_tls: "{{ matrix_federation_traefik_entrypoint_tls }}" + +matrix_tuwunel_container_labels_internal_client_api_enabled: "{{ matrix_playbook_internal_matrix_client_api_traefik_entrypoint_enabled }}" +matrix_tuwunel_container_labels_internal_client_api_traefik_entrypoints: "{{ matrix_playbook_internal_matrix_client_api_traefik_entrypoint_name }}" + +matrix_tuwunel_config_well_known_livekit_url: "{{ matrix_livekit_jwt_service_public_url if matrix_livekit_jwt_service_enabled else '' }}" + +matrix_tuwunel_config_turn_uris: "{{ coturn_turn_uris if coturn_enabled else [] }}" +matrix_tuwunel_config_turn_secret: "{{ coturn_turn_static_auth_secret if (coturn_enabled and coturn_authentication_method == 'auth-secret') else '' }}" +matrix_tuwunel_config_turn_username: "{{ coturn_lt_cred_mech_username if (coturn_enabled and coturn_authentication_method == 'lt-cred-mech') else '' }}" +matrix_tuwunel_config_turn_password: "{{ coturn_lt_cred_mech_password if (coturn_enabled and coturn_authentication_method == 'lt-cred-mech') else '' }}" + +matrix_tuwunel_self_check_validate_certificates: "{{ matrix_playbook_ssl_enabled }}" + +matrix_tuwunel_systemd_wanted_services_list_auto: | + {{ + ([coturn_identifier ~ '.service'] if coturn_enabled else []) + }} + +###################################################################### +# +# /matrix-tuwunel +# +###################################################################### + + ###################################################################### # # matrix-user-creator diff --git a/roles/custom/matrix-base/defaults/main.yml b/roles/custom/matrix-base/defaults/main.yml index 2cd854111..c89d7ea84 100644 --- a/roles/custom/matrix-base/defaults/main.yml +++ b/roles/custom/matrix-base/defaults/main.yml @@ -84,7 +84,7 @@ matrix_monitoring_container_network: matrix-monitoring matrix_homeserver_enabled: true # This will contain the homeserver implementation that is in use. -# Valid values: synapse, dendrite, conduit, continuwuity +# Valid values: synapse, dendrite, conduit, continuwuity, tuwunel # # By default, we use Synapse, because it's the only full-featured Matrix server at the moment. # diff --git a/roles/custom/matrix-base/tasks/validate_config.yml b/roles/custom/matrix-base/tasks/validate_config.yml index 237bcec00..45df63ddc 100644 --- a/roles/custom/matrix-base/tasks/validate_config.yml +++ b/roles/custom/matrix-base/tasks/validate_config.yml @@ -13,7 +13,7 @@ - name: Fail if invalid homeserver implementation ansible.builtin.fail: msg: "You need to set a valid homeserver implementation in `matrix_homeserver_implementation`" - when: "matrix_homeserver_implementation not in ['synapse', 'dendrite', 'conduit', 'continuwuity']" + when: "matrix_homeserver_implementation not in ['synapse', 'dendrite', 'conduit', 'continuwuity', 'tuwunel']" - name: (Deprecation) Catch and report renamed settings ansible.builtin.fail: diff --git a/roles/custom/matrix-tuwunel/defaults/main.yml b/roles/custom/matrix-tuwunel/defaults/main.yml new file mode 100644 index 000000000..1a855f45d --- /dev/null +++ b/roles/custom/matrix-tuwunel/defaults/main.yml @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- +# Tuwunel is a Matrix homeserver, the official successor to conduwuit. +# Project source code URL: https://github.com/matrix-construct/tuwunel +# See: https://matrix-construct.github.io/tuwunel/ + +matrix_tuwunel_enabled: true + +matrix_tuwunel_hostname: '' + +# renovate: datasource=docker depName=ghcr.io/matrix-construct/tuwunel +matrix_tuwunel_version: latest + +matrix_tuwunel_container_image: "{{ matrix_tuwunel_container_image_registry_prefix }}matrix-construct/tuwunel:{{ matrix_tuwunel_container_image_tag }}" +matrix_tuwunel_container_image_tag: "{{ matrix_tuwunel_version }}" +matrix_tuwunel_container_image_registry_prefix: "{{ matrix_tuwunel_container_image_registry_prefix_upstream }}" +matrix_tuwunel_container_image_registry_prefix_upstream: "{{ matrix_tuwunel_container_image_registry_prefix_upstream_default }}" +matrix_tuwunel_container_image_registry_prefix_upstream_default: ghcr.io/ + +matrix_tuwunel_base_path: "{{ matrix_base_data_path }}/tuwunel" +matrix_tuwunel_config_path: "{{ matrix_tuwunel_base_path }}/config" +matrix_tuwunel_data_path: "{{ matrix_tuwunel_base_path }}/data" + +matrix_tuwunel_config_port_number: 6167 + +matrix_tuwunel_tmp_directory_size_mb: 500 + +# List of systemd services that matrix-tuwunel.service depends on +matrix_tuwunel_systemd_required_services_list: "{{ matrix_tuwunel_systemd_required_services_list_default + matrix_tuwunel_systemd_required_services_list_auto + matrix_tuwunel_systemd_required_services_list_custom }}" +matrix_tuwunel_systemd_required_services_list_default: "{{ [devture_systemd_docker_base_docker_service_name] if devture_systemd_docker_base_docker_service_name else [] }}" +matrix_tuwunel_systemd_required_services_list_auto: [] +matrix_tuwunel_systemd_required_services_list_custom: [] + +# List of systemd services that matrix-tuwunel.service wants +matrix_tuwunel_systemd_wanted_services_list: "{{ matrix_tuwunel_systemd_wanted_services_list_default + matrix_tuwunel_systemd_wanted_services_list_auto + matrix_tuwunel_systemd_wanted_services_list_custom }}" +matrix_tuwunel_systemd_wanted_services_list_default: [] +matrix_tuwunel_systemd_wanted_services_list_auto: [] +matrix_tuwunel_systemd_wanted_services_list_custom: [] + +# Controls how long to sleep for after starting the matrix-tuwunel container, +# so that subsequent services that depend on it can start after the homeserver +# is fully up. +# +# Set to 0 to remove the delay. +matrix_tuwunel_systemd_service_post_start_delay_seconds: 3 + +# The base container network. It will be auto-created by this role if it doesn't exist already. +matrix_tuwunel_container_network: "" + +# A list of additional container networks that the container would be connected to. +# The role does not create these networks, so make sure they already exist. +# Use this to expose this container to another reverse proxy, which runs in a different container network. +matrix_tuwunel_container_additional_networks: "{{ matrix_tuwunel_container_additional_networks_auto + matrix_tuwunel_container_additional_networks_custom }}" +matrix_tuwunel_container_additional_networks_auto: [] +matrix_tuwunel_container_additional_networks_custom: [] + +# matrix_tuwunel_container_labels_traefik_enabled controls whether labels to assist a Traefik reverse-proxy will be attached to the container. +# See `../templates/labels.j2` for details. +# +# To inject your own other container labels, see `matrix_tuwunel_container_labels_additional_labels`. +matrix_tuwunel_container_labels_traefik_enabled: true +matrix_tuwunel_container_labels_traefik_docker_network: "{{ matrix_tuwunel_container_network }}" +matrix_tuwunel_container_labels_traefik_entrypoints: web-secure +matrix_tuwunel_container_labels_traefik_tls_certResolver: default # noqa var-naming + +# Controls whether labels will be added for handling the root (/) path on a public Traefik entrypoint. +matrix_tuwunel_container_labels_public_client_root_enabled: true +matrix_tuwunel_container_labels_public_client_root_traefik_hostname: "{{ matrix_tuwunel_hostname }}" +matrix_tuwunel_container_labels_public_client_root_traefik_rule: "Host(`{{ matrix_tuwunel_container_labels_public_client_root_traefik_hostname }}`) && Path(`/`)" +matrix_tuwunel_container_labels_public_client_root_traefik_priority: 0 +matrix_tuwunel_container_labels_public_client_root_traefik_entrypoints: "{{ matrix_tuwunel_container_labels_traefik_entrypoints }}" +matrix_tuwunel_container_labels_public_client_root_traefik_tls: "{{ matrix_tuwunel_container_labels_public_client_root_traefik_entrypoints != 'web' }}" +matrix_tuwunel_container_labels_public_client_root_traefik_tls_certResolver: "{{ matrix_tuwunel_container_labels_traefik_tls_certResolver }}" # noqa var-naming +matrix_tuwunel_container_labels_public_client_root_redirection_enabled: false +matrix_tuwunel_container_labels_public_client_root_redirection_url: "" + +# Controls whether labels will be added that expose the Client-Server API on a public Traefik entrypoint. +matrix_tuwunel_container_labels_public_client_api_enabled: true +matrix_tuwunel_container_labels_public_client_api_traefik_hostname: "{{ matrix_tuwunel_hostname }}" +matrix_tuwunel_container_labels_public_client_api_traefik_path_prefix: /_matrix +matrix_tuwunel_container_labels_public_client_api_traefik_rule: "Host(`{{ matrix_tuwunel_container_labels_public_client_api_traefik_hostname }}`) && PathPrefix(`{{ matrix_tuwunel_container_labels_public_client_api_traefik_path_prefix }}`)" +matrix_tuwunel_container_labels_public_client_api_traefik_priority: 0 +matrix_tuwunel_container_labels_public_client_api_traefik_entrypoints: "{{ matrix_tuwunel_container_labels_traefik_entrypoints }}" +matrix_tuwunel_container_labels_public_client_api_traefik_tls: "{{ matrix_tuwunel_container_labels_public_client_api_traefik_entrypoints != 'web' }}" +matrix_tuwunel_container_labels_public_client_api_traefik_tls_certResolver: "{{ matrix_tuwunel_container_labels_traefik_tls_certResolver }}" # noqa var-naming + +# Controls whether labels will be added that expose the Client-Server API on the internal Traefik entrypoint. +matrix_tuwunel_container_labels_internal_client_api_enabled: false +matrix_tuwunel_container_labels_internal_client_api_traefik_path_prefix: "{{ matrix_tuwunel_container_labels_public_client_api_traefik_path_prefix }}" +matrix_tuwunel_container_labels_internal_client_api_traefik_rule: "PathPrefix(`{{ matrix_tuwunel_container_labels_internal_client_api_traefik_path_prefix }}`)" +matrix_tuwunel_container_labels_internal_client_api_traefik_priority: "{{ matrix_tuwunel_container_labels_public_client_api_traefik_priority }}" +matrix_tuwunel_container_labels_internal_client_api_traefik_entrypoints: "" + +# Controls whether labels will be added that expose the Server-Server (Federation) API on a public Traefik entrypoint. +matrix_tuwunel_container_labels_public_federation_api_enabled: "{{ matrix_tuwunel_config_allow_federation }}" +matrix_tuwunel_container_labels_public_federation_api_traefik_hostname: "{{ matrix_tuwunel_hostname }}" +matrix_tuwunel_container_labels_public_federation_api_traefik_path_prefix: /_matrix +matrix_tuwunel_container_labels_public_federation_api_traefik_rule: "Host(`{{ matrix_tuwunel_container_labels_public_federation_api_traefik_hostname }}`) && PathPrefix(`{{ matrix_tuwunel_container_labels_public_federation_api_traefik_path_prefix }}`)" +matrix_tuwunel_container_labels_public_federation_api_traefik_priority: 0 +matrix_tuwunel_container_labels_public_federation_api_traefik_entrypoints: '' +# TLS is force-enabled because the spec (https://spec.matrix.org/latest/server-server-api/#tls) requires the federation API use HTTPS. +matrix_tuwunel_container_labels_public_federation_api_traefik_tls: true +matrix_tuwunel_container_labels_public_federation_api_traefik_tls_certResolver: "{{ matrix_tuwunel_container_labels_traefik_tls_certResolver }}" # noqa var-naming + +# Additional Docker container labels (multiline string) appended verbatim to the label file. +# See `../templates/labels.j2`. +matrix_tuwunel_container_labels_additional_labels: '' + +# Extra arguments for the Docker container +matrix_tuwunel_container_extra_arguments: [] + +# Specifies which template files to use when configuring tuwunel. +# To override the rendered config wholesale, copy the template into your inventory and point this at it: +# matrix_tuwunel_template_tuwunel_config: "{{ playbook_dir }}/inventory/host_vars/matrix.example.com/tuwunel.toml.j2" +matrix_tuwunel_template_tuwunel_config: "{{ role_path }}/templates/tuwunel.toml.j2" + +# The pretty server name used as a suffix on user/room IDs. Cannot be changed after first start without a database wipe. +matrix_tuwunel_config_server_name: "{{ matrix_domain }}" + +# Max size for uploads, in bytes +matrix_tuwunel_config_max_request_size: 20000000 + +# Enables open registration. If false, no users can register on this server. +matrix_tuwunel_config_allow_registration: false + +# When registration is enabled, set a strong token to protect the endpoint from abuse. +# Generate one with e.g. `pwgen -s 64 1`. If left empty AND `allow_registration` is true, +# you must explicitly opt in via the open-registration acknowledgement variable below. +matrix_tuwunel_config_registration_token: '' + +# Acknowledgement required to allow registration with no token. +# Maps to tuwunel's `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`. +matrix_tuwunel_config_yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: false + +# Suffix appended to new-user displaynames upon registration. Empty disables it. +# Upstream defaults to a flag glyph; we keep MDAD homeserver behavior consistent and leave this empty. +matrix_tuwunel_config_new_user_displayname_suffix: "" + +# Emergency password to grant access to the admin user when locked out. Empty disables. +matrix_tuwunel_config_emergency_password: '' + +# Trusted notary servers used for key verification. +matrix_tuwunel_config_trusted_servers: + - "matrix.org" + +# Logging directives in `tracing-subscriber` env-filter syntax. +matrix_tuwunel_config_log: "info,state_res=warn" + +# TURN integration +matrix_tuwunel_config_turn_uris: [] +matrix_tuwunel_config_turn_secret: '' +matrix_tuwunel_config_turn_username: '' +matrix_tuwunel_config_turn_password: '' + +# Self-check toggles +matrix_tuwunel_self_check_validate_certificates: true + +# Encryption / room creation policy +matrix_tuwunel_config_allow_encryption: true +matrix_tuwunel_config_allow_room_creation: true + +# Default room version newly created rooms will use. +matrix_tuwunel_config_default_room_version: '12' + +# Rooms newly registered users will be auto-joined to. +# Must be rooms this server has joined at least once and that are public. +matrix_tuwunel_config_auto_join_rooms: [] + +# (De)federation toggles +matrix_tuwunel_config_allow_federation: true +matrix_tuwunel_config_allowed_remote_server_names: [] +matrix_tuwunel_config_forbidden_remote_server_names: [] +matrix_tuwunel_config_forbidden_remote_room_directory_server_names: [] +matrix_tuwunel_config_prevent_media_downloads_from: [] + +# Outgoing presence is heavy on CPU and network and almost no clients use it. Off by default. +matrix_tuwunel_config_allow_outgoing_presence: false + +# URL preview gating +matrix_tuwunel_config_url_preview_domain_contains_allowlist: [] +matrix_tuwunel_config_url_preview_domain_explicit_allowlist: [] +matrix_tuwunel_config_url_preview_check_root_domain: false + +# Well-known overrides +# Maps to `[global.well_known] client = "..."` and `server = "host:port"`. +matrix_tuwunel_config_well_known_client: '' +matrix_tuwunel_config_well_known_server: '' +matrix_tuwunel_config_well_known_support_page: '' +matrix_tuwunel_config_well_known_support_email: '' +matrix_tuwunel_config_well_known_support_mxid: '' + +# MatrixRTC foci served via /_matrix/client/v1/rtc/transports (MSC4143) +matrix_tuwunel_config_well_known_livekit_url: '' + +# RocksDB tuning. Empty values let tuwunel auto-pick. +matrix_tuwunel_config_rocksdb_compression_algo: 'zstd' # one of: zstd, lz4, bz2, none +matrix_tuwunel_config_rocksdb_compression_level: '' +matrix_tuwunel_config_rocksdb_bottommost_compression_level: '' +matrix_tuwunel_config_rocksdb_direct_io: false +matrix_tuwunel_config_rocksdb_parallelism_threads: 0 +matrix_tuwunel_config_rocksdb_max_log_file_size: '' +matrix_tuwunel_config_rocksdb_log_time_to_roll: '' +matrix_tuwunel_config_database_backup_path: '' +matrix_tuwunel_config_database_backups_to_keep: 1 + +# Cache sizing. Empty values let tuwunel auto-pick (scaled by CPU count). +matrix_tuwunel_config_cache_capacity_modifier: '' +matrix_tuwunel_config_db_cache_capacity_mb: '' +matrix_tuwunel_config_db_write_buffer_capacity_mb: '' + +# Admin room +matrix_tuwunel_config_create_admin_room: true +matrix_tuwunel_config_federate_admin_room: false +matrix_tuwunel_config_grant_admin_to_first_user: true + +# Sentry crash/error reporting (off by default) +matrix_tuwunel_config_sentry_enabled: false +matrix_tuwunel_config_sentry_endpoint: '' +matrix_tuwunel_config_sentry_send_server_name: false +matrix_tuwunel_config_sentry_traces_sample_rate: 0.15 + +# Blurhashing for image previews +matrix_tuwunel_config_blurhashing_enabled: true +matrix_tuwunel_config_blurhashing_components_x: 4 +matrix_tuwunel_config_blurhashing_components_y: 3 +matrix_tuwunel_config_blurhashing_max_raw_size: 33554432 + +# Native TLS (use only when reverse-proxying is not desired) +matrix_tuwunel_config_tls_certs: '' +matrix_tuwunel_config_tls_key: '' +matrix_tuwunel_config_tls_dual_protocol: false + +# LDAP authentication ([global.ldap] in tuwunel.toml). +# See: https://matrix-construct.github.io/tuwunel/authentication/providers.html +matrix_tuwunel_config_ldap_enabled: false +matrix_tuwunel_config_ldap_uri: '' +matrix_tuwunel_config_ldap_base_dn: '' +matrix_tuwunel_config_ldap_bind_dn: '' +matrix_tuwunel_config_ldap_bind_password_file: '' +matrix_tuwunel_config_ldap_filter: '(objectClass=*)' +matrix_tuwunel_config_ldap_uid_attribute: 'uid' +matrix_tuwunel_config_ldap_name_attribute: 'givenName' +matrix_tuwunel_config_ldap_admin_base_dn: '' +matrix_tuwunel_config_ldap_admin_filter: '' + +# JWT authentication ([global.jwt] in tuwunel.toml). +matrix_tuwunel_config_jwt_enabled: false +matrix_tuwunel_config_jwt_key: '' +matrix_tuwunel_config_jwt_format: 'HMAC' # one of: HMAC, B64HMAC, ECDSA, EDDSA +matrix_tuwunel_config_jwt_algorithm: 'HS256' +matrix_tuwunel_config_jwt_register_user: true +matrix_tuwunel_config_jwt_audience: [] +matrix_tuwunel_config_jwt_issuer: [] +matrix_tuwunel_config_jwt_require_exp: false +matrix_tuwunel_config_jwt_require_nbf: false +matrix_tuwunel_config_jwt_validate_exp: true +matrix_tuwunel_config_jwt_validate_nbf: true + +# OAuth2/OIDC identity providers. +# +# Each entry becomes a `[[global.identity_provider]]` block. Only fields you set are emitted; +# tuwunel applies brand-aware defaults for known providers (Google, GitHub, Keycloak, MAS, etc). +# +# Example: +# matrix_tuwunel_config_identity_providers: +# - brand: keycloak +# client_id: matrix +# client_secret: '...' +# issuer_url: https://sso.example.com/realms/matrix +# callback_url: https://matrix.example.com/_matrix/client/unstable/login/sso/callback/matrix +# trusted: true +# - brand: github +# client_id: '...' +# client_secret: '...' +# +# See: https://matrix-construct.github.io/tuwunel/authentication/providers.html +matrix_tuwunel_config_identity_providers: [] + +# Media storage providers. +# +# Each entry maps an ID to a backend. `kind` is `local` or `s3`; remaining keys map directly +# to fields under `[global.storage_provider..]`. +# +# Examples: +# matrix_tuwunel_config_storage_providers: +# - id: primary +# kind: local +# base_path: /var/lib/tuwunel/media +# - id: archive +# kind: s3 +# url: s3://my-bucket/media +# region: us-east-1 +# key: AKIA... +# secret: '...' +# +# See: https://matrix-construct.github.io/tuwunel/media/storage.html +matrix_tuwunel_config_storage_providers: [] + +# Additional environment variables to pass to the container, one per line. +# Environment variables override the rendered config file. +# +# Example: +# matrix_tuwunel_environment_variables_extension: | +# TUWUNEL_REQUEST_TIMEOUT=60 +# TUWUNEL_DNS_CACHE_SIZE=131072 +matrix_tuwunel_environment_variables_extension: '' + +# matrix_tuwunel_restart_necessary controls whether the service will be restarted (when true) +# or merely started (when false) by the systemd service-manager role when conditional restart +# is enabled. Computed during installation based on whether config / unit / image changed. +matrix_tuwunel_restart_necessary: false diff --git a/roles/custom/matrix-tuwunel/tasks/install.yml b/roles/custom/matrix-tuwunel/tasks/install.yml new file mode 100644 index 000000000..d3ac3eceb --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/install.yml @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Ensure tuwunel config path exists + ansible.builtin.file: + path: "{{ matrix_tuwunel_config_path }}" + state: directory + mode: '0750' + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + +- name: Ensure tuwunel data path exists + ansible.builtin.file: + path: "{{ matrix_tuwunel_data_path }}" + state: directory + mode: '0770' + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + +- name: Ensure tuwunel configuration installed + ansible.builtin.template: + src: "{{ matrix_tuwunel_template_tuwunel_config }}" + dest: "{{ matrix_tuwunel_config_path }}/tuwunel.toml" + mode: '0644' + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + register: matrix_tuwunel_config_result + +- name: Ensure tuwunel support files installed + ansible.builtin.template: + src: "{{ role_path }}/templates/{{ item }}.j2" + dest: "{{ matrix_tuwunel_base_path }}/{{ item }}" + mode: '0640' + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + with_items: + - labels + - env + register: matrix_tuwunel_support_files_result + +- name: Ensure tuwunel container network is created + community.general.docker_network: + enable_ipv6: "{{ devture_systemd_docker_base_ipv6_enabled }}" + name: "{{ matrix_tuwunel_container_network }}" + driver: bridge + driver_options: "{{ devture_systemd_docker_base_container_networks_driver_options }}" + +- name: Ensure tuwunel container image is pulled + community.docker.docker_image_pull: + name: "{{ matrix_tuwunel_container_image }}" + pull: always + register: matrix_tuwunel_container_image_pull_result + retries: "{{ devture_playbook_help_container_retries_count }}" + delay: "{{ devture_playbook_help_container_retries_delay }}" + until: matrix_tuwunel_container_image_pull_result is not failed + +- name: Ensure matrix-tuwunel.service installed + ansible.builtin.template: + src: "{{ role_path }}/templates/systemd/matrix-tuwunel.service.j2" + dest: "{{ devture_systemd_docker_base_systemd_path }}/matrix-tuwunel.service" + mode: '0644' + register: matrix_tuwunel_systemd_service_result + +- name: Determine whether tuwunel needs a restart + ansible.builtin.set_fact: + matrix_tuwunel_restart_necessary: >- + {{ + matrix_tuwunel_config_result.changed | default(false) + or matrix_tuwunel_support_files_result.changed | default(false) + or matrix_tuwunel_systemd_service_result.changed | default(false) + or matrix_tuwunel_container_image_pull_result.changed | default(false) + }} diff --git a/roles/custom/matrix-tuwunel/tasks/main.yml b/roles/custom/matrix-tuwunel/tasks/main.yml new file mode 100644 index 000000000..edb309c93 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/main.yml @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- tags: + - setup-all + - setup-tuwunel + - install-all + - install-tuwunel + block: + - when: matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/validate_config.yml" + + - when: matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/install.yml" + +- tags: + - setup-all + - setup-tuwunel + block: + - when: not matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/uninstall.yml" + +- tags: + - self-check + block: + - when: matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/self_check_client_api.yml" + + - when: matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/self_check_federation_api.yml" + +- tags: + - tuwunel-migrate-from-conduwuit + block: + - when: matrix_tuwunel_enabled | bool + ansible.builtin.include_tasks: "{{ role_path }}/tasks/migrate_from_conduwuit.yml" diff --git a/roles/custom/matrix-tuwunel/tasks/migrate_from_conduwuit.yml b/roles/custom/matrix-tuwunel/tasks/migrate_from_conduwuit.yml new file mode 100644 index 000000000..ecaa1a7a8 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/migrate_from_conduwuit.yml @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +# Migrates from the conduwuit server implementation (`/matrix/conduwuit`) to tuwunel (`/matrix/tuwunel`). +# Tuwunel is the official successor to conduwuit and reads conduwuit's RocksDB layout directly. +# We back up the freshly generated tuwunel directory, copy conduwuit's data into it, +# rename the config file, restore tuwunel's labels file, and start the new service. + +- name: Check existence of conduwuit directory + ansible.builtin.stat: + path: "{{ matrix_base_data_path }}/conduwuit" + register: matrix_removed_conduwuit_directory_stat + +- name: Check existence of tuwunel directory + ansible.builtin.stat: + path: "{{ matrix_base_data_path }}/tuwunel" + register: matrix_tuwunel_directory_stat + +- when: > + matrix_removed_conduwuit_directory_stat.stat.exists | bool and + matrix_tuwunel_directory_stat.stat.exists | bool + block: + - name: Ensure matrix-tuwunel.service systemd service is stopped + ansible.builtin.systemd: + name: matrix-tuwunel + state: stopped + enabled: false + daemon_reload: true + + - name: Ensure tuwunel directory is backed up + ansible.builtin.command: + cmd: "mv {{ matrix_base_data_path }}/tuwunel {{ matrix_base_data_path }}/tuwunel_old" + creates: "{{ matrix_base_data_path }}/tuwunel_old" + removes: "{{ matrix_base_data_path }}/tuwunel" + + - name: Ensure conduwuit directory contents are copied to tuwunel + ansible.builtin.copy: + src: "{{ matrix_base_data_path }}/conduwuit/" + dest: "{{ matrix_base_data_path }}/tuwunel" + remote_src: true + mode: preserve + + - name: Ensure conduwuit.toml file is renamed + ansible.builtin.command: + cmd: "mv {{ matrix_base_data_path }}/tuwunel/config/conduwuit.toml {{ matrix_base_data_path }}/tuwunel/config/tuwunel.toml" + removes: "{{ matrix_base_data_path }}/tuwunel/config/conduwuit.toml" + + - name: Ensure tuwunel labels are restored + ansible.builtin.copy: + src: "{{ matrix_base_data_path }}/tuwunel_old/labels" + dest: "{{ matrix_base_data_path }}/tuwunel/labels" + remote_src: true + force: true + mode: preserve + + - name: Ensure directories ownership is set + block: + - name: Set tuwunel ownership + ansible.builtin.file: + path: "{{ matrix_base_data_path }}/tuwunel" + state: directory + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + recurse: true + + - name: Set tuwunel_old ownership + ansible.builtin.file: + path: "{{ matrix_base_data_path }}/tuwunel_old" + state: directory + owner: "{{ matrix_user_name }}" + group: "{{ matrix_group_name }}" + recurse: true + + - name: Ensure matrix-tuwunel.service systemd service is started + ansible.builtin.systemd: + name: matrix-tuwunel + state: started + enabled: true + daemon_reload: true diff --git a/roles/custom/matrix-tuwunel/tasks/self_check_client_api.yml b/roles/custom/matrix-tuwunel/tasks/self_check_client_api.yml new file mode 100644 index 000000000..7199958e6 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/self_check_client_api.yml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Check Matrix Client API + ansible.builtin.uri: + url: "{{ matrix_tuwunel_client_api_url_endpoint_public }}" + follow_redirects: none + validate_certs: "{{ matrix_tuwunel_self_check_validate_certificates }}" + register: result_matrix_tuwunel_client_api + ignore_errors: true + check_mode: false + when: matrix_tuwunel_enabled | bool + delegate_to: 127.0.0.1 + become: false + +- name: Fail if Matrix Client API not working + ansible.builtin.fail: + msg: "Failed checking Matrix Client API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_tuwunel_client_api_url_endpoint_public }}`). Is tuwunel running? Is port 443 open in your firewall? Full error: {{ result_matrix_tuwunel_client_api }}" + when: "matrix_tuwunel_enabled | bool and (result_matrix_tuwunel_client_api.failed or 'json' not in result_matrix_tuwunel_client_api)" + +- name: Report working Matrix Client API + ansible.builtin.debug: + msg: "The Matrix Client API at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_tuwunel_client_api_url_endpoint_public }}`) is working" + when: matrix_tuwunel_enabled | bool diff --git a/roles/custom/matrix-tuwunel/tasks/self_check_federation_api.yml b/roles/custom/matrix-tuwunel/tasks/self_check_federation_api.yml new file mode 100644 index 000000000..1d4c2fe37 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/self_check_federation_api.yml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Check Matrix Federation API + ansible.builtin.uri: + url: "{{ matrix_tuwunel_federation_api_url_endpoint_public }}" + follow_redirects: none + validate_certs: "{{ matrix_tuwunel_self_check_validate_certificates }}" + register: result_matrix_tuwunel_federation_api + ignore_errors: true + check_mode: false + when: matrix_tuwunel_enabled | bool + delegate_to: 127.0.0.1 + become: false + +- name: Fail if Matrix Federation API not working + ansible.builtin.fail: + msg: "Failed checking Matrix Federation API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_tuwunel_federation_api_url_endpoint_public }}`). Is tuwunel running? Is port {{ matrix_federation_public_port }} open in your firewall? Full error: {{ result_matrix_tuwunel_federation_api }}" + when: "matrix_tuwunel_enabled | bool and matrix_tuwunel_config_allow_federation | bool and (result_matrix_tuwunel_federation_api.failed or 'json' not in result_matrix_tuwunel_federation_api)" + +- name: Fail if Matrix Federation API unexpectedly enabled + ansible.builtin.fail: + msg: "Matrix Federation API is up at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_tuwunel_federation_api_url_endpoint_public }}`) despite being disabled." + when: "matrix_tuwunel_enabled | bool and not matrix_tuwunel_config_allow_federation | bool and not result_matrix_tuwunel_federation_api.failed" + +- name: Report working Matrix Federation API + ansible.builtin.debug: + msg: "The Matrix Federation API at `{{ matrix_server_fqn_matrix }}` (checked endpoint: `{{ matrix_tuwunel_federation_api_url_endpoint_public }}`) is working" + when: "matrix_tuwunel_enabled | bool and matrix_tuwunel_config_allow_federation | bool" diff --git a/roles/custom/matrix-tuwunel/tasks/setup_install.yml b/roles/custom/matrix-tuwunel/tasks/setup_install.yml new file mode 100644 index 000000000..5f0d848c0 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/setup_install.yml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- ansible.builtin.include_tasks: "{{ role_path }}/tasks/install.yml" diff --git a/roles/custom/matrix-tuwunel/tasks/setup_uninstall.yml b/roles/custom/matrix-tuwunel/tasks/setup_uninstall.yml new file mode 100644 index 000000000..159de8d6b --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/setup_uninstall.yml @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- ansible.builtin.include_tasks: "{{ role_path }}/tasks/uninstall.yml" diff --git a/roles/custom/matrix-tuwunel/tasks/uninstall.yml b/roles/custom/matrix-tuwunel/tasks/uninstall.yml new file mode 100644 index 000000000..1ef8ea3f7 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/uninstall.yml @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Check existence of matrix-tuwunel service + ansible.builtin.stat: + path: "{{ devture_systemd_docker_base_systemd_path }}/matrix-tuwunel.service" + register: matrix_tuwunel_service_stat + +- when: matrix_tuwunel_service_stat.stat.exists | bool + block: + - name: Ensure matrix-tuwunel is stopped + ansible.builtin.systemd: + name: matrix-tuwunel + state: stopped + daemon_reload: true + + - name: Ensure matrix-tuwunel.service doesn't exist + ansible.builtin.file: + path: "{{ devture_systemd_docker_base_systemd_path }}/matrix-tuwunel.service" + state: absent diff --git a/roles/custom/matrix-tuwunel/tasks/validate_config.yml b/roles/custom/matrix-tuwunel/tasks/validate_config.yml new file mode 100644 index 000000000..c8a41c9d6 --- /dev/null +++ b/roles/custom/matrix-tuwunel/tasks/validate_config.yml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +- name: Fail if required tuwunel settings not defined + ansible.builtin.fail: + msg: >- + You need to define a required configuration setting (`{{ item.name }}`). + when: "item.when | bool and lookup('vars', item.name, default='') | string | length == 0" + with_items: + - {'name': 'matrix_tuwunel_hostname', when: true} + - {'name': 'matrix_tuwunel_container_network', when: true} + - {'name': 'matrix_tuwunel_container_labels_internal_client_api_traefik_entrypoints', when: "{{ matrix_tuwunel_container_labels_internal_client_api_enabled }}"} + +- name: Fail if registration is enabled without a token or explicit acknowledgement + ansible.builtin.fail: + msg: >- + `matrix_tuwunel_config_allow_registration` is true, but neither + `matrix_tuwunel_config_registration_token` nor + `matrix_tuwunel_config_yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse` + is set. Set a registration token (recommended) or explicitly opt in to open registration. + when: >- + matrix_tuwunel_config_allow_registration | bool + and (matrix_tuwunel_config_registration_token | length == 0) + and not (matrix_tuwunel_config_yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse | bool) + +- name: Fail if a storage provider is missing required fields + ansible.builtin.fail: + msg: >- + Storage provider `{{ item.id | default('?') }}` is missing required fields. + Each entry must define both `id` and `kind` (one of: local, s3). + when: "(item.id | default('') | length == 0) or (item.kind | default('') not in ['local', 's3'])" + with_items: "{{ matrix_tuwunel_config_storage_providers }}" + +- name: Fail if an identity provider is missing required fields + ansible.builtin.fail: + msg: >- + Identity provider entry is missing both `client_id` and `brand`. + At minimum one of these is required for tuwunel to identify the provider. + when: "(item.client_id | default('') | length == 0) and (item.brand | default('') | length == 0)" + with_items: "{{ matrix_tuwunel_config_identity_providers }}" diff --git a/roles/custom/matrix-tuwunel/templates/env.j2 b/roles/custom/matrix-tuwunel/templates/env.j2 new file mode 100644 index 000000000..1ad68e77f --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/env.j2 @@ -0,0 +1 @@ +{{ matrix_tuwunel_environment_variables_extension }} diff --git a/roles/custom/matrix-tuwunel/templates/env.j2.license b/roles/custom/matrix-tuwunel/templates/env.j2.license new file mode 100644 index 000000000..71b91553b --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/env.j2.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2026 MDAD project contributors +SPDX-FileCopyrightText: 2026 Slavi Pantaleev + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/roles/custom/matrix-tuwunel/templates/labels.j2 b/roles/custom/matrix-tuwunel/templates/labels.j2 new file mode 100644 index 000000000..756ec3d57 --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/labels.j2 @@ -0,0 +1,141 @@ +{# +SPDX-FileCopyrightText: 2026 MDAD project contributors +SPDX-FileCopyrightText: 2026 Slavi Pantaleev + +SPDX-License-Identifier: AGPL-3.0-or-later +#} + +{% if matrix_tuwunel_container_labels_traefik_enabled %} +traefik.enable=true + +{% if matrix_tuwunel_container_labels_traefik_docker_network %} +traefik.docker.network={{ matrix_tuwunel_container_labels_traefik_docker_network }} +{% endif %} + +traefik.http.services.matrix-tuwunel.loadbalancer.server.port={{ matrix_tuwunel_config_port_number }} + + +{% if matrix_tuwunel_container_labels_public_client_root_enabled %} +############################################################ +# # +# Public Root path (/) # +# # +############################################################ + +{% set client_root_middlewares = [] %} + +{% if matrix_tuwunel_container_labels_public_client_root_redirection_enabled %} +{% set client_root_middlewares = client_root_middlewares + ['matrix-tuwunel-client-root-redirect'] %} +traefik.http.middlewares.matrix-tuwunel-client-root-redirect.redirectregex.regex=(.*) +traefik.http.middlewares.matrix-tuwunel-client-root-redirect.redirectregex.replacement={{ matrix_tuwunel_container_labels_public_client_root_redirection_url }} +{% endif %} + +traefik.http.routers.matrix-tuwunel-public-client-root.rule={{ matrix_tuwunel_container_labels_public_client_root_traefik_rule }} + +traefik.http.routers.matrix-tuwunel-public-client-root.middlewares={{ client_root_middlewares | join(',') }} + +{% if matrix_tuwunel_container_labels_public_client_root_traefik_priority | int > 0 %} +traefik.http.routers.matrix-tuwunel-public-client-root.priority={{ matrix_tuwunel_container_labels_public_client_root_traefik_priority }} +{% endif %} + +traefik.http.routers.matrix-tuwunel-public-client-root.service=matrix-tuwunel +traefik.http.routers.matrix-tuwunel-public-client-root.entrypoints={{ matrix_tuwunel_container_labels_public_client_root_traefik_entrypoints }} +traefik.http.routers.matrix-tuwunel-public-client-root.tls={{ matrix_tuwunel_container_labels_public_client_root_traefik_tls | to_json }} + +{% if matrix_tuwunel_container_labels_public_client_root_traefik_tls %} +traefik.http.routers.matrix-tuwunel-public-client-root.tls.certResolver={{ matrix_tuwunel_container_labels_public_client_root_traefik_tls_certResolver }} +{% endif %} + +############################################################ +# # +# /Public Root path (/) # +# # +############################################################ +{% endif %} + + +{% if matrix_tuwunel_container_labels_public_client_api_enabled %} +############################################################ +# # +# Public Client-API (/_matrix) # +# # +############################################################ + +traefik.http.routers.matrix-tuwunel-public-client-api.rule={{ matrix_tuwunel_container_labels_public_client_api_traefik_rule }} + +{% if matrix_tuwunel_container_labels_public_client_api_traefik_priority | int > 0 %} +traefik.http.routers.matrix-tuwunel-public-client-api.priority={{ matrix_tuwunel_container_labels_public_client_api_traefik_priority }} +{% endif %} + +traefik.http.routers.matrix-tuwunel-public-client-api.service=matrix-tuwunel +traefik.http.routers.matrix-tuwunel-public-client-api.entrypoints={{ matrix_tuwunel_container_labels_public_client_api_traefik_entrypoints }} + +traefik.http.routers.matrix-tuwunel-public-client-api.tls={{ matrix_tuwunel_container_labels_public_client_api_traefik_tls | to_json }} +{% if matrix_tuwunel_container_labels_public_client_api_traefik_tls %} +traefik.http.routers.matrix-tuwunel-public-client-api.tls.certResolver={{ matrix_tuwunel_container_labels_public_client_api_traefik_tls_certResolver }} +{% endif %} + +############################################################ +# # +# /Public Client-API (/_matrix) # +# # +############################################################ +{% endif %} + + +{% if matrix_tuwunel_container_labels_internal_client_api_enabled %} +############################################################ +# # +# Internal Client-API (/_matrix) # +# # +############################################################ + +traefik.http.routers.matrix-tuwunel-internal-client-api.rule={{ matrix_tuwunel_container_labels_internal_client_api_traefik_rule }} + +{% if matrix_tuwunel_container_labels_internal_client_api_traefik_priority | int > 0 %} +traefik.http.routers.matrix-tuwunel-internal-client-api.priority={{ matrix_tuwunel_container_labels_internal_client_api_traefik_priority }} +{% endif %} + +traefik.http.routers.matrix-tuwunel-internal-client-api.service=matrix-tuwunel +traefik.http.routers.matrix-tuwunel-internal-client-api.entrypoints={{ matrix_tuwunel_container_labels_internal_client_api_traefik_entrypoints }} + +############################################################ +# # +# /Internal Client-API (/_matrix) # +# # +############################################################ +{% endif %} + + +{% if matrix_tuwunel_container_labels_public_federation_api_enabled %} +############################################################ +# # +# Public Federation-API (/_matrix) # +# # +############################################################ + +traefik.http.routers.matrix-tuwunel-public-federation-api.rule={{ matrix_tuwunel_container_labels_public_federation_api_traefik_rule }} + +{% if matrix_tuwunel_container_labels_public_federation_api_traefik_priority | int > 0 %} +traefik.http.routers.matrix-tuwunel-public-federation-api.priority={{ matrix_tuwunel_container_labels_public_federation_api_traefik_priority }} +{% endif %} + +traefik.http.routers.matrix-tuwunel-public-federation-api.service=matrix-tuwunel +traefik.http.routers.matrix-tuwunel-public-federation-api.entrypoints={{ matrix_tuwunel_container_labels_public_federation_api_traefik_entrypoints }} + +traefik.http.routers.matrix-tuwunel-public-federation-api.tls={{ matrix_tuwunel_container_labels_public_federation_api_traefik_tls | to_json }} +{% if matrix_tuwunel_container_labels_public_federation_api_traefik_tls %} +traefik.http.routers.matrix-tuwunel-public-federation-api.tls.certResolver={{ matrix_tuwunel_container_labels_public_federation_api_traefik_tls_certResolver }} +{% endif %} + +############################################################ +# # +# /Public Federation-API (/_matrix) # +# # +############################################################ +{% endif %} + + +{% endif %} + +{{ matrix_tuwunel_container_labels_additional_labels }} diff --git a/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2 b/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2 new file mode 100644 index 000000000..6d1db04f2 --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2 @@ -0,0 +1,55 @@ +#jinja2: lstrip_blocks: True +[Unit] +Description=Tuwunel Matrix homeserver +{% for service in matrix_tuwunel_systemd_required_services_list %} +Requires={{ service }} +After={{ service }} +{% endfor %} +{% for service in matrix_tuwunel_systemd_wanted_services_list %} +Wants={{ service }} +{% endfor %} + +[Service] +Type=simple +Environment="HOME={{ devture_systemd_docker_base_systemd_unit_home_path }}" +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-tuwunel 2>/dev/null || true' +ExecStartPre=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-tuwunel 2>/dev/null || true' + +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} create \ + --rm \ + --name=matrix-tuwunel \ + --log-driver=none \ + --user={{ matrix_user_uid }}:{{ matrix_user_gid }} \ + --cap-drop=ALL \ + --read-only \ + --tmpfs=/tmp:rw,noexec,nosuid,size={{ matrix_tuwunel_tmp_directory_size_mb }}m \ + --network={{ matrix_tuwunel_container_network }} \ + --env-file={{ matrix_tuwunel_base_path }}/env \ + --env TUWUNEL_CONFIG=/etc/tuwunel/tuwunel.toml \ + --label-file={{ matrix_tuwunel_base_path }}/labels \ + --mount type=bind,src={{ matrix_tuwunel_data_path }},dst=/var/lib/tuwunel \ + --mount type=bind,src={{ matrix_tuwunel_config_path }},dst=/etc/tuwunel,ro \ + {% for arg in matrix_tuwunel_container_extra_arguments %} + {{ arg }} \ + {% endfor %} + {{ matrix_tuwunel_container_image }} + +{% for network in matrix_tuwunel_container_additional_networks %} +ExecStartPre={{ devture_systemd_docker_base_host_command_docker }} network connect {{ network }} matrix-tuwunel +{% endfor %} + +ExecStart={{ devture_systemd_docker_base_host_command_docker }} start --attach matrix-tuwunel + +{% if matrix_tuwunel_systemd_service_post_start_delay_seconds != 0 %} +ExecStartPost=-{{ matrix_host_command_sleep }} {{ matrix_tuwunel_systemd_service_post_start_delay_seconds }} +{% endif %} + +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} stop -t {{ devture_systemd_docker_base_container_stop_grace_time_seconds }} matrix-tuwunel 2>/dev/null || true' +ExecStop=-{{ devture_systemd_docker_base_host_command_sh }} -c '{{ devture_systemd_docker_base_host_command_docker }} rm matrix-tuwunel 2>/dev/null || true' +ExecReload={{ devture_systemd_docker_base_host_command_docker }} exec matrix-tuwunel /bin/sh -c 'kill -HUP 1' +Restart=always +RestartSec=30 +SyslogIdentifier=matrix-tuwunel + +[Install] +WantedBy=multi-user.target diff --git a/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2.license b/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2.license new file mode 100644 index 000000000..71b91553b --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2026 MDAD project contributors +SPDX-FileCopyrightText: 2026 Slavi Pantaleev + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2 b/roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2 new file mode 100644 index 000000000..364d3a5df --- /dev/null +++ b/roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2 @@ -0,0 +1,238 @@ +{# +SPDX-FileCopyrightText: 2026 MDAD project contributors +SPDX-FileCopyrightText: 2026 Slavi Pantaleev + +SPDX-License-Identifier: AGPL-3.0-or-later +#} +### Tuwunel configuration rendered by matrix-docker-ansible-deploy. +### +### This file only emits options exposed as Ansible variables. All other knobs +### keep tuwunel's upstream defaults. To override anything not surfaced here, +### use `matrix_tuwunel_environment_variables_extension` (env vars override TOML) +### or replace the template via `matrix_tuwunel_template_tuwunel_config`. +### +### Reference: https://matrix-construct.github.io/tuwunel/configuration.html + +[global] +server_name = {{ matrix_tuwunel_config_server_name | to_json }} +address = "0.0.0.0" +port = {{ matrix_tuwunel_config_port_number }} +database_path = "/var/lib/tuwunel" + +max_request_size = {{ matrix_tuwunel_config_max_request_size }} + +new_user_displayname_suffix = {{ matrix_tuwunel_config_new_user_displayname_suffix | to_json }} + +allow_registration = {{ matrix_tuwunel_config_allow_registration | to_json }} +{% if matrix_tuwunel_config_registration_token | length > 0 %} +registration_token = {{ matrix_tuwunel_config_registration_token | to_json }} +{% endif %} +{% if matrix_tuwunel_config_yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse | bool %} +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +{% endif %} + +{% if matrix_tuwunel_config_emergency_password | length > 0 %} +emergency_password = {{ matrix_tuwunel_config_emergency_password | to_json }} +{% endif %} + +allow_encryption = {{ matrix_tuwunel_config_allow_encryption | to_json }} +allow_room_creation = {{ matrix_tuwunel_config_allow_room_creation | to_json }} +default_room_version = {{ matrix_tuwunel_config_default_room_version | to_json }} +{% if matrix_tuwunel_config_auto_join_rooms | length > 0 %} +auto_join_rooms = {{ matrix_tuwunel_config_auto_join_rooms | to_json }} +{% endif %} + +allow_federation = {{ matrix_tuwunel_config_allow_federation | to_json }} +trusted_servers = {{ matrix_tuwunel_config_trusted_servers | to_json }} +{% if matrix_tuwunel_config_allowed_remote_server_names | length > 0 %} +allowed_remote_server_names_experimental = {{ matrix_tuwunel_config_allowed_remote_server_names | to_json }} +{% endif %} +{% if matrix_tuwunel_config_forbidden_remote_server_names | length > 0 %} +forbidden_remote_server_names = {{ matrix_tuwunel_config_forbidden_remote_server_names | to_json }} +{% endif %} +{% if matrix_tuwunel_config_forbidden_remote_room_directory_server_names | length > 0 %} +forbidden_remote_room_directory_server_names = {{ matrix_tuwunel_config_forbidden_remote_room_directory_server_names | to_json }} +{% endif %} +{% if matrix_tuwunel_config_prevent_media_downloads_from | length > 0 %} +prevent_media_downloads_from = {{ matrix_tuwunel_config_prevent_media_downloads_from | to_json }} +{% endif %} + +allow_outgoing_presence = {{ matrix_tuwunel_config_allow_outgoing_presence | to_json }} + +{% if matrix_tuwunel_config_url_preview_domain_contains_allowlist | length > 0 %} +url_preview_domain_contains_allowlist = {{ matrix_tuwunel_config_url_preview_domain_contains_allowlist | to_json }} +{% endif %} +{% if matrix_tuwunel_config_url_preview_domain_explicit_allowlist | length > 0 %} +url_preview_domain_explicit_allowlist = {{ matrix_tuwunel_config_url_preview_domain_explicit_allowlist | to_json }} +{% endif %} +url_preview_check_root_domain = {{ matrix_tuwunel_config_url_preview_check_root_domain | to_json }} + +create_admin_room = {{ matrix_tuwunel_config_create_admin_room | to_json }} +federate_admin_room = {{ matrix_tuwunel_config_federate_admin_room | to_json }} +grant_admin_to_first_user = {{ matrix_tuwunel_config_grant_admin_to_first_user | to_json }} + +log = {{ matrix_tuwunel_config_log | to_json }} + +{% if matrix_tuwunel_config_turn_uris | length > 0 %} +turn_uris = {{ matrix_tuwunel_config_turn_uris | to_json }} +{% endif %} +{% if matrix_tuwunel_config_turn_secret | length > 0 %} +turn_secret = {{ matrix_tuwunel_config_turn_secret | to_json }} +{% endif %} +{% if matrix_tuwunel_config_turn_username | length > 0 %} +turn_username = {{ matrix_tuwunel_config_turn_username | to_json }} +{% endif %} +{% if matrix_tuwunel_config_turn_password | length > 0 %} +turn_password = {{ matrix_tuwunel_config_turn_password | to_json }} +{% endif %} + +{% if matrix_tuwunel_config_rocksdb_compression_algo | length > 0 %} +rocksdb_compression_algo = {{ matrix_tuwunel_config_rocksdb_compression_algo | to_json }} +{% endif %} +{% if matrix_tuwunel_config_rocksdb_compression_level | string | length > 0 %} +rocksdb_compression_level = {{ matrix_tuwunel_config_rocksdb_compression_level }} +{% endif %} +{% if matrix_tuwunel_config_rocksdb_bottommost_compression_level | string | length > 0 %} +rocksdb_bottommost_compression_level = {{ matrix_tuwunel_config_rocksdb_bottommost_compression_level }} +{% endif %} +rocksdb_direct_io = {{ matrix_tuwunel_config_rocksdb_direct_io | to_json }} +{% if matrix_tuwunel_config_rocksdb_parallelism_threads | int > 0 %} +rocksdb_parallelism_threads = {{ matrix_tuwunel_config_rocksdb_parallelism_threads }} +{% endif %} +{% if matrix_tuwunel_config_rocksdb_max_log_file_size | string | length > 0 %} +rocksdb_max_log_file_size = {{ matrix_tuwunel_config_rocksdb_max_log_file_size }} +{% endif %} +{% if matrix_tuwunel_config_rocksdb_log_time_to_roll | string | length > 0 %} +rocksdb_log_time_to_roll = {{ matrix_tuwunel_config_rocksdb_log_time_to_roll }} +{% endif %} +{% if matrix_tuwunel_config_database_backup_path | length > 0 %} +database_backup_path = {{ matrix_tuwunel_config_database_backup_path | to_json }} +database_backups_to_keep = {{ matrix_tuwunel_config_database_backups_to_keep }} +{% endif %} + +{% if matrix_tuwunel_config_cache_capacity_modifier | string | length > 0 %} +cache_capacity_modifier = {{ matrix_tuwunel_config_cache_capacity_modifier }} +{% endif %} +{% if matrix_tuwunel_config_db_cache_capacity_mb | string | length > 0 %} +db_cache_capacity_mb = {{ matrix_tuwunel_config_db_cache_capacity_mb }} +{% endif %} +{% if matrix_tuwunel_config_db_write_buffer_capacity_mb | string | length > 0 %} +db_write_buffer_capacity_mb = {{ matrix_tuwunel_config_db_write_buffer_capacity_mb }} +{% endif %} + +{% if matrix_tuwunel_config_sentry_enabled | bool %} +sentry = true +{% if matrix_tuwunel_config_sentry_endpoint | length > 0 %} +sentry_endpoint = {{ matrix_tuwunel_config_sentry_endpoint | to_json }} +{% endif %} +sentry_send_server_name = {{ matrix_tuwunel_config_sentry_send_server_name | to_json }} +sentry_traces_sample_rate = {{ matrix_tuwunel_config_sentry_traces_sample_rate }} +{% endif %} + +{% if (matrix_tuwunel_config_tls_certs | length > 0) and (matrix_tuwunel_config_tls_key | length > 0) %} + +[global.tls] +certs = {{ matrix_tuwunel_config_tls_certs | to_json }} +key = {{ matrix_tuwunel_config_tls_key | to_json }} +dual_protocol = {{ matrix_tuwunel_config_tls_dual_protocol | to_json }} +{% endif %} + +{% set well_known_keys = [ + matrix_tuwunel_config_well_known_client, + matrix_tuwunel_config_well_known_server, + matrix_tuwunel_config_well_known_support_page, + matrix_tuwunel_config_well_known_support_email, + matrix_tuwunel_config_well_known_support_mxid, + matrix_tuwunel_config_well_known_livekit_url, +] %} +{% if well_known_keys | select | list | length > 0 %} + +[global.well_known] +{% if matrix_tuwunel_config_well_known_client | length > 0 %} +client = {{ matrix_tuwunel_config_well_known_client | to_json }} +{% endif %} +{% if matrix_tuwunel_config_well_known_server | length > 0 %} +server = {{ matrix_tuwunel_config_well_known_server | to_json }} +{% endif %} +{% if matrix_tuwunel_config_well_known_support_page | length > 0 %} +support_page = {{ matrix_tuwunel_config_well_known_support_page | to_json }} +{% endif %} +{% if matrix_tuwunel_config_well_known_support_email | length > 0 %} +support_email = {{ matrix_tuwunel_config_well_known_support_email | to_json }} +{% endif %} +{% if matrix_tuwunel_config_well_known_support_mxid | length > 0 %} +support_mxid = {{ matrix_tuwunel_config_well_known_support_mxid | to_json }} +{% endif %} +{% if matrix_tuwunel_config_well_known_livekit_url | length > 0 %} +livekit_url = {{ matrix_tuwunel_config_well_known_livekit_url | to_json }} +{% endif %} +{% endif %} + +{% if matrix_tuwunel_config_blurhashing_enabled | bool %} + +[global.blurhashing] +components_x = {{ matrix_tuwunel_config_blurhashing_components_x }} +components_y = {{ matrix_tuwunel_config_blurhashing_components_y }} +blurhash_max_raw_size = {{ matrix_tuwunel_config_blurhashing_max_raw_size }} +{% endif %} + +{% if matrix_tuwunel_config_ldap_enabled | bool %} + +[global.ldap] +enable = true +uri = {{ matrix_tuwunel_config_ldap_uri | to_json }} +base_dn = {{ matrix_tuwunel_config_ldap_base_dn | to_json }} +{% if matrix_tuwunel_config_ldap_bind_dn | length > 0 %} +bind_dn = {{ matrix_tuwunel_config_ldap_bind_dn | to_json }} +{% endif %} +{% if matrix_tuwunel_config_ldap_bind_password_file | length > 0 %} +bind_password_file = {{ matrix_tuwunel_config_ldap_bind_password_file | to_json }} +{% endif %} +filter = {{ matrix_tuwunel_config_ldap_filter | to_json }} +uid_attribute = {{ matrix_tuwunel_config_ldap_uid_attribute | to_json }} +name_attribute = {{ matrix_tuwunel_config_ldap_name_attribute | to_json }} +{% if matrix_tuwunel_config_ldap_admin_base_dn | length > 0 %} +admin_base_dn = {{ matrix_tuwunel_config_ldap_admin_base_dn | to_json }} +{% endif %} +{% if matrix_tuwunel_config_ldap_admin_filter | length > 0 %} +admin_filter = {{ matrix_tuwunel_config_ldap_admin_filter | to_json }} +{% endif %} +{% endif %} + +{% if matrix_tuwunel_config_jwt_enabled | bool %} + +[global.jwt] +enable = true +{% if matrix_tuwunel_config_jwt_key | length > 0 %} +key = {{ matrix_tuwunel_config_jwt_key | to_json }} +{% endif %} +format = {{ matrix_tuwunel_config_jwt_format | to_json }} +algorithm = {{ matrix_tuwunel_config_jwt_algorithm | to_json }} +register_user = {{ matrix_tuwunel_config_jwt_register_user | to_json }} +{% if matrix_tuwunel_config_jwt_audience | length > 0 %} +audience = {{ matrix_tuwunel_config_jwt_audience | to_json }} +{% endif %} +{% if matrix_tuwunel_config_jwt_issuer | length > 0 %} +issuer = {{ matrix_tuwunel_config_jwt_issuer | to_json }} +{% endif %} +require_exp = {{ matrix_tuwunel_config_jwt_require_exp | to_json }} +require_nbf = {{ matrix_tuwunel_config_jwt_require_nbf | to_json }} +validate_exp = {{ matrix_tuwunel_config_jwt_validate_exp | to_json }} +validate_nbf = {{ matrix_tuwunel_config_jwt_validate_nbf | to_json }} +{% endif %} + +{% for idp in matrix_tuwunel_config_identity_providers %} + +[[global.identity_provider]] +{% for key, value in idp.items() %} +{{ key }} = {{ value | to_json }} +{% endfor %} +{% endfor %} + +{% for sp in matrix_tuwunel_config_storage_providers %} + +[global.storage_provider.{{ sp.id }}.{{ sp.kind }}] +{% for key, value in sp.items() if key not in ['id', 'kind'] %} +{{ key }} = {{ value | to_json }} +{% endfor %} +{% endfor %} diff --git a/roles/custom/matrix-tuwunel/vars/main.yml b/roles/custom/matrix-tuwunel/vars/main.yml new file mode 100644 index 000000000..0d0797b72 --- /dev/null +++ b/roles/custom/matrix-tuwunel/vars/main.yml @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 MDAD project contributors +# SPDX-FileCopyrightText: 2026 Slavi Pantaleev +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +--- + +matrix_tuwunel_client_api_url_endpoint_public: "{{ 'https' if matrix_playbook_ssl_enabled else 'http' }}://{{ matrix_tuwunel_hostname }}/_matrix/client/versions" +matrix_tuwunel_federation_api_url_endpoint_public: "{{ 'https' if matrix_playbook_ssl_enabled else 'http' }}://{{ matrix_tuwunel_hostname }}:{{ matrix_federation_public_port }}/_matrix/federation/v1/version" diff --git a/setup.yml b/setup.yml index 91dc4ea0b..a184aa663 100644 --- a/setup.yml +++ b/setup.yml @@ -97,6 +97,7 @@ - custom/matrix-dendrite - custom/matrix-conduit - custom/matrix-continuwuity + - custom/matrix-tuwunel - custom/matrix-ketesa - custom/matrix-synapse-usage-exporter - galaxy/prometheus_nginxlog_exporter