From ab53e328668a70f41efcaa22f6e06b79946302c6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 4 Nov 2024 02:51:46 +0000 Subject: [PATCH] a first stab at a `docker compose up` matrix 2.0 stack --- .env-sample | 28 ++ .gitignore | 6 + compose.yml | 241 ++++++++++++++++++ data-template/element-web/config.json | 34 +++ data-template/mas/config.yaml | 101 ++++++++ data-template/nginx/.well-known/matrix/client | 14 + data-template/nginx/.well-known/matrix/server | 3 + .../nginx/.well-known/matrix/support | 7 + data-template/nginx/app.conf | 112 ++++++++ .../create-multiple-postgresql-databases.sh | 24 ++ data-template/synapse/homeserver.yaml | 98 +++++++ data-template/synapse/log.config | 75 ++++++ .../workers/synapse-federation-sender-1.yaml | 4 + .../workers/synapse-generic-worker-1.yaml | 11 + init/Dockerfile | 4 + init/generate-mas-secrets.sh | 14 + init/generate-synapse-secrets.sh | 12 + init/init-letsencrypt.sh | 83 ++++++ init/init.sh | 82 ++++++ 19 files changed, 953 insertions(+) create mode 100644 .env-sample create mode 100644 .gitignore create mode 100644 compose.yml create mode 100644 data-template/element-web/config.json create mode 100644 data-template/mas/config.yaml create mode 100644 data-template/nginx/.well-known/matrix/client create mode 100644 data-template/nginx/.well-known/matrix/server create mode 100644 data-template/nginx/.well-known/matrix/support create mode 100644 data-template/nginx/app.conf create mode 100755 data-template/postgres/create-multiple-postgresql-databases.sh create mode 100644 data-template/synapse/homeserver.yaml create mode 100644 data-template/synapse/log.config create mode 100644 data-template/synapse/workers/synapse-federation-sender-1.yaml create mode 100644 data-template/synapse/workers/synapse-generic-worker-1.yaml create mode 100644 init/Dockerfile create mode 100755 init/generate-mas-secrets.sh create mode 100755 init/generate-synapse-secrets.sh create mode 100755 init/init-letsencrypt.sh create mode 100755 init/init.sh diff --git a/.env-sample b/.env-sample new file mode 100644 index 0000000..adccc84 --- /dev/null +++ b/.env-sample @@ -0,0 +1,28 @@ +# These env vars get templated into the configs in the respective containers via init scripts. +# +# If you want to make more customisations then either edit the templates to add more env variables below +# (free free to contribute them back) +# or edit the templates directly. + +# n.b. SECRETS_* env variables get pulled in on demand from files in ./secrets + +VOLUME_PATH=. +DOMAIN=example.com +HOMESERVER_FQDN=matrix.example.com +ELEMENT_WEB_FQDN=element.example.com +ELEMENT_CALL_FQDN=call.example.com +MAS_FQDN=auth.example.com + +REPORT_STATS=yes + +IDENTITY_SERVER_URL=https://vector.im + +MAIL_NOTIF_FROM_ADDRESS=noreply@example.com +ABUSE_SUPPORT_EMAIL=abuse@example.com +SECURITY_SUPPORT_EMAIL=security@example.com + +MAS_CLIENT_ID="0000000000000000000SYNAPSE" +MAS_EMAIL_FROM='"Authentication Service" ' +MAS_EMAIL_REPLY_TO='"Authentication Service" ' + +COUNTRY=GB diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caa79b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +**/.DS_Store + +.env +data +secrets diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..478c051 --- /dev/null +++ b/compose.yml @@ -0,0 +1,241 @@ +# FIXME: define a frontend & backend network, and only expose backend services to the frontend (nginx) +networks: + backend: + +secrets: + postgres_password: + file: secrets/postgres/postgres_password + synapse_signing_key: + file: secrets/synapse/${DOMAIN}.signing.key + +services: + # dependencies for optionally generating default configs + secrets + generate-synapse-secrets: + image: ghcr.io/element-hq/synapse:latest + restart: "no" + volumes: + - ${VOLUME_PATH}/data/synapse:/data:rw + - ${VOLUME_PATH}/init/generate-synapse-secrets.sh:/entrypoint.sh + env_file: .env + environment: + SYNAPSE_CONFIG_DIR: /data + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml.default + SYNAPSE_SERVER_NAME: ${DOMAIN} + SYNAPSE_REPORT_STATS: ${REPORT_STATS} + entrypoint: "/entrypoint.sh" + + generate-mas-secrets: + restart: "no" + image: ghcr.io/element-hq/matrix-authentication-service:latest + volumes: + - ${VOLUME_PATH}/data/mas:/data:rw + env_file: .env + # FIXME: stop this regenerating a spurious default config every time + # We can't do the same approach as synapse (unless use a debug image of MAS) as MAS is distroless and has no bash. + command: "config generate -o /data/config.yaml.default" + + # dependency for templating /data-template into /data (having extracted any secrets from any default generated configs) + init: + build: init + restart: "no" + volumes: + - ${VOLUME_PATH}/secrets:/secrets + - ${VOLUME_PATH}/data:/data + - ${VOLUME_PATH}/data-template:/data-template + - ${VOLUME_PATH}/init/init.sh:/init.sh + command: "/init.sh" + env_file: .env + depends_on: + generate-synapse-secrets: + condition: service_completed_successfully + generate-mas-secrets: + condition: service_completed_successfully + + # nginx: + # image: nginx:latest + # restart: unless-stopped + # ports: + # - "80:80" + # - "443:443" + # volumes: + # - ${VOLUME_PATH}/data/nginx:/etc/nginx/conf.d + # - ${VOLUME_PATH}/data/certbot/conf:/etc/letsencrypt + # - ${VOLUME_PATH}/data/certbot/www:/var/www/certbot + # command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + # depends_on: + # init: + # condition: service_completed_successfully + + # certbot: + # image: certbot/certbot:latest + # restart: unless-stopped + # volumes: + # - ${VOLUME_PATH}/data/certbot/conf:/etc/letsencrypt + # - ${VOLUME_PATH}/data/certbot/www:/var/www/certbot + # entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + # depends_on: + # init: + # condition: service_completed_successfully + + postgres: + image: postgres:latest + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/data/postgres:/var/lib/postgresql/data:rw + - ${VOLUME_PATH}/data-template/postgres/create-multiple-postgresql-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgresql-databases.sh + networks: + - backend + environment: + POSTGRES_MULTIPLE_DATABASES: synapse,mas + POSTGRES_USER: matrix # FIXME: use different username+passwords for synapse & MAS DBs. + POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password + POSTGRES_INITDB_ARGS: --encoding=UTF8 --locale=C + secrets: + - postgres_password + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + start_period: "1s" + interval: "1s" + timeout: "5s" + depends_on: + init: + condition: service_completed_successfully + + redis: + image: redis:latest + restart: unless-stopped + networks: + - backend + + synapse: + image: ghcr.io/element-hq/synapse:latest + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/data/synapse:/data:rw + ports: + - 8008:8008 + networks: + - backend + environment: + SYNAPSE_CONFIG_DIR: /data + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + secrets: + - synapse_signing_key + depends_on: + postgres: + condition: service_healthy + init: + condition: service_completed_successfully + + synapse-generic-worker-1: + image: ghcr.io/element-hq/synapse:latest + restart: unless-stopped + entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/synapse-generic-worker-1.yaml"] + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8081/health || exit 1"] + start_period: "5s" + interval: "15s" + timeout: "5s" + networks: + - backend + volumes: + - ${VOLUME_PATH}/data/synapse:/data:rw + environment: + SYNAPSE_WORKER: synapse.app.generic_worker + # Expose port if required so your reverse proxy can send requests to this worker + # Port configuration will depend on how the http listener is defined in the worker configuration file + ports: + - 8081:8081 + secrets: + - synapse_signing_key + depends_on: + - synapse + + synapse-federation-sender-1: + image: ghcr.io/element-hq/synapse:latest + restart: unless-stopped + entrypoint: ["/start.py", "run", "--config-path=/data/homeserver.yaml", "--config-path=/data/workers/synapse-federation-sender-1.yaml"] + healthcheck: + disable: true + networks: + - backend + volumes: + - ${VOLUME_PATH}/data/synapse:/data:rw # Replace VOLUME_PATH with the path to your Synapse volume + environment: + SYNAPSE_WORKER: synapse.app.federation_sender + secrets: + - synapse_signing_key + depends_on: + - synapse + + matrix-authentication-service: + image: ghcr.io/element-hq/matrix-authentication-service:latest + restart: unless-stopped + ports: + - 8083:8080 + volumes: + - ${VOLUME_PATH}/data/mas:/data:rw + networks: + - backend + # FIXME: do we also need to sync the db? + command: "server --config=/data/config.yaml" + depends_on: + postgres: + condition: service_healthy + init: + condition: service_completed_successfully + + # as a basic local MTA + mailhog: + image: mailhog/mailhog:latest + restart: unless-stopped + ports: + - 8025:8025 + - 1025:1025 + networks: + - backend + + element-web: + image: vectorim/element-web:latest + restart: unless-stopped + ports: + - 8080:80 + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8080/version || exit 1"] + start_period: "5s" + interval: "15s" + timeout: "5s" + networks: + - backend + volumes: + - ${VOLUME_PATH}/data/element-web/config.json:/app/config.json + depends_on: + init: + condition: service_completed_successfully + + element-call: + image: ghcr.io/element-hq/element-call + restart: unless-stopped + ports: + - 8082:80 + networks: + - backend + volumes: + - ${VOLUME_PATH}/data/element-call/config.json:/app/config.json + depends_on: + init: + condition: service_completed_successfully + + # livekit-server: + # image: livekit/livekit-server:latest + # restart: unless-stopped + # ports: + # - 7880:7880 # HTTP API + # - 7881:7881 # WS signalling + # # - 50000-60000:50000-60000/tcp # TCP media + # # - 50000-60000:50000-60000/udp # UDP media + # networks: + # - backend + # depends_on: + # init: + # condition: service_completed_successfully diff --git a/data-template/element-web/config.json b/data-template/element-web/config.json new file mode 100644 index 0000000..3628f17 --- /dev/null +++ b/data-template/element-web/config.json @@ -0,0 +1,34 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://${HOMESERVER_FQDN}", + "server_name": "${DOMAIN}" + }, + "m.identity_server": { + "base_url": "${IDENTITY_SERVER_URL}" + } + }, + "disable_custom_urls": false, + "disable_guests": false, + "disable_login_language_selector": false, + "disable_3pid_login": false, + "force_verification": false, + "brand": "Element", + "default_widget_container_height": 280, + "default_country_code": "${COUNTRY}", + "show_labs_settings": false, + "features": {}, + "default_federate": true, + "default_theme": "light", + "room_directory": { + "servers": ["${DOMAIN}"] + }, + "setting_defaults": { + "breadcrumbs": true + }, + "element_call": { + "url": "https://${ELEMENT_CALL_FQDN}", + "brand": "Element Call" + }, + "map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx" +} \ No newline at end of file diff --git a/data-template/mas/config.yaml b/data-template/mas/config.yaml new file mode 100644 index 0000000..19c44b4 --- /dev/null +++ b/data-template/mas/config.yaml @@ -0,0 +1,101 @@ +${CONFIG_HEADER} + +http: + listeners: + - name: web + resources: + - name: discovery + - name: human + - name: oauth + - name: compat + - name: graphql + - name: assets + binds: + - address: '[::]:8080' + proxy_protocol: false + - name: internal + resources: + - name: health + binds: + - host: localhost + port: 8081 + proxy_protocol: false + trusted_proxies: + - 192.168.0.0/16 + - 172.16.0.0/12 + - 10.0.0.0/10 + - 127.0.0.1/8 + - fd00::/8 + - ::1/128 + public_base: http://[::]:8080/ + issuer: http://[::]:8080/ +database: + host: postgres + database: mas + username: matrix + password: ${SECRETS_POSTGRES_PASSWORD} + max_connections: 10 + min_connections: 0 + connect_timeout: 30 + idle_timeout: 600 + max_lifetime: 1800 +email: + from: '${MAS_EMAIL_FROM}' + reply_to: '${MAS_EMAIL_REPLY_TO}' + transport: smtp + mode: plain + hostname: mailhog + port: 1025 +${SECRETS_MAS_SECRETS} +passwords: + enabled: true + schemes: + - version: 1 + algorithm: argon2id + minimum_complexity: 3 +matrix: + homeserver: localhost:8008 + secret: '${SECRETS_MAS_MATRIX_SECRET}' + endpoint: http://localhost:8008/ + +# please keep config above this point as close as possible to the original generated config +# so that upstream generated config changes can be detected + +# these taken from midhun's quick-mas-setup +clients: + - client_id: ${MAS_CLIENT_ID} + client_auth_method: client_secret_basic + client_secret: '${SECRETS_MAS_CLIENT_SECRET}' + +templates: + path: /usr/local/share/mas-cli/templates/ + assets_manifest: /usr/local/share/mas-cli/manifest.json + translations_path: /usr/local/share/mas-cli/translations/ + +policy: + wasm_module: /usr/local/share/mas-cli/policy.wasm + client_registration_entrypoint: client_registration/violation + register_entrypoint: register/violation + authorization_grant_entrypoint: authorization_grant/violation + password_entrypoint: password/violation + email_entrypoint: email/violation + data: + client_registration: + allow_insecure_uris: true # allow non-SSL and localhost URIs + allow_missing_contacts: true # EW doesn't have contacts at this time + admin_users: + - admin + +branding: + service_name: null + policy_uri: null + tos_uri: null + imprint: null + logo_uri: null + +upstream_oauth2: + providers: [] + +experimental: + access_token_ttl: 86400 + compat_token_ttl: 86400 diff --git a/data-template/nginx/.well-known/matrix/client b/data-template/nginx/.well-known/matrix/client new file mode 100644 index 0000000..82d13fc --- /dev/null +++ b/data-template/nginx/.well-known/matrix/client @@ -0,0 +1,14 @@ +{ + "m.homeserver": { + "base_url": "https://${HOMESERVER_FQDN}" + }, + "m.identity_server": { + "base_url": "${IDENTITY_SERVER_URL}" + }, + "org.matrix.msc4143.rtc_foci": [ + { + "type": "livekit", + "livekit_service_url": "https://${ELEMENT_CALL_FQDN}" + } + ] +} diff --git a/data-template/nginx/.well-known/matrix/server b/data-template/nginx/.well-known/matrix/server new file mode 100644 index 0000000..0c8c2a2 --- /dev/null +++ b/data-template/nginx/.well-known/matrix/server @@ -0,0 +1,3 @@ +{ + "m.server": "${HOMESERVER_FQDN}:443" +} diff --git a/data-template/nginx/.well-known/matrix/support b/data-template/nginx/.well-known/matrix/support new file mode 100644 index 0000000..01cd283 --- /dev/null +++ b/data-template/nginx/.well-known/matrix/support @@ -0,0 +1,7 @@ +{ + "support_page": "https://matrix.org/contact/", + "contacts": [ + { "role": "m.role.admin", "email_address": "${ABUSE_SUPPORT_EMAIL}" }, + { "role": "m.role.security", "email_address": "${SECURITY_SUPPORT_EMAIL}" } + ] +} diff --git a/data-template/nginx/app.conf b/data-template/nginx/app.conf new file mode 100644 index 0000000..6a9b25a --- /dev/null +++ b/data-template/nginx/app.conf @@ -0,0 +1,112 @@ +# taken from https://element-hq.github.io/synapse/latest/reverse_proxy.html +# mixed with https://github.com/wmnnd/nginx-certbot/tree/master/data/nginx + +server { + server_name example.com; + server_tokens off; + + listen 80; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + server_name element.example.com; + server_tokens off; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://element-web:8080; + } +} + +server { + server_name call.example.com; + server_tokens off; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://element-call:8082; + } +} + +server { + server_name auth.example.com; + server_tokens off; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + location / { + proxy_pass http://auth:8083; + } +} + +server { + server_name matrix.example.com; + server_tokens off; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + # For the federation port + listen 8448 ssl default_server; + listen [::]:8448 ssl default_server; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # pass auth to MAS + location ~ ^/_matrix/client/(.*)/(login|logout|refresh) { proxy_pass http://auth:8083 } + + # use the generic worker as a synchrotron: + # taken from https://element-hq.github.io/synapse/latest/workers.html#synapseappgeneric_worker + location ~ ^/_matrix/client/(r0|v3)/sync$ { proxy_pass http://synapse-generic-worker-1:8081 } + location ~ ^/_matrix/client/(api/v1|r0|v3)/events$ { proxy_pass http://synapse-generic-worker-1:8081 } + location ~ ^/_matrix/client/(api/v1|r0|v3)/initialSync$ { proxy_pass http://synapse-generic-worker-1:8081 } + location ~ ^/_matrix/client/(api/v1|r0|v3)/rooms/[^/]+/initialSync$ { proxy_pass http://synapse-generic-worker-1:8081 } + + location / { + proxy_pass http://synapse:8008; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + + # Nginx by default only allows file uploads up to 1M in size + # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml + client_max_body_size 50M; + } + + location /.well-known {} + + # Synapse responses may be chunked, which is an HTTP/1.1 feature. + proxy_http_version 1.1; +} + diff --git a/data-template/postgres/create-multiple-postgresql-databases.sh b/data-template/postgres/create-multiple-postgresql-databases.sh new file mode 100755 index 0000000..348f66a --- /dev/null +++ b/data-template/postgres/create-multiple-postgresql-databases.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# from https://github.com/mrts/docker-postgresql-multiple-databases + +set -e +set -u + +function create_user_and_database() { + local database=$1 + echo " Creating user and database '$database'" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL + CREATE USER $database; + CREATE DATABASE $database; + GRANT ALL PRIVILEGES ON DATABASE $database TO $database; +EOSQL +} + +if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_user_and_database $db + done + echo "Multiple databases created" +fi \ No newline at end of file diff --git a/data-template/synapse/homeserver.yaml b/data-template/synapse/homeserver.yaml new file mode 100644 index 0000000..1b52843 --- /dev/null +++ b/data-template/synapse/homeserver.yaml @@ -0,0 +1,98 @@ +${CONFIG_HEADER} + +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html +# +# For more information on how to configure Synapse, including a complete accounting of +# each option, go to docs/usage/configuration/config_documentation.md or +# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html +server_name: ${DOMAIN} +pid_file: /data/homeserver.pid +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + resources: + - names: [client, federation] + compress: false + - port: 9093 + tls: false + type: http + resources: + - names: [replication] + +database: + name: psycopg2 + args: + user: matrix + password: '${SECRETS_POSTGRES_PASSWORD}' + host: postgres + database: synapse + +log_config: "/data/log.config" +media_store_path: /data/media_store +registration_shared_secret: '${SECRETS_SYNAPSE_REGISTRATION_SHARED_SECRET}' +report_stats: false +macaroon_secret_key: '${SECRETS_SYNAPSE_MACAROON_SECRET_KEY}' +form_secret: '${SECRETS_SYNAPSE_FORM_SECRET}' +signing_key_path: "/run/secrets/synapse_signing_key" +trusted_key_servers: + - server_name: "matrix.org" + +# please keep config above this point as close as possible to the original generated config +# so that upstream generated config changes can be detected + +send_federation: false +federation_sender_instances: + - synapse-federation-sender-1 + +instance_map: + main: + host: 'synapse' + port: 9093 + +redis: + enabled: true + host: redis + port: 6379 + +email: + smtp_host: mailhog + smtp_port: 1025 + enable_tls: false + notif_from: "Your %(app)s homeserver <${MAIL_NOTIF_FROM_ADDRESS}>" + app_name: Matrix + enable_notifs: true + notif_for_new_users: false + client_base_url: https://${ELEMENT_WEB_FQDN} + validation_token_lifetime: 15m + invite_client_location: https://${ELEMENT_WEB_FQDN} + subjects: + message_from_person_in_room: "[%(app)s] You have a message on %(app)s from %(person)s in the %(room)s room..." + message_from_person: "[%(app)s] You have a message on %(app)s from %(person)s..." + messages_from_person: "[%(app)s] You have messages on %(app)s from %(person)s..." + messages_in_room: "[%(app)s] You have messages on %(app)s in the %(room)s room..." + messages_in_room_and_others: "[%(app)s] You have messages on %(app)s in the %(room)s room and others..." + messages_from_person_and_others: "[%(app)s] You have messages on %(app)s from %(person)s and others..." + invite_from_person_to_room: "[%(app)s] %(person)s has invited you to join the %(room)s room on %(app)s..." + invite_from_person: "[%(app)s] %(person)s has invited you to chat on %(app)s..." + password_reset: "[%(server_name)s] Password reset" + email_validation: "[%(server_name)s] Validate your email" + +experimental_features: + msc3861: # OIDC + enabled: true + issuer: http://localhost:8080/ + client_id: ${MAS_CLIENT_ID} + client_auth_method: client_secret_basic + client_secret: '${SECRETS_MAS_CLIENT_SECRET}' + admin_token: '${SECRETS_MAS_MATRIX_SECRET}' + account_management_url: "https://${MAS_FQDN}/account" + +# vim:ft=yaml \ No newline at end of file diff --git a/data-template/synapse/log.config b/data-template/synapse/log.config new file mode 100644 index 0000000..fd36ca7 --- /dev/null +++ b/data-template/synapse/log.config @@ -0,0 +1,75 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema +# [2]: https://element-hq.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + file: + class: logging.handlers.TimedRotatingFileHandler + formatter: precise + filename: /data/homeserver.log + when: midnight + backupCount: 3 # Does not include the current log file. + encoding: utf8 + + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. + buffer: + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + target: file + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better + # performance, at the expensive of it taking longer for log lines to + # be written to disk. + # This parameter is required. + capacity: 10 + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 + + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. + # + handlers: [console] + +disable_existing_loggers: false diff --git a/data-template/synapse/workers/synapse-federation-sender-1.yaml b/data-template/synapse/workers/synapse-federation-sender-1.yaml new file mode 100644 index 0000000..2148660 --- /dev/null +++ b/data-template/synapse/workers/synapse-federation-sender-1.yaml @@ -0,0 +1,4 @@ +worker_app: synapse.app.federation_sender +worker_name: synapse-federation-sender-1 + +worker_log_config: /data/log.config diff --git a/data-template/synapse/workers/synapse-generic-worker-1.yaml b/data-template/synapse/workers/synapse-generic-worker-1.yaml new file mode 100644 index 0000000..6f60e21 --- /dev/null +++ b/data-template/synapse/workers/synapse-generic-worker-1.yaml @@ -0,0 +1,11 @@ +worker_app: synapse.app.generic_worker +worker_name: synapse-generic-worker-1 + +worker_listeners: + - type: http + port: 8081 + x_forwarded: true + resources: + - names: [client, federation] + +worker_log_config: /data/log.config diff --git a/init/Dockerfile b/init/Dockerfile new file mode 100644 index 0000000..e741a7f --- /dev/null +++ b/init/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:latest + +# TODO: check this doesn't reinstall yq on every launch and use a builder if necessary +RUN apk update && apk add yq bash envsubst diff --git a/init/generate-mas-secrets.sh b/init/generate-mas-secrets.sh new file mode 100755 index 0000000..f26c1f1 --- /dev/null +++ b/init/generate-mas-secrets.sh @@ -0,0 +1,14 @@ +#!/usr/bin/ash + +# a replacement entrypoint script for the MAS docker image which generates default config & secrets if needed. +# N.B. NOT USED CURRENTLY AS THE MAS IMAGE HAS NO SHELL + + +if [[ -f /data/config.yaml ]] +then + echo "MAS config found - not generating default" + exit 0 +fi + +echo "MAS config not found - generating default for secrets" +exec mas-cli config generate -o /data/config.yaml.default \ No newline at end of file diff --git a/init/generate-synapse-secrets.sh b/init/generate-synapse-secrets.sh new file mode 100755 index 0000000..10161d7 --- /dev/null +++ b/init/generate-synapse-secrets.sh @@ -0,0 +1,12 @@ +#!/usr/bin/bash + +# a replacement entrypoint script for the synapse docker image which generates default config & secrets if needed. + +if [[ -f ${SYNAPSE_CONFIG_PATH} ]] +then + echo "Synapse config found - not generating default" + exit 0 +fi + +echo "Synapse config not found - generating default for secrets" +exec /start.py generate \ No newline at end of file diff --git a/init/init-letsencrypt.sh b/init/init-letsencrypt.sh new file mode 100755 index 0000000..d3992e0 --- /dev/null +++ b/init/init-letsencrypt.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# taken from https://raw.githubusercontent.com/wmnnd/nginx-certbot/refs/heads/master/init-letsencrypt.sh + +if ! [ -x "$(command -v docker-compose)" ]; then + echo 'Error: docker-compose is not installed.' >&2 + exit 1 +fi + +. .env +domains=($DOMAIN $HOMESERVER_FQDN $ELEMENT_FQDN $CALL_FQDN $MAS_FQDN) +rsa_key_size=4096 +data_path="./data/certbot" +email="" # Adding a valid address is strongly recommended +staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits + +if [ -d "$data_path" ]; then + read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision + if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then + exit + fi +fi + + +if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then + echo "### Downloading recommended TLS parameters ..." + mkdir -p "$data_path/conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" + curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" + echo +fi + +echo "### Creating dummy certificate for $domains ..." +path="/etc/letsencrypt/live/$domains" +mkdir -p "$data_path/conf/live/$domains" +docker-compose run --rm --entrypoint "\ + openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\ + -keyout '$path/privkey.pem' \ + -out '$path/fullchain.pem' \ + -subj '/CN=localhost'" certbot +echo + + +echo "### Starting nginx ..." +docker-compose up --force-recreate -d nginx +echo + +echo "### Deleting dummy certificate for $domains ..." +docker-compose run --rm --entrypoint "\ + rm -Rf /etc/letsencrypt/live/$domains && \ + rm -Rf /etc/letsencrypt/archive/$domains && \ + rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot +echo + + +echo "### Requesting Let's Encrypt certificate for $domains ..." +#Join $domains to -d args +domain_args="" +for domain in "${domains[@]}"; do + domain_args="$domain_args -d $domain" +done + +# Select appropriate email arg +case "$email" in + "") email_arg="--register-unsafely-without-email" ;; + *) email_arg="--email $email" ;; +esac + +# Enable staging mode if needed +if [ $staging != "0" ]; then staging_arg="--staging"; fi + +docker-compose run --rm --entrypoint "\ + certbot certonly --webroot -w /var/www/certbot \ + $staging_arg \ + $email_arg \ + $domain_args \ + --rsa-key-size $rsa_key_size \ + --agree-tos \ + --force-renewal" certbot +echo + +echo "### Reloading nginx ..." +docker-compose exec nginx nginx -s reload \ No newline at end of file diff --git a/init/init.sh b/init/init.sh new file mode 100755 index 0000000..da8cb65 --- /dev/null +++ b/init/init.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e +#set -x + +# basic script to generate templated config for our various docker images. +# it runs in its own alpine docker image to pull in yq as a dep, and to let the whole thing be managed by docker-compose. + +# by this point, synapse & mas should generated default config files & secrets +# via generate-synapse-secrets.sh and generate-mas-secrets.sh + +if [[ ! -f /secrets/synapse/${DOMAIN}.signing.key ]] # TODO: check for existence of other secrets? +then + # extract synapse secrets from the config and move them into ./secrets + echo "Extracting generated synapse secrets..." + mkdir -p /secrets/synapse + for secret in registration_shared_secret macaroon_secret_key form_secret + do + yq .$secret /data/synapse/homeserver.yaml.default > /secrets/synapse/$secret + done + # ...and files too, just to keep all our secrets in one place + mv /data/synapse/${DOMAIN}.signing.key /secrets/synapse +fi + +if [[ ! -f /secrets/mas/secrets ]] # TODO: check for existence of other secrets? +then + echo "Extracting generated MAS secrets..." + mkdir -p /secrets/mas + # extract MAS secrets from the config and move them into ./secrets + for secret in matrix.secret + do + yq .$secret /data/mas/config.yaml.default > /secrets/mas/$secret + done + yq '(.secrets) as $s + ireduce({}; setpath($s | path; $s))' /data/mas/config.yaml.default > /secrets/mas/secrets + head -c16 /dev/urandom | base64 | tr -d '=' > /secrets/mas/client.secret +fi + +if [[ ! -f /secrets/postgres/postgres_password ]] +then + mkdir -p /secrets/postgres + head -c16 /dev/urandom | base64 | tr -d '=' > /secrets/postgres/postgres_password +fi + +# TODO: compare the default generated config with our templates to see if our templates are stale +# we'd have to strip out the secrets from the generated configs to be able to diff them sensibly + +# now we have our secrets extracted from the default configs, overwrite the configs with our templates + +# for simplicity, we just use envsubst for now rather than ansible+jinja or something. +template() { + dir=$1 + echo "Templating configs in $dir" + for file in `find $dir -type f` + do + mkdir -p `dirname ${file/-template/}` + envsubst < $file > ${file/-template/} + done +} + +export CONFIG_HEADER="# WARNING: This file is autogenerated by element-quick-start from templates" +( + export SECRETS_SYNAPSE_REGISTRATION_SHARED_SECRET=$(