diff --git a/containers/matrix/docker-compose.yml b/containers/matrix/docker-compose.yml new file mode 100644 index 0000000..5213989 --- /dev/null +++ b/containers/matrix/docker-compose.yml @@ -0,0 +1,116 @@ +networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + external: true + +services: + # Synapse — the homeserver. Everything that matters lives in + # ./data/homeserver.yaml, written by matrix_install_post_compose from + # resources/homeserver.yaml; Synapse takes no meaningful configuration from + # the environment, so there is little to see here. + matrix-synapse: #LIBREPORTAL|SERVICE_TAG_1|matrix-synapse + container_name: matrix-synapse + image: matrixdotorg/synapse:v1.158.0 #LIBREPORTAL|MATRIX_VERSION_TAG|v1.158.0 + # Synapse writes the media store, and under rootless Docker the image's + # own uid maps to a host sub-UID that owns nothing. Same fix as the + # other apps: run as whoever owns the bind mounts. + user: "USER_DATA" #LIBREPORTAL|USER_TAG|USER_DATA + restart: unless-stopped + # GLUETUN_OFF_BEGIN + ports: + - "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1 + # GLUETUN_OFF_END + volumes: + - ./data:/data + environment: + - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA + - SYNAPSE_CONFIG_PATH=/data/homeserver.yaml + depends_on: + - matrix-postgres + labels: + libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA + libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA + libreportal.backup.db: "postgres:matrix-postgres:postgres:" + # The media store holds every uploaded file and avatar. It is not in + # the database, so without this it would not come back on restore. + libreportal.backup.files: "matrix-synapse:/data/media_store:data/media_store" + traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_1_BEGIN + traefik.http.routers.matrix-synapse.entrypoints: web,websecure + traefik.http.routers.matrix-synapse.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1 + traefik.http.routers.matrix-synapse.tls: true + traefik.http.routers.matrix-synapse.tls.certresolver: production + traefik.http.services.matrix-synapse.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1 + traefik.http.routers.matrix-synapse.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1 + # TRAEFIK_PORT_1_END + traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + healthcheck: + disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA + # GLUETUN_OFF_BEGIN + networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1 + # GLUETUN_OFF_END + # GLUETUN_ON_BEGIN + # network_mode: "container:gluetun-service" + # GLUETUN_ON_END + + # Element web — a static single-page app served by nginx. It talks to + # Synapse from the user's browser, not server-side, so it needs no link to + # the homeserver container beyond the base_url baked into config.json. + matrix-element: #LIBREPORTAL|SERVICE_TAG_2|matrix-element + container_name: matrix-element + image: vectorim/element-web:v1.12.25 #LIBREPORTAL|MATRIX_ELEMENT_VERSION_TAG|v1.12.25 + restart: unless-stopped + # GLUETUN_OFF_BEGIN + ports: + - "PORTS_DATA_2" #LIBREPORTAL|PORTS_TAG_2|PORTS_DATA_2 + # GLUETUN_OFF_END + volumes: + - ./element/config.json:/app/config.json:ro + environment: + - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA + labels: + libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA + libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA + traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA + # TRAEFIK_PORT_2_BEGIN + traefik.http.routers.matrix-element.entrypoints: web,websecure + traefik.http.routers.matrix-element.rule: Host(`DOMAINSUBNAME_DATA_2`) #LIBREPORTAL|DOMAINSUBNAME_TAG_2|DOMAINSUBNAME_DATA_2 + traefik.http.routers.matrix-element.tls: true + traefik.http.routers.matrix-element.tls.certresolver: production + traefik.http.services.matrix-element.loadbalancer.server.port: PORT_INTERNAL_DATA_2 #LIBREPORTAL|PORT_INTERNAL_TAG_2|PORT_INTERNAL_DATA_2 + traefik.http.routers.matrix-element.middlewares: MIDDLEWARE_DATA_2 #LIBREPORTAL|MIDDLEWARE_TAG_2|MIDDLEWARE_DATA_2 + # TRAEFIK_PORT_2_END + traefik.docker.network: DOCKER_NETWORK_DATA #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + healthcheck: + disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA + # GLUETUN_OFF_BEGIN + networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + ipv4_address: IP_DATA_2 #LIBREPORTAL|IP_TAG_2|IP_DATA_2 + # GLUETUN_OFF_END + # GLUETUN_ON_BEGIN + # network_mode: "container:gluetun-service" + # GLUETUN_ON_END + + # No `user:` override — the postgres entrypoint starts as root, chowns + # PGDATA and drops privileges, which works under rootless because + # container-root is the install user that owns the mount. + matrix-postgres: + image: postgres:15-alpine + container_name: matrix-postgres + restart: unless-stopped + environment: + - TZ=TIMEZONE_DATA #LIBREPORTAL|TIMEZONE_TAG|TIMEZONE_DATA + - POSTGRES_USER=synapse + - POSTGRES_PASSWORD=PASSWORD_DATA_1 #LIBREPORTAL|PASSWORD_TAG_1|PASSWORD_DATA_1 + - POSTGRES_DB=synapse + # Not optional. Synapse refuses to start against a database with any + # other collation or ctype — it needs deterministic byte ordering for + # its indexes, and a C.UTF-8 locale is the only thing that gives it. + - POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C + volumes: + - ./postgres:/var/lib/postgresql/data + networks: + DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA + ipv4_address: IP_DATA_3 #LIBREPORTAL|IP_TAG_3|IP_DATA_3 diff --git a/containers/matrix/matrix.config b/containers/matrix/matrix.config new file mode 100644 index 0000000..2abe9f9 --- /dev/null +++ b/containers/matrix/matrix.config @@ -0,0 +1,95 @@ +# +# ============================================================================= +# GENERAL CONFIGURATION +# ============================================================================= +# APP_NAME = name of application for use in scripts +# REQUIRES = comma-separated install prerequisites (see scripts/checks/requirements/check_app_install.sh) +# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is +# BACKUP = if true, include this application in backup operations +# UPDATE_TYPE = auto: new image builds are applied automatically (a recovery snapshot is taken first), manual: only when you press Update +# HEALTHCHECK = if true, default docker health checks for that container will be enabled +# AUTHELIA = if true, use Authelia authentication, if false turned off. +# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote +# ENABLE_REGISTRATION = if true, anyone who can reach the homeserver can create an account on it +# ADMIN_USERNAME = localpart of the first admin account created at install (the full ID becomes @:) +# ADMIN_PASSWORD = password for that first admin account +# MONITORING = if true, export this app's metrics to Prometheus + Grafana (needs both apps installed) +# +CFG_MATRIX_APP_NAME=matrix +# A domain and Traefik are hard requirements, not conveniences. Synapse bakes +# server_name into every event and user ID it has ever signed, and it CANNOT be +# changed afterwards without throwing the database away — so the homeserver must +# know its real public name at install time, over real TLS. +CFG_MATRIX_REQUIRES="domain,traefik" +CFG_MATRIX_BACKUP=true +CFG_MATRIX_BACKUP_STRATEGY=auto +CFG_MATRIX_UPDATE_TYPE=auto +CFG_MATRIX_COMPOSE_FILE=default +CFG_MATRIX_HEALTHCHECK=true +# Must stay false. Authelia's forward-auth would sit in front of /_matrix, which +# is the API every Matrix client and every federating server speaks — they +# authenticate with Matrix access tokens and cannot follow an Authelia redirect, +# so turning this on breaks all clients and federation at once. Synapse can do +# real SSO against Authelia instead, via the OIDC block in +# resources/homeserver.yaml. +CFG_MATRIX_AUTHELIA=false +CFG_MATRIX_HEADSCALE=false +CFG_MATRIX_ENABLE_REGISTRATION=false +CFG_MATRIX_ADMIN_USERNAME=admin +CFG_MATRIX_ADMIN_PASSWORD=RANDOMIZEDPASSWORD1 +CFG_MATRIX_MONITORING=false +# +# ============================================================================= +# METADATA +# ============================================================================= +# CATEGORY = application category for grouping +# TITLE = display name for the application +# DESCRIPTION = short description of the application +# LONG_DESCRIPTION = detailed description of the application +# URL = source repository or documentation URL +# ACTIONS = available actions for this application +# REQUIRES_SERVICE = name of another LibrePortal app that must be installed before this one can be configured +# +CFG_MATRIX_CATEGORY="communication" +CFG_MATRIX_TITLE="Matrix" +CFG_MATRIX_DESCRIPTION="Federated Chat" +CFG_MATRIX_LONG_DESCRIPTION="Matrix is the open federated chat protocol — Spaces and rooms cover what Discord servers and channels do, with end-to-end encryption and bridges to Discord, IRC and Slack. This installs the Synapse homeserver on Postgres plus the Element web client, on their own subdomains. Because it federates, accounts on this server can talk to every other Matrix server without either side giving up control" +CFG_MATRIX_URL="https://github.com/element-hq/synapse" +CFG_MATRIX_ACTIONS="configure|install|restart|shutdown|uninstall" +CFG_MATRIX_REQUIRES_SERVICE=traefik +# +# ============================================================================= +# NETWORK CONFIGURATION +# ============================================================================= +# DOMAIN = number of domain from the general config, useful when using multiple domains +# WHITELIST = if true only allow whitelisted ips (see general config), if false allow all +# +CFG_MATRIX_DOMAIN=1 +CFG_MATRIX_WHITELIST=false +CFG_MATRIX_NETWORK=default +# +# ============================================================================= +# PORT CONFIGURATION +# ============================================================================= +# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description +# - app: application name +# - name: service identifier (webui, dns, ssh, etc.) +# - external:internal: port mapping (external can be 'random' for auto-allocation) +# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running) +# - protocol: 'tcp' or 'udp' +# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true) +# - traefik: if true, Traefik handles this port (reverse proxy) +# - webui: if true, this port serves the main web interface +# - description: human-readable description of the service +# +# Two hosts, on purpose. Port 1 is the homeserver API on matrix., which +# becomes server_name — so user IDs read @alice:matrix.. Port 2 is the +# Element web client on element.. Keeping them apart means Synapse can +# answer /.well-known/matrix/server for itself and federation needs no +# delegation from the apex domain, which this app has no way to configure. +# +# Port 1 must NOT be marked login=true: /_matrix is the client and federation +# API and basic-auth in front of it locks out every client and every peer. +# +CFG_MATRIX_PORT_1="matrix-synapse|homeserver|random:8008|public|tcp|false|true|false|Matrix Homeserver (client + federation API)||matrix" +CFG_MATRIX_PORT_2="matrix-element|webui|random:80|public|tcp|false|true|true|Element Web Interface||element" diff --git a/containers/matrix/matrix.svg b/containers/matrix/matrix.svg new file mode 100644 index 0000000..98105c9 --- /dev/null +++ b/containers/matrix/matrix.svg @@ -0,0 +1 @@ + diff --git a/containers/matrix/resources/element-config.json b/containers/matrix/resources/element-config.json new file mode 100644 index 0000000..efe129b --- /dev/null +++ b/containers/matrix/resources/element-config.json @@ -0,0 +1,24 @@ +{ + "default_server_config": { + "m.homeserver": { + "base_url": "https://ELEMENT_HOMESERVER_PLACEHOLDER", + "server_name": "ELEMENT_HOMESERVER_PLACEHOLDER" + } + }, + "brand": "Element", + "disable_custom_urls": false, + "disable_guests": true, + "disable_login_language_selector": false, + "disable_3pid_login": false, + "default_country_code": "GB", + "show_labs_settings": true, + "room_directory": { + "servers": ["ELEMENT_HOMESERVER_PLACEHOLDER", "matrix.org"] + }, + "setting_defaults": { + "breadcrumbs": true + }, + "jitsi": { + "preferredDomain": "meet.element.io" + } +} diff --git a/containers/matrix/resources/homeserver.yaml b/containers/matrix/resources/homeserver.yaml new file mode 100644 index 0000000..b707752 --- /dev/null +++ b/containers/matrix/resources/homeserver.yaml @@ -0,0 +1,103 @@ +# Synapse homeserver configuration. +# +# Copied to /data/homeserver.yaml by matrix_install_post_compose, which +# substitutes the *_PLACEHOLDER values below. Edit the deployed copy, not this +# template — this one is only read at install time. +# +# Restart the container after editing: docker restart matrix-synapse + +# server_name is permanent. It is signed into every event this server has ever +# sent and forms the second half of every user ID (@alice:). +# Changing it later does not migrate anything — it orphans the whole database. +server_name: "SYNAPSE_SERVER_NAME_PLACEHOLDER" +public_baseurl: "https://SYNAPSE_SERVER_NAME_PLACEHOLDER/" +pid_file: /data/homeserver.pid + +# Serve /.well-known/matrix/server ourselves, advertising port 443. Federation +# otherwise defaults to port 8448 on server_name, which Traefik is not +# listening on. Because server_name is the same host Traefik already terminates +# TLS for, this is all the delegation that is needed — nothing has to be +# published at the apex domain. +serve_server_wellknown: true + +listeners: + # Port 8008 is the container-internal port and is deliberately hardcoded: it + # is what CFG_MATRIX_PORT_1 declares as the internal half of its mapping, and + # what the Traefik service label points at. Change one and you must change all + # three. + - port: 8008 + tls: false + type: http + # Traefik terminates TLS and proxies onward, so the source address Synapse + # sees is Traefik's. Without this, rate limiting and the audit log would + # attribute every request in the world to a single internal IP. + x_forwarded: true + bind_addresses: ['0.0.0.0'] + resources: + - names: [client, federation] + compress: false + +database: + name: psycopg2 + args: + user: synapse + password: "SYNAPSE_DB_PASSWORD_PLACEHOLDER" + dbname: synapse + host: matrix-postgres + port: 5432 + cp_min: 5 + cp_max: 10 + +log_config: "/data/log.config" +media_store_path: /data/media_store +signing_key_path: "/data/signing.key" + +# Uploads. Raise max_upload_size if your users share video; remember the +# reverse proxy has its own limit too. +max_upload_size: 50M + +# Open registration is off by default: a reachable homeserver with registration +# enabled will be found and used for spam within days. The install creates one +# admin account for you; invite everyone else, or turn this on deliberately via +# CFG_MATRIX_ENABLE_REGISTRATION and re-run the install. +enable_registration: SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER +enable_registration_without_verification: SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER + +registration_shared_secret: "SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER" +macaroon_secret_key: "SYNAPSE_MACAROON_SECRET_PLACEHOLDER" +form_secret: "SYNAPSE_FORM_SECRET_PLACEHOLDER" + +report_stats: false +suppress_key_server_warning: true + +# Which servers to fetch other servers' signing keys from. matrix.org is the +# conventional default; federation still works if it is unreachable, just more +# slowly on first contact with a new server. +trusted_key_servers: + - server_name: "matrix.org" + +# --------------------------------------------------------------------------- +# Single sign-on against Authelia (optional) +# --------------------------------------------------------------------------- +# CFG_MATRIX_AUTHELIA must stay false — that switch puts Authelia's forward-auth +# in front of /_matrix, which breaks every client. Real SSO is done here +# instead, with Synapse as an OIDC client of Authelia. +# +# Register the client in Authelia's configuration.yml first, then uncomment and +# fill in the block below and restart the container. +# +# oidc_providers: +# - idp_id: authelia +# idp_name: "Authelia" +# issuer: "https://auth." +# client_id: "synapse" +# client_secret: "" +# scopes: ["openid", "profile", "email"] +# user_mapping_provider: +# config: +# localpart_template: "{{ user.preferred_username }}" +# display_name_template: "{{ user.name }}" +# email_template: "{{ user.email }}" +# +# The redirect URI to register in Authelia is: +# https://SYNAPSE_SERVER_NAME_PLACEHOLDER/_synapse/client/oidc/callback diff --git a/containers/matrix/resources/log.config b/containers/matrix/resources/log.config new file mode 100644 index 0000000..15209ce --- /dev/null +++ b/containers/matrix/resources/log.config @@ -0,0 +1,32 @@ +# Synapse logging configuration. +# +# Logs go to stdout only, so `docker logs matrix-synapse` and the LibrePortal +# log viewer both see them, and nothing accumulates inside the container that +# the host does not rotate. + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # Set to INFO to log every database query — useful when chasing a slow + # server, far too noisy for normal running. + level: WARNING + + # Very chatty at INFO, and rarely what you are looking for. + synapse.access.http.8008: + level: WARNING + +root: + level: INFO + handlers: [console] + +disable_existing_loggers: false diff --git a/containers/matrix/scripts/matrix_install_hooks.sh b/containers/matrix/scripts/matrix_install_hooks.sh new file mode 100644 index 0000000..78c4b34 --- /dev/null +++ b/containers/matrix/scripts/matrix_install_hooks.sh @@ -0,0 +1,223 @@ +#!/bin/bash + +# Matrix (Synapse + Element) install hooks. +# +# Synapse takes essentially no configuration from the environment — it reads +# homeserver.yaml and nothing else — so the real install work is done here: +# generate the signing key, write homeserver.yaml from the template with the +# server name and secrets filled in, write Element's config.json, and create the +# first admin account once the homeserver is answering. + +matrix_install_pre() +{ + local app_name="$1" + if ! appInstallCheckRequirements "$app_name" "$CFG_MATRIX_REQUIRES"; then + matrix=n + return 1 + fi +} + +# The homeserver's public host, read back out of the deployed compose after tag +# substitution has run. DOMAINSUBNAME_TAG_1 belongs to CFG_MATRIX_PORT_1 (the +# Synapse router), so this is the host that becomes server_name — deliberately +# not $host_setup, which for this two-host app points at Element instead. +_matrixServerName() +{ + local app_name="$1" + tagsManagerGetTagContent "$containers_dir$app_name/docker-compose.yml" "DOMAINSUBNAME_TAG_1" +} + +matrix_install_post_compose() +{ + local app_name="$1" + local app_dir="$containers_dir$app_name" + local data_dir="$app_dir/data" + + ((menu_number++)) + echo "" + echo "---- $menu_number. Generating the Synapse homeserver configuration" + echo "" + + local server_name + server_name=$(_matrixServerName "$app_name") + if [[ -z "$server_name" ]]; then + isError "Could not determine the homeserver name from the compose file — aborting Synapse configuration." + isNotice "Check that CFG_MATRIX_PORT_1 is public and Traefik-managed, then reinstall." + return 1 + fi + + # Must match the password the compose handed to Postgres. Reading it back + # from the deployed compose is the only way to stay in step: the value is + # generated per-install by the password tag processor. + local db_password + db_password=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "PASSWORD_TAG_1") + if [[ -z "$db_password" || "$db_password" == "PASSWORD_DATA_1" ]]; then + isError "Database password was not generated in the compose file — aborting Synapse configuration." + return 1 + fi + + local result + result=$(createFolders "loud" "$docker_install_user" "$data_dir" "$app_dir/element") + checkSuccess "Creating $app_name data folders" + + # Synapse signs every federated event with this key, and a peer that has + # seen one key will reject events signed by a different one. So: generate it + # exactly once, and never regenerate it over an existing install. + # + # Upstream's `generate` command is used rather than hand-rolling the key + # file, because the format encodes a key ID that other servers cache. It + # also emits a homeserver.yaml and a log config, which we throw away in + # favour of the templates below. + if [[ ! -s "$data_dir/signing.key" ]]; then + local synapse_image + synapse_image=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_VERSION_TAG") + synapse_image="matrixdotorg/synapse:${synapse_image:-latest}" + + result=$(runFileOp docker run --rm \ + -e SYNAPSE_SERVER_NAME="$server_name" \ + -e SYNAPSE_REPORT_STATS=no \ + -v "$data_dir":/data \ + "$synapse_image" generate 2>&1) + checkSuccess "Generating the Synapse signing key with $synapse_image" + + # `generate` names the key after the server; homeserver.yaml expects it + # at a fixed path so the file does not have to be renamed if the app is + # ever restored under a different name. + if [[ -f "$data_dir/$server_name.signing.key" ]]; then + result=$(runFileOp mv "$data_dir/$server_name.signing.key" "$data_dir/signing.key") + checkSuccess "Storing the signing key at data/signing.key" + fi + # Ours replace both of these. + result=$(runFileOp rm -f "$data_dir/homeserver.yaml" "$data_dir/$server_name.log.config") + checkSuccess "Discarding the generated config in favour of the LibrePortal template" + else + isNotice "An existing signing key was found — keeping it (regenerating would break federation)." + fi + + if [[ ! -s "$data_dir/signing.key" ]]; then + isError "No signing key was produced — Synapse will not start. Check that the image could be pulled." + return 1 + fi + + result=$(copyResource "$app_name" "homeserver.yaml" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) + checkSuccess "Copying homeserver.yaml to $data_dir" + + result=$(copyResource "$app_name" "log.config" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) + checkSuccess "Copying log.config to $data_dir" + + local homeserver_file="$data_dir/homeserver.yaml" + + # Three independent secrets, each generated fresh. registration_shared_secret + # can mint an account on this server, so it is as sensitive as an admin + # password — it is why homeserver.yaml is chmod 600 below. + local registration_secret macaroon_secret form_secret + registration_secret=$(openssl rand -hex 32) + macaroon_secret=$(openssl rand -hex 32) + form_secret=$(openssl rand -hex 32) + + local enable_registration="false" + [[ "$CFG_MATRIX_ENABLE_REGISTRATION" == "true" ]] && enable_registration="true" + + runFileOp sed -i "s|SYNAPSE_SERVER_NAME_PLACEHOLDER|$server_name|g" "$homeserver_file" + runFileOp sed -i "s|SYNAPSE_DB_PASSWORD_PLACEHOLDER|$db_password|g" "$homeserver_file" + runFileOp sed -i "s|SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER|$registration_secret|g" "$homeserver_file" + runFileOp sed -i "s|SYNAPSE_MACAROON_SECRET_PLACEHOLDER|$macaroon_secret|g" "$homeserver_file" + runFileOp sed -i "s|SYNAPSE_FORM_SECRET_PLACEHOLDER|$form_secret|g" "$homeserver_file" + runFileOp sed -i "s|SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER|$enable_registration|g" "$homeserver_file" + checkSuccess "Writing homeserver.yaml (server_name=$server_name registration=$enable_registration)" + + runFileOp chmod 600 "$homeserver_file" "$data_dir/signing.key" + runFileOp chown -R "$docker_install_user":"$docker_install_user" "$data_dir" + checkSuccess "Restricting permissions on the Synapse secrets" + + # Element is a static bundle; config.json is the only thing that makes it + # point at this homeserver rather than matrix.org. + result=$(copyResource "$app_name" "element-config.json" "element" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) + checkSuccess "Copying Element configuration to $app_dir/element" + + result=$(runFileOp mv "$app_dir/element/element-config.json" "$app_dir/element/config.json") + checkSuccess "Renaming Element configuration to config.json" + + runFileOp sed -i "s|ELEMENT_HOMESERVER_PLACEHOLDER|$server_name|g" "$app_dir/element/config.json" + runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir/element" + checkSuccess "Pointing Element at https://$server_name" +} + +matrix_install_post_start() +{ + local app_name="$1" + + ((menu_number++)) + echo "" + echo "---- $menu_number. Creating the first Matrix admin account" + echo "" + + # Synapse runs its database migrations on first boot, which on an empty + # Postgres takes appreciably longer than the container takes to start. + # /health answers only once it is actually serving. + # + # Probed with python rather than curl: the Synapse image is debian-slim with + # no curl or wget in it, but python is what Synapse itself runs on, so it is + # always there. + local attempts=0 + while ((attempts < 60)); do + if runFileOp docker exec matrix-synapse python -c \ + "import urllib.request; urllib.request.urlopen('http://localhost:8008/health', timeout=5)" >/dev/null 2>&1; then + break + fi + sleep 2 + ((attempts++)) + done + + if ((attempts >= 60)); then + isError "Synapse did not become ready in time — no admin account was created." + isNotice "Check 'docker logs matrix-synapse'. Once it is up, create the account with:" + isNotice " docker exec -it matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008" + return 0 + fi + + local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" + local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD}" + if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then + isNotice "No admin password is set in matrix.config — skipping admin account creation." + return 0 + fi + + # Idempotent in practice: on a reinstall over existing data the account + # already exists and register_new_matrix_user fails with "User ID already + # taken", which is not worth failing the install over. + local result + result=$(runFileOp docker exec matrix-synapse register_new_matrix_user \ + -u "$admin_user" -p "$admin_pass" -a \ + -c /data/homeserver.yaml http://localhost:8008 2>&1) + if [[ "$result" == *"already taken"* ]]; then + isNotice "Matrix admin '$admin_user' already exists — leaving the existing account alone." + else + checkSuccess "Creating Matrix admin account '$admin_user'" + fi +} + +matrix_install_post() +{ + local app_name="$1" + local server_name + server_name=$(_matrixServerName "$app_name") + local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" + + echo "" + isNotice "Matrix homeserver:" + echo "" + echo " Server name : ${server_name}" + echo " Your user ID : @${admin_user}:${server_name}" + echo " Password : ${CFG_MATRIX_ADMIN_PASSWORD}" + echo "" + echo " Sign in through the Element web interface, or any Matrix client" + echo " (Element mobile/desktop, FluffyChat, Nheko) using the server name" + echo " above." + echo "" + echo " Registration is ${CFG_MATRIX_ENABLE_REGISTRATION:-false}. To invite" + echo " others while it stays closed, create their accounts with:" + echo " docker exec -it matrix-synapse register_new_matrix_user \\" + echo " -c /data/homeserver.yaml http://localhost:8008" + echo "" +}