Explorar el Código

matrix-tuwunel: add Tuwunel homeserver role (#5200)

Tuwunel is a Matrix homeserver maintained by the matrix-construct
organisation. See https://matrix-construct.github.io/tuwunel/.

The rendered TOML emits only keys exposed as Ansible variables; the
rest fall back to tuwunel's upstream defaults. Anything not surfaced
can be set via the TUWUNEL_* env extension or by overriding the
template path.

Popular features Tuwunel adds variables for:

- OAuth2/OIDC identity providers (a list of `[[global.identity_provider]]`
  blocks; brand-aware defaults for Google, GitHub, Keycloak, MAS, etc)
- LDAP and JWT authentication
- Media storage providers (native local and S3 with multipart upload)
- RocksDB tuning (compression, direct_io, parallelism, online backups)
- Native TLS dual-protocol mode
- Blurhashing, Sentry crash reporting

Auto-wired from existing playbook globals: well-known client URL,
TURN/coturn, MatrixRTC LiveKit URL, federation.

The `tuwunel-migrate-from-conduwuit` tag performs a binary-swap
migration. Migration from any other Conduit derivative is unsupported
and would corrupt the database.

Signed-off-by: Jason Volk <jason@zemos.net>
pull/5201/head
Jason Volk hace 18 horas
committed by GitHub
padre
commit
c111008d25
No se encontró ninguna clave conocida en la base de datos para esta firma ID de clave GPG: B5690EEEBB952194
Se han modificado 26 ficheros con 1419 adiciones y 3 borrados
  1. +1
    -0
      README.md
  2. +234
    -0
      docs/configuring-playbook-tuwunel.md
  3. +2
    -0
      docs/configuring-playbook.md
  4. +1
    -0
      docs/container-images.md
  5. +1
    -1
      docs/howto-srv-server-delegation.md
  6. +66
    -0
      group_vars/matrix_servers
  7. +1
    -1
      roles/custom/matrix-base/defaults/main.yml
  8. +1
    -1
      roles/custom/matrix-base/tasks/validate_config.yml
  9. +315
    -0
      roles/custom/matrix-tuwunel/defaults/main.yml
  10. +76
    -0
      roles/custom/matrix-tuwunel/tasks/install.yml
  11. +40
    -0
      roles/custom/matrix-tuwunel/tasks/main.yml
  12. +83
    -0
      roles/custom/matrix-tuwunel/tasks/migrate_from_conduwuit.yml
  13. +28
    -0
      roles/custom/matrix-tuwunel/tasks/self_check_client_api.yml
  14. +33
    -0
      roles/custom/matrix-tuwunel/tasks/self_check_federation_api.yml
  15. +8
    -0
      roles/custom/matrix-tuwunel/tasks/setup_install.yml
  16. +8
    -0
      roles/custom/matrix-tuwunel/tasks/setup_uninstall.yml
  17. +24
    -0
      roles/custom/matrix-tuwunel/tasks/uninstall.yml
  18. +44
    -0
      roles/custom/matrix-tuwunel/tasks/validate_config.yml
  19. +1
    -0
      roles/custom/matrix-tuwunel/templates/env.j2
  20. +4
    -0
      roles/custom/matrix-tuwunel/templates/env.j2.license
  21. +141
    -0
      roles/custom/matrix-tuwunel/templates/labels.j2
  22. +55
    -0
      roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2
  23. +4
    -0
      roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2.license
  24. +238
    -0
      roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2
  25. +9
    -0
      roles/custom/matrix-tuwunel/vars/main.yml
  26. +1
    -0
      setup.yml

+ 1
- 0
README.md Ver fichero

@@ -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


+ 234
- 0
docs/configuring-playbook-tuwunel.md Ver fichero

@@ -0,0 +1,234 @@
<!--
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Jason Volk

SPDX-License-Identifier: AGPL-3.0-or-later
-->

# 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 `<well-known>/_matrix/client/unstable/login/sso/callback/<client_id>` 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: '<provider 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: '<github oauth app id>'
client_secret: '<github oauth app 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: '<shared secret>'
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.<id>.<kind>]` 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: '<aws 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_name>` 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: <token>
hs_token: <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.

+ 2
- 0
docs/configuring-playbook.md Ver fichero

@@ -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:


+ 1
- 0
docs/container-images.md Ver fichero

@@ -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


+ 1
- 1
docs/howto-srv-server-delegation.md Ver fichero

@@ -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.


+ 66
- 0
group_vars/matrix_servers Ver fichero

@@ -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


+ 1
- 1
roles/custom/matrix-base/defaults/main.yml Ver fichero

@@ -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.
#


+ 1
- 1
roles/custom/matrix-base/tasks/validate_config.yml Ver fichero

@@ -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:


+ 315
- 0
roles/custom/matrix-tuwunel/defaults/main.yml Ver fichero

@@ -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.<ID>.<kind>]`.
#
# 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

+ 76
- 0
roles/custom/matrix-tuwunel/tasks/install.yml Ver fichero

@@ -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)
}}

+ 40
- 0
roles/custom/matrix-tuwunel/tasks/main.yml Ver fichero

@@ -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"

+ 83
- 0
roles/custom/matrix-tuwunel/tasks/migrate_from_conduwuit.yml Ver fichero

@@ -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

+ 28
- 0
roles/custom/matrix-tuwunel/tasks/self_check_client_api.yml Ver fichero

@@ -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

+ 33
- 0
roles/custom/matrix-tuwunel/tasks/self_check_federation_api.yml Ver fichero

@@ -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"

+ 8
- 0
roles/custom/matrix-tuwunel/tasks/setup_install.yml Ver fichero

@@ -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"

+ 8
- 0
roles/custom/matrix-tuwunel/tasks/setup_uninstall.yml Ver fichero

@@ -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"

+ 24
- 0
roles/custom/matrix-tuwunel/tasks/uninstall.yml Ver fichero

@@ -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

+ 44
- 0
roles/custom/matrix-tuwunel/tasks/validate_config.yml Ver fichero

@@ -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 }}"

+ 1
- 0
roles/custom/matrix-tuwunel/templates/env.j2 Ver fichero

@@ -0,0 +1 @@
{{ matrix_tuwunel_environment_variables_extension }}

+ 4
- 0
roles/custom/matrix-tuwunel/templates/env.j2.license Ver fichero

@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Slavi Pantaleev

SPDX-License-Identifier: AGPL-3.0-or-later

+ 141
- 0
roles/custom/matrix-tuwunel/templates/labels.j2 Ver fichero

@@ -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 }}

+ 55
- 0
roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2 Ver fichero

@@ -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

+ 4
- 0
roles/custom/matrix-tuwunel/templates/systemd/matrix-tuwunel.service.j2.license Ver fichero

@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2026 MDAD project contributors
SPDX-FileCopyrightText: 2026 Slavi Pantaleev

SPDX-License-Identifier: AGPL-3.0-or-later

+ 238
- 0
roles/custom/matrix-tuwunel/templates/tuwunel.toml.j2 Ver fichero

@@ -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 %}

+ 9
- 0
roles/custom/matrix-tuwunel/vars/main.yml Ver fichero

@@ -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"

+ 1
- 0
setup.yml Ver fichero

@@ -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


Cargando…
Cancelar
Guardar