diff --git a/data/global.yaml b/data/global.yaml index be1c9ab9..b92dee25 100644 --- a/data/global.yaml +++ b/data/global.yaml @@ -1,5 +1,7 @@ --- lookup_options: + '^profile::.+::sensitive_\w+$': + convert_to: 'Sensitive' profiles::ssh_keys::additional_keys::user_list: merge: deep diff --git a/data/nodes/matrix01.voxpupu.li.yaml b/data/nodes/matrix01.voxpupu.li.yaml index a5c5f69c..3f2329a5 100644 --- a/data/nodes/matrix01.voxpupu.li.yaml +++ b/data/nodes/matrix01.voxpupu.li.yaml @@ -1,11 +1,9 @@ --- +role: roles::matrix_server + profiles::ssh_keys::additional_keys::user_list: - genebean - ananace -classes: - - profiles::nginx - - profiles::matrix - profiles::borg::borg_includes: - /srv/pg_dumps diff --git a/site/profiles/files/matrix/Dockerfile.synapse b/site/profiles/files/matrix/Dockerfile.synapse new file mode 100644 index 00000000..62209ab0 --- /dev/null +++ b/site/profiles/files/matrix/Dockerfile.synapse @@ -0,0 +1,8 @@ +# Dockerfile.synapse +# Extends the official Synapse image with the S3 storage provider. +# Build: docker build -f Dockerfile.synapse -t synapse-s3:latest . +# Then replace matrixdotorg/synapse:latest with synapse-s3:latest +# in docker-compose.yml for all synapse* and media-cron services. + +FROM matrixdotorg/synapse:latest +RUN pip install --no-cache-dir synapse-s3-storage-provider diff --git a/site/profiles/files/matrix/docker-compose.yml b/site/profiles/files/matrix/docker-compose.yml new file mode 100644 index 00000000..ebfc9303 --- /dev/null +++ b/site/profiles/files/matrix/docker-compose.yml @@ -0,0 +1,236 @@ +services: + # ───────────────────────────────────────── + # DATABASE (PostgreSQL backend) + # ───────────────────────────────────────── + postgres: + image: postgres:16-alpine + restart: unless-stopped + env_file: + - .env + environment: + POSTGRES_DB: synapse + POSTGRES_USER: synapse + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD required} + POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C + volumes: + - ./data/postgres:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - pg_isready -U synapse + interval: 10s + timeout: 5s + retries: 5 + # ───────────────────────────────────────── + # REDIS (Replication bus for workers) + # ───────────────────────────────────────── + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --save "" --appendonly no + volumes: + - ./data/redis:/data + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 10s + timeout: 5s + retries: 5 + # ───────────────────────────────────────── + # SYNAPSE MAIN PROCESS (Admin API, fallback routing, background tasks) + # ───────────────────────────────────────── + synapse-main: + build: + context: . + dockerfile: Dockerfile.synapse + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./data/media:/media + - ./logs/synapse:/logs + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + ports: + - 127.0.0.1:8008:8008 + # ───────────────────────────────────────── + # WORKER: generic_worker1 (Client API endpoints) + # ───────────────────────────────────────── + synapse-worker1: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: generic_worker1 + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8081, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"client\", \"federation\"\ + ]}]},\n {\"type\": \"http\", \"port\": 9101, \"bind_addresses\": [\"127.0.0.1\"\ + ],\n \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./logs/synapse:/logs + depends_on: + - synapse-main + ports: + - 127.0.0.1:8081:8081 + # ───────────────────────────────────────── + # WORKER: generic_worker2 (Client API endpoints) + # ───────────────────────────────────────── + synapse-worker2: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: generic_worker2 + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8082, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"client\", \"federation\"\ + ]}]},\n {\"type\": \"http\", \"port\": 9102, \"bind_addresses\": [\"127.0.0.1\"\ + ],\n \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./logs/synapse:/logs + depends_on: + - synapse-main + ports: + - 127.0.0.1:8082:8082 + # ───────────────────────────────────────── + # WORKER: generic_worker3 (Federation sender and receiver) + # ───────────────────────────────────────── + synapse-worker3: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: generic_worker3 + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8083, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"federation\"]}]},\n {\"\ + type\": \"http\", \"port\": 9103, \"bind_addresses\": [\"127.0.0.1\"],\n \ + \ \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./logs/synapse:/logs + depends_on: + - synapse-main + ports: + - 127.0.0.1:8083:8083 + # ───────────────────────────────────────── + # WORKER: generic_worker4 (Media endpoints and client Sync) + # ───────────────────────────────────────── + synapse-worker4: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: generic_worker4 + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8084, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"media\", \"client\"]}]},\n\ + \ {\"type\": \"http\", \"port\": 9104, \"bind_addresses\": [\"127.0.0.1\"\ + ],\n \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./data/media:/media + - ./logs/synapse:/logs + depends_on: + - synapse-main + ports: + - 127.0.0.1:8084:8084 + # ───────────────────────────────────────── + # STREAM WRITER: events_persister (Handles event persistence streams) + # ───────────────────────────────────────── + synapse-events-persister: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: events_persister + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8085, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"replication\"]}]},\n {\"\ + type\": \"http\", \"port\": 9105, \"bind_addresses\": [\"127.0.0.1\"],\n \ + \ \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./logs/synapse:/logs + depends_on: + - synapse-main + # ───────────────────────────────────────── + # STREAM WRITER: receipts_writer (Handles typing/receipts/to-device streams) + # ───────────────────────────────────────── + synapse-receipts-writer: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + SYNAPSE_WORKER_APP: synapse.app.generic_worker + SYNAPSE_WORKER_NAME: receipts_writer + SYNAPSE_WORKER_REPLICATION_HOST: synapse-main + SYNAPSE_WORKER_REPLICATION_HTTP_PORT: '9093' + SYNAPSE_WORKER_LISTENERS: "[{\"type\": \"http\", \"port\": 8086, \"bind_addresses\"\ + : [\"0.0.0.0\"],\n \"resources\": [{\"names\": [\"replication\"]}]},\n {\"\ + type\": \"http\", \"port\": 9106, \"bind_addresses\": [\"127.0.0.1\"],\n \ + \ \"resources\": [{\"names\": [\"metrics\"]}]}]\n" + volumes: + - ./config/synapse:/config:ro + - ./data/synapse:/data + - ./logs/synapse:/logs + depends_on: + - synapse-main + # ───────────────────────────────────────── + # MEDIA EVICTION CRON (S3 sync to Hetzner Object Storage) + # ───────────────────────────────────────── + media-cron: + image: synapse-s3-local:latest + restart: unless-stopped + env_file: + - .env + entrypoint: "sh -c \"while true; do\n python -m synapse.app.media_repository\ + \ \\\n --config-path /config/homeserver.yaml \\\n --media-path /media\ + \ \\\n --storage-providers s3; \\\n sleep 86400; \\\ndone\"\n" + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + volumes: + - ./config/synapse:/config:ro + - ./data/media:/media + depends_on: + - synapse-main diff --git a/site/profiles/manifests/matrix.pp b/site/profiles/manifests/matrix.pp index ea39e462..6be5c80a 100644 --- a/site/profiles/manifests/matrix.pp +++ b/site/profiles/manifests/matrix.pp @@ -1,83 +1,28 @@ -class profiles::matrix { - $domain = 'voxpupuli.party' - $root = "/srv/${domain}" - file { $root: - ensure => 'directory', - } - file { "${root}/.well-known": - ensure => 'directory', - } - file { "${root}/.well-known/matrix/": - ensure => 'directory', - } - file { "${root}/.well-known/matrix/client": - ensure => 'file', - } - file { "${root}/.well-known/matrix/server": - ensure => 'file', - } - require nftables::rules::http - require nftables::rules::https - - if fact("letsencrypt_directory.\"${domain}\"") { - $path = fact("letsencrypt_directory.\"${domain}\"") - $ssl = { - ssl_key => "${path}/privkey.pem", - ssl_cert => "${path}/fullchain.pem", - ssl => true, - ssl_port => 443, - ssl_session_timeout => '1d', - ssl_session_tickets => 'off', - ssl_protocols => 'TLSv1.2 TLSv1.3', - ssl_prefer_server_ciphers => 'off', - add_header => { - 'Strict-Transport-Security' => { - 'max-age=63072000' => 'always', - }, - }, - } - } else { - $ssl = {} - } - nginx::resource::server { $domain: - listen_port => 80, - server_name => [$domain], - ipv6_enable => true, - ipv6_listen_options => '', # when using IPv4 & IPv6 the default options break Nginx - http2 => 'on', - access_log => "/var/log/nginx/${domain}.access.log", - error_log => "/var/log/nginx/${domain}.error.log", - format_log => 'combined', - use_default_location => false, - index_files => [], - www_root => $root, - * => $ssl, - } - nginx::resource::location { "${domain} Let's Encrypt challenges": - server => $domain, - www_root => '/var/lib/letsencrypt/', - index_files => [], - location => '^~ /.well-known/acme-challenge/', - } - nginx::resource::location { "${domain} acme-challenge directory": - server => $domain, - location_cfg_append => { 'return' => '404', }, - index_files => [], - location => '= /.well-known/acme-challenge/', - } - letsencrypt::certonly { $domain: - domains => [$domain], - plugin => 'nginx', - manage_cron => true, - additional_args => [ - '--no-redirect', - ], - require => [ - Nginx::Resource::Server[$domain], - Nginx::Resource::Location[ - "${domain} Let's Encrypt challenges", - "${domain} acme-challenge directory", - ], - ], - } +# @summary A wrapper profile to set up Matrix Synapse and its required services +# +# This profile serves as the entry point for configuring the Matrix server. It configures +# Nginx as a reverse proxy, provisions SSL certificates, and orchestrates the Synapse +# Docker backend with Hetzner S3 object storage for media eviction and federation routing. +# +# @param sensitive_postgres_password The password for the Synapse PostgreSQL database. +# @param sensitive_macaroon_secret_key The secret key used for signing macaroons. +# @param sensitive_form_secret The secret key used for form validation. +# @param sensitive_s3_access_key The access key for the S3 object storage bucket. +# @param sensitive_s3_secret_key The secret key for the S3 object storage bucket. +# @param s3_bucket The name of the S3 bucket to act as the media store. +# @param s3_region The region where the S3 bucket is hosted. +# @param s3_endpoint The endpoint URL for the S3 object storage. +# +class profiles::matrix ( + Sensitive[String] $sensitive_postgres_password, + Sensitive[String] $sensitive_macaroon_secret_key, + Sensitive[String] $sensitive_form_secret, + Sensitive[String] $sensitive_s3_access_key, + Sensitive[String] $sensitive_s3_secret_key, + String $s3_bucket, + String $s3_region, + String $s3_endpoint, +) { + include profiles::matrix::nginx + include profiles::matrix::synapse } diff --git a/site/profiles/manifests/matrix/nginx.pp b/site/profiles/manifests/matrix/nginx.pp new file mode 100644 index 00000000..58cd2b7f --- /dev/null +++ b/site/profiles/manifests/matrix/nginx.pp @@ -0,0 +1,238 @@ +# @summary Nginx and Let's Encrypt configuration for the Matrix profile +# +# This class manages the host-level Nginx server and Let's Encrypt certificates. +# It handles the ACME challenges, provisions SSL certificates for both the delegation +# server (`voxpupuli.party`) and the Matrix API Reverse Proxy (`matrix01.voxpupu.li`). +# +# It creates upstreams mapping to the local Synapse workers and routes the incoming federation, +# client, media, and sync traffic to the appropriate Dockerized endpoints. +# +class profiles::matrix::nginx { + $domain = 'voxpupuli.party' + $matrix_domain = 'matrix01.voxpupu.li' + + # --------------------------------------------------------- + # voxpupuli.party (DELEGATION SERVER) + # --------------------------------------------------------- + $root = "/srv/${domain}" + + file { + default: + ensure => 'directory', + ; + $root: + ; + "${root}/.well-known": + ; + "${root}/.well-known/matrix/": + ; + [ + "${root}/.well-known/matrix/client", + "${root}/.well-known/matrix/server", + ]: + ensure => 'file', + ; + } + + if fact("letsencrypt_directory.\"${domain}\"") { + $path = fact("letsencrypt_directory.\"${domain}\"") + $ssl = { + ssl_key => "${path}/privkey.pem", + ssl_cert => "${path}/fullchain.pem", + ssl => true, + ssl_port => 443, + ssl_session_timeout => '1d', + ssl_session_tickets => 'off', + ssl_protocols => 'TLSv1.2 TLSv1.3', + ssl_prefer_server_ciphers => 'off', + add_header => { + 'Strict-Transport-Security' => { + 'max-age=63072000' => 'always', + }, + }, + } + } else { + $ssl = {} + } + + nginx::resource::server { $domain: + listen_port => 80, + server_name => [$domain], + ipv6_enable => true, + ipv6_listen_options => '', + http2 => 'on', + access_log => "/var/log/nginx/${domain}.access.log", + error_log => "/var/log/nginx/${domain}.error.log", + format_log => 'combined', + use_default_location => false, + index_files => [], + www_root => $root, + * => $ssl, + } + + letsencrypt::certonly { $domain: + domains => [$domain], + plugin => 'nginx', + manage_cron => true, + additional_args => ['--no-redirect'], + require => [ + Nginx::Resource::Server[$domain], + Nginx::Resource::Location[ + "${domain} Let's Encrypt challenges", + "${domain} acme-challenge directory", + ], + ], + } + + # --------------------------------------------------------- + # matrix01.voxpupu.li (SYNAPSE REVERSE PROXY) + # --------------------------------------------------------- + + # Upstreams matching compose mapping + nginx::resource::upstream { + 'synapse_client': + members => ['127.0.0.1:8081', '127.0.0.1:8082']; + 'synapse_federation': + members => ['127.0.0.1:8081', '127.0.0.1:8082', '127.0.0.1:8083']; + 'synapse_media': + members => ['127.0.0.1:8084']; + 'synapse_main': + members => ['127.0.0.1:8008']; + } + + if fact("letsencrypt_directory.\"${matrix_domain}\"") { + $matrix_path = fact("letsencrypt_directory.\"${matrix_domain}\"") + $matrix_ssl = { + ssl_key => "${matrix_path}/privkey.pem", + ssl_cert => "${matrix_path}/fullchain.pem", + ssl => true, + ssl_port => 443, + ssl_session_timeout => '1d', + ssl_session_cache => 'shared:SSL:10m', + ssl_session_tickets => 'off', + ssl_protocols => 'TLSv1.2 TLSv1.3', + ssl_ciphers => 'ECDHE+AESGCM:DHE+AESGCM:ECDHE+ECDSA+AES+SHA256', + ssl_prefer_server_ciphers => 'off', + add_header => { + 'Strict-Transport-Security' => { + 'max-age=63072000' => 'always', + }, + }, + } + } else { + $matrix_ssl = {} + } + + nginx::resource::server { $matrix_domain: + listen_port => 80, + server_name => [$matrix_domain], + client_max_body_size => '50m', + ipv6_enable => true, + ipv6_listen_options => '', + http2 => 'on', + access_log => "/var/log/nginx/${matrix_domain}.access.log", + error_log => "/var/log/nginx/${matrix_domain}.error.log", + format_log => 'combined', + use_default_location => false, + * => $matrix_ssl, + } + + letsencrypt::certonly { $matrix_domain: + domains => [$matrix_domain], + plugin => 'nginx', + manage_cron => true, + additional_args => ['--no-redirect'], + require => [ + Nginx::Resource::Server[$matrix_domain], + Nginx::Resource::Location[ + "${matrix_domain} Let's Encrypt challenges", + "${matrix_domain} acme-challenge directory", + ], + ], + } + + # --------------------------------------------------------- + # Nginx Locations Multi-Declaration + # --------------------------------------------------------- + + # Acme and static locations + nginx::resource::location { + default: + index_files => [], + ; + "${domain} Let's Encrypt challenges": + server => $domain, + www_root => '/var/lib/letsencrypt/', + location => '^~ /.well-known/acme-challenge/', + ; + "${domain} acme-challenge directory": + server => $domain, + location_cfg_append => { 'return' => '404', }, + location => '= /.well-known/acme-challenge/', + ; + "${matrix_domain} Let's Encrypt challenges": + server => $matrix_domain, + www_root => '/var/lib/letsencrypt/', + location => '^~ /.well-known/acme-challenge/', + ; + "${matrix_domain} acme-challenge directory": + server => $matrix_domain, + location_cfg_append => { 'return' => '404', }, + location => '= /.well-known/acme-challenge/', + ; + "${matrix_domain} root fallback": + server => $matrix_domain, + location => '/', + location_cfg_append => { 'return' => '404' }, + ; + } + + # Reverse proxy endpoints + $proxy_params = { + 'X-Forwarded-For' => '$remote_addr', + 'X-Forwarded-Proto' => '$scheme', + 'Host' => '$host', + } + + nginx::resource::location { + default: + server => $matrix_domain, + proxy_set_header => $proxy_params, + ; + "${matrix_domain} admin API": + location => '/_synapse/admin', + proxy => 'http://synapse_main', + location_allow => ['127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'], + location_deny => ['all'], + ; + "${matrix_domain} media": + location => '~ ^/_matrix/media/', + proxy => 'http://synapse_media', + ; + "${matrix_domain} client sync": + location => '~ ^/_matrix/client/.*/sync$', + proxy => 'http://synapse_media', + proxy_read_timeout => '60s', + ; + "${matrix_domain} client paths": + location => '~ ^/_matrix/client/.*/(user|rooms/.*/initialSync)', + proxy => 'http://synapse_media', + ; + "${matrix_domain} federation": + location => '/_matrix/federation/', + proxy => 'http://synapse_federation', + ; + "${matrix_domain} key": + location => '/_matrix/key/', + proxy => 'http://synapse_federation', + ; + "${matrix_domain} client": + location => '/_matrix/client/', + proxy => 'http://synapse_client', + ; + "${matrix_domain} fallback": + location => '/_matrix/', + proxy => 'http://synapse_main', + ; + } +} diff --git a/site/profiles/manifests/matrix/synapse.pp b/site/profiles/manifests/matrix/synapse.pp new file mode 100644 index 00000000..43428c10 --- /dev/null +++ b/site/profiles/manifests/matrix/synapse.pp @@ -0,0 +1,67 @@ +# @summary Docker Compose deployment for Matrix Synapse +# +# This class ensures the presence of the `/opt/matrix-synapse` directory and coordinates +# the templated layout of `.env` and `homeserver.yaml` files holding sensitive deployment +# secrets securely. Finally, it uses `puppetlabs-docker` to orchestrate the Docker Compose +# multi-worker stack based on the provided configuration. +# +class profiles::matrix::synapse { + $matrix_dir = '/opt/matrix-synapse' + + file { + default: + owner => 'root', + group => 'root', + ; + $matrix_dir: + ensure => directory, + mode => '0750', + ; + [ + "${matrix_dir}/config", + "${matrix_dir}/config/synapse", + ]: + ensure => directory, + ; + "${matrix_dir}/docker-compose.yml": + ensure => file, + source => 'puppet:///modules/profiles/matrix/docker-compose.yml', + ; + "${matrix_dir}/Dockerfile.synapse": + ensure => file, + source => 'puppet:///modules/profiles/matrix/Dockerfile.synapse', + ; + "${matrix_dir}/.env": + ensure => file, + mode => '0600', + content => epp('profiles/matrix/env.epp', { + 'postgres_password' => $profiles::matrix::sensitive_postgres_password, + }), + ; + "${matrix_dir}/config/synapse/homeserver.yaml": + ensure => file, + mode => '0600', + content => epp('profiles/matrix/homeserver.yaml.epp', { + 'macaroon_secret_key' => $profiles::matrix::sensitive_macaroon_secret_key, + 'form_secret' => $profiles::matrix::sensitive_form_secret, + 'postgres_password' => $profiles::matrix::sensitive_postgres_password, + 's3_bucket' => $profiles::matrix::s3_bucket, + 's3_region' => $profiles::matrix::s3_region, + 's3_endpoint' => $profiles::matrix::s3_endpoint, + 's3_access_key' => $profiles::matrix::sensitive_s3_access_key, + 's3_secret_key' => $profiles::matrix::sensitive_s3_secret_key, + }), + ; + } + + docker::compose { 'matrix-synapse': + ensure => present, + compose_files => ["${matrix_dir}/docker-compose.yml"], + subscribe => [ + File["${matrix_dir}/docker-compose.yml"], + File["${matrix_dir}/Dockerfile.synapse"], + File["${matrix_dir}/.env"], + File["${matrix_dir}/config/synapse/homeserver.yaml"], + ], + } +} diff --git a/site/profiles/templates/matrix/env.epp b/site/profiles/templates/matrix/env.epp new file mode 100644 index 00000000..a8dbd866 --- /dev/null +++ b/site/profiles/templates/matrix/env.epp @@ -0,0 +1,7 @@ +<%- | Sensitive[String] $postgres_password | -%> +# ── PostgreSQL ────────────────────────────────────────────── +POSTGRES_PASSWORD=<%= $postgres_password %> + +# ── Synapse ───────────────────────────────────────────────── +SYNAPSE_SERVER_NAME=voxpupuli.party +SYNAPSE_REPORT_STATS=yes diff --git a/site/profiles/templates/matrix/homeserver.yaml.epp b/site/profiles/templates/matrix/homeserver.yaml.epp new file mode 100644 index 00000000..0fd4029c --- /dev/null +++ b/site/profiles/templates/matrix/homeserver.yaml.epp @@ -0,0 +1,162 @@ +<%- + | Sensitive[String] $macaroon_secret_key, + Sensitive[String] $form_secret, + Sensitive[String] $postgres_password, + String $s3_bucket, + String $s3_region, + String $s3_endpoint, + Sensitive[String] $s3_access_key, + Sensitive[String] $s3_secret_key + | +-%> +# homeserver.yaml +# Place this at ./config/synapse/homeserver.yaml +# +# Generate a base config first: +# docker run --rm -v $(pwd)/config/synapse:/data \ +# -e SYNAPSE_SERVER_NAME=voxpupuli.party \ +# -e SYNAPSE_REPORT_STATS=no \ +# matrixdotorg/synapse generate +# Then replace with this file, preserving the generated secrets. + +server_name: "voxpupuli.party" +public_baseurl: "https://matrix01.voxpupu.li" +report_stats: true + +# Pid / log +pid_file: /data/homeserver.pid +log_config: /config/log.yaml + +# ── Listeners ───────────────────────────────────────────────────────────────── +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + bind_addresses: ["0.0.0.0"] + resources: + - names: [client, federation] + compress: false + # Replication listener for workers + - port: 9093 + type: http + bind_addresses: ["0.0.0.0"] + resources: + - names: [replication] + +# ── Database ────────────────────────────────────────────────────────────────── +database: + name: psycopg2 + args: + user: synapse + password: "<%= $postgres_password %>" + database: synapse + host: postgres + port: 5432 + cp_min: 5 + cp_max: 10 + +# ── Redis (replication bus) ─────────────────────────────────────────────────── +redis: + enabled: true + host: redis + port: 6379 + +# ── Worker configuration ────────────────────────────────────────────────────── + +# Disable federation sending in the main process +send_federation: false +federation_sender_instances: + - generic_worker3 + +# Stream writers — these workers own specific write streams +stream_writers: + events: + - events_persister + receipts: + - receipts_writer + typing: + - receipts_writer + to_device: + - receipts_writer + account_data: + - receipts_writer + +# Route /sync to the dedicated media/sync worker +run_background_tasks_on: generic_worker4 + +# Instance map — all workers must be reachable by name +instance_map: + main: + host: synapse-main + port: 9093 + generic_worker1: + host: synapse-worker1 + port: 8081 + generic_worker2: + host: synapse-worker2 + port: 8082 + generic_worker3: + host: synapse-worker3 + port: 8083 + generic_worker4: + host: synapse-worker4 + port: 8084 + events_persister: + host: synapse-events-persister + port: 8085 + receipts_writer: + host: synapse-receipts-writer + port: 8086 + +# ── Media storage ───────────────────────────────────────────────────────────── +media_store_path: /media/local_content +remote_media_delay_days: 90 + +# Hetzner Object Storage via s3_storage_provider +# Install first: pip install synapse-s3-storage-provider +media_storage_providers: + - module: s3_storage_provider.S3StorageProviderBackend + store_local: true + store_remote: true + store_synchronous: false # async upload; cron handles eviction + config: + bucket: "<%= $s3_bucket %>" + region_name: "<%= $s3_region %>" + endpoint_url: "<%= $s3_endpoint %>" + access_key_id: "<%= $s3_access_key %>" + secret_access_key: "<%= $s3_secret_key %>" + +# ── Registration ────────────────────────────────────────────────────────────── +# Very few local accounts — disable open registration +enable_registration: false +# To create local accounts manually: +# docker compose exec synapse-main register_new_matrix_user \ +# -c /config/homeserver.yaml http://localhost:8008 + +# ── Delegation ─────────────────────────────────────────────────────────────── +# Nginx on voxpupuli.party serves: +# /.well-known/matrix/server → {"m.server": "matrix01.voxpupu.li:443"} +# /.well-known/matrix/client → {"m.homeserver": {"base_url": "https://matrix01.voxpupu.li"}} +# No additional config needed here — delegation is fully handled outside of this stack. + +# ── Security ────────────────────────────────────────────────────────────────── +# Paste generated values from `docker run ... generate` here: +macaroon_secret_key: "<%= $macaroon_secret_key %>" +form_secret: "<%= $form_secret %>" +signing_key_path: /data/voxpupuli.party.signing.key + +trusted_key_servers: + - server_name: "matrix.org" + +# ── Retention / limits ──────────────────────────────────────────────────────── +max_upload_size: 50M +url_preview_enabled: true +url_preview_ip_range_blacklist: + - "127.0.0.0/8" + - "10.0.0.0/8" + - "172.16.0.0/12" + - "192.168.0.0/16" + - "100.64.0.0/10" + - "::1/128" + - "fe80::/10" diff --git a/site/roles/manifests/matrix_server.pp b/site/roles/manifests/matrix_server.pp new file mode 100644 index 00000000..91ddee71 --- /dev/null +++ b/site/roles/manifests/matrix_server.pp @@ -0,0 +1,19 @@ +# @summary Sets up a matrix server with synapse, nginx, and letsencrypt +# +# Sets up a highly scalable matrix server with Synapse, Nginx, and Let's Encrypt managed +# via Docker Compose and host-level ACME challenges. +# +# The Docker Compose stack dynamically provisions multiple Synapse workers to distribute +# processing loads (such as media caching, client APIs, and sync handling), specifically +# optimizing for heavy inbound API federation traffic—a crucial requirement since the +# majority of matrix interactions will originate externally from matrix.org rather than +# on this localized host. +# +class roles::matrix_server { + include nftables::rules::http + include nftables::rules::https + include profiles::certbot + include profiles::lets_encrypt + include profiles::matrix + include profiles::nginx +}