diff options
Diffstat (limited to 'nixos/modules/services/matrix')
-rw-r--r-- | nixos/modules/services/matrix/matrix-synapse-log_config.yaml | 25 | ||||
-rw-r--r-- | nixos/modules/services/matrix/matrix-synapse.nix | 773 | ||||
-rw-r--r-- | nixos/modules/services/matrix/matrix-synapse.xml | 231 | ||||
-rw-r--r-- | nixos/modules/services/matrix/mjolnir.nix | 242 | ||||
-rw-r--r-- | nixos/modules/services/matrix/mjolnir.xml | 134 | ||||
-rw-r--r-- | nixos/modules/services/matrix/pantalaimon-options.nix | 70 | ||||
-rw-r--r-- | nixos/modules/services/matrix/pantalaimon.nix | 70 |
7 files changed, 1545 insertions, 0 deletions
diff --git a/nixos/modules/services/matrix/matrix-synapse-log_config.yaml b/nixos/modules/services/matrix/matrix-synapse-log_config.yaml new file mode 100644 index 00000000000..d85bdd1208f --- /dev/null +++ b/nixos/modules/services/matrix/matrix-synapse-log_config.yaml @@ -0,0 +1,25 @@ +version: 1 + +# In systemd's journal, loglevel is implicitly stored, so let's omit it +# from the message text. +formatters: + journal_fmt: + format: '%(name)s: [%(request)s] %(message)s' + +filters: + context: + (): synapse.util.logcontext.LoggingContextFilter + request: "" + +handlers: + journal: + class: systemd.journal.JournalHandler + formatter: journal_fmt + filters: [context] + SYSLOG_IDENTIFIER: synapse + +root: + level: INFO + handlers: [journal] + +disable_existing_loggers: False diff --git a/nixos/modules/services/matrix/matrix-synapse.nix b/nixos/modules/services/matrix/matrix-synapse.nix new file mode 100644 index 00000000000..c4d14dbd547 --- /dev/null +++ b/nixos/modules/services/matrix/matrix-synapse.nix @@ -0,0 +1,773 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + cfg = config.services.matrix-synapse; + format = pkgs.formats.yaml {}; + + # remove null values from the final configuration + finalSettings = lib.filterAttrsRecursive (_: v: v != null) cfg.settings; + configFile = format.generate "homeserver.yaml" finalSettings; + logConfigFile = format.generate "log_config.yaml" cfg.logConfig; + + pluginsEnv = cfg.package.python.buildEnv.override { + extraLibs = cfg.plugins; + }; + + usePostgresql = cfg.settings.database.name == "psycopg2"; + hasLocalPostgresDB = let args = cfg.settings.database.args; in + usePostgresql && (!(args ? host) || (elem args.host [ "localhost" "127.0.0.1" "::1" ])); + + registerNewMatrixUser = + let + isIpv6 = x: lib.length (lib.splitString ":" x) > 1; + listener = + lib.findFirst ( + listener: lib.any ( + resource: lib.any ( + name: name == "client" + ) resource.names + ) listener.resources + ) (lib.last cfg.settings.listeners) cfg.settings.listeners; + # FIXME: Handle cases with missing client listener properly, + # don't rely on lib.last, this will not work. + + # add a tail, so that without any bind_addresses we still have a useable address + bindAddress = head (listener.bind_addresses ++ [ "127.0.0.1" ]); + listenerProtocol = if listener.tls + then "https" + else "http"; + in + pkgs.writeShellScriptBin "matrix-synapse-register_new_matrix_user" '' + exec ${cfg.package}/bin/register_new_matrix_user \ + $@ \ + ${lib.concatMapStringsSep " " (x: "-c ${x}") ([ configFile ] ++ cfg.extraConfigFiles)} \ + "${listenerProtocol}://${ + if (isIpv6 bindAddress) then + "[${bindAddress}]" + else + "${bindAddress}" + }:${builtins.toString listener.port}/" + ''; +in { + + imports = [ + + (mkRemovedOptionModule [ "services" "matrix-synapse" "trusted_third_party_id_servers" ] '' + The `trusted_third_party_id_servers` option as been removed in `matrix-synapse` v1.4.0 + as the behavior is now obsolete. + '') + (mkRemovedOptionModule [ "services" "matrix-synapse" "create_local_database" ] '' + Database configuration must be done manually. An exemplary setup is demonstrated in + <nixpkgs/nixos/tests/matrix-synapse.nix> + '') + (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "") + (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] '' + You may add additional event types via + `services.matrix-synapse.room_prejoin_state.additional_event_types` and + disable the default events via + `services.matrix-synapse.room_prejoin_state.disable_default_event_types`. + '') + + # options that don't exist in synapse anymore + (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_host" ] "Use listener settings instead." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "bind_port" ] "Use listener settings instead." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "expire_access_tokens" ] "" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "no_tls" ] "It is no longer supported by synapse." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_dh_param_path" ] "It was removed from synapse." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "unsecure_port" ] "Use settings.listeners instead." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "user_creation_max_duration" ] "It is no longer supported by synapse." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "verbose" ] "Use a log config instead." ) + + # options that were moved into rfc42 style settigns + (mkRemovedOptionModule [ "services" "matrix-synapse" "app_service_config_files" ] "Use settings.app_service_config_Files instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "database_args" ] "Use settings.database.args instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "database_name" ] "Use settings.database.args.database instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "database_type" ] "Use settings.database.name instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "database_user" ] "Use settings.database.args.user instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "dynamic_thumbnails" ] "Use settings.dynamic_thumbnails instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_metrics" ] "Use settings.enable_metrics instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration" ] "Use settings.enable_registration instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "extraConfig" ] "Use settings instead." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "listeners" ] "Use settings.listeners instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "logConfig" ] "Use settings.log_config instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "max_image_pixels" ] "Use settings.max_image_pixels instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "max_upload_size" ] "Use settings.max_upload_size instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "presence" "enabled" ] "Use settings.presence.enabled instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "public_baseurl" ] "Use settings.public_baseurl instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "report_stats" ] "Use settings.report_stats instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "server_name" ] "Use settings.server_name instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "servers" ] "Use settings.trusted_key_servers instead." ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_certificate_path" ] "Use settings.tls_certificate_path instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "tls_private_key_path" ] "Use settings.tls_private_key_path instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_shared_secret" ] "Use settings.turn_shared_secret instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_uris" ] "Use settings.turn_uris instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "turn_user_lifetime" ] "Use settings.turn_user_lifetime instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_enabled" ] "Use settings.url_preview_enabled instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_blacklist" ] "Use settings.url_preview_ip_range_blacklist instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_ip_range_whitelist" ] "Use settings.url_preview_ip_range_whitelist instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "url_preview_url_blacklist" ] "Use settings.url_preview_url_blacklist instead" ) + + # options that are too specific to mention them explicitly in settings + (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "email" ] "Use settings.account_threepid_delegates.email instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "account_threepid_delegates" "msisdn" ] "Use settings.account_threepid_delegates.msisdn instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "allow_guest_access" ] "Use settings.allow_guest_access instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "bcrypt_rounds" ] "Use settings.bcrypt_rounds instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "enable_registration_captcha" ] "Use settings.enable_registration_captcha instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "event_cache_size" ] "Use settings.event_cache_size instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_concurrent" ] "Use settings.rc_federation.concurrent instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_reject_limit" ] "Use settings.rc_federation.reject_limit instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_delay" ] "Use settings.rc_federation.sleep_delay instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_sleep_limit" ] "Use settings.rc_federation.sleep_limit instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "federation_rc_window_size" ] "Use settings.rc_federation.window_size instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "key_refresh_interval" ] "Use settings.key_refresh_interval instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_burst_count" ] "Use settings.rc_messages.burst_count instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "rc_messages_per_second" ] "Use settings.rc_messages.per_second instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_private_key" ] "Use settings.recaptcha_private_key instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "recaptcha_public_key" ] "Use settings.recaptcha_public_key instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "redaction_retention_period" ] "Use settings.redaction_retention_period instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "additional_event_types" ] "Use settings.room_prejoin_state.additional_event_types instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "room_prejoin_state" "disable_default_event_types" ] "Use settings.room_prejoin-state.disable_default_event_types instead" ) + + # Options that should be passed via extraConfigFiles, so they are not persisted into the nix store + (mkRemovedOptionModule [ "services" "matrix-synapse" "macaroon_secret_key" ] "Pass this value via extraConfigFiles instead" ) + (mkRemovedOptionModule [ "services" "matrix-synapse" "registration_shared_secret" ] "Pass this value via extraConfigFiles instead" ) + + ]; + + options = { + services.matrix-synapse = { + enable = mkEnableOption "matrix.org synapse"; + + configFile = mkOption { + type = types.str; + readOnly = true; + description = '' + Path to the configuration file on the target system. Useful to configure e.g. workers + that also need this. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.matrix-synapse; + defaultText = literalExpression "pkgs.matrix-synapse"; + description = '' + Overridable attribute of the matrix synapse server package to use. + ''; + }; + + plugins = mkOption { + type = types.listOf types.package; + default = [ ]; + example = literalExpression '' + with config.services.matrix-synapse.package.plugins; [ + matrix-synapse-ldap3 + matrix-synapse-pam + ]; + ''; + description = '' + List of additional Matrix plugins to make available. + ''; + }; + + withJemalloc = mkOption { + type = types.bool; + default = false; + description = '' + Whether to preload jemalloc to reduce memory fragmentation and overall usage. + ''; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/matrix-synapse"; + description = '' + The directory where matrix-synapse stores its stateful data such as + certificates, media and uploads. + ''; + }; + + settings = mkOption { + default = {}; + description = '' + The primary synapse configuration. See the + <link xlink:href="https://github.com/matrix-org/synapse/blob/v${cfg.package.version}/docs/sample_config.yaml">sample configuration</link> + for possible values. + + Secrets should be passed in by using the <literal>extraConfigFiles</literal> option. + ''; + type = with types; submodule { + freeformType = format.type; + options = { + # This is a reduced set of popular options and defaults + # Do not add every available option here, they can be specified + # by the user at their own discretion. This is a freeform type! + + server_name = mkOption { + type = types.str; + example = "example.com"; + default = config.networking.hostName; + defaultText = literalExpression "config.networking.hostName"; + description = '' + The domain name of the server, with optional explicit port. + This is used by remote servers to look up the server address. + This is also the last part of your UserID. + + The server_name cannot be changed later so it is important to configure this correctly before you start Synapse. + ''; + }; + + enable_registration = mkOption { + type = types.bool; + default = false; + description = '' + Enable registration for new users. + ''; + }; + + registration_shared_secret = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set, allows registration by anyone who also has the shared + secret, even if registration is otherwise disabled. + + Secrets should be passed in via <literal>extraConfigFiles</literal>! + ''; + }; + + macaroon_secret_key = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Secret key for authentication tokens. If none is specified, + the registration_shared_secret is used, if one is given; otherwise, + a secret key is derived from the signing key. + + Secrets should be passed in via <literal>extraConfigFiles</literal>! + ''; + }; + + enable_metrics = mkOption { + type = types.bool; + default = false; + description = '' + Enable collection and rendering of performance metrics + ''; + }; + + report_stats = mkOption { + type = types.bool; + default = false; + description = '' + Whether or not to report anonymized homeserver usage statistics. + ''; + }; + + signing_key_path = mkOption { + type = types.path; + default = "${cfg.dataDir}/homeserver.signing.key"; + description = '' + Path to the signing key to sign messages with. + ''; + }; + + pid_file = mkOption { + type = types.path; + default = "/run/matrix-synapse.pid"; + readOnly = true; + description = '' + The file to store the PID in. + ''; + }; + + log_config = mkOption { + type = types.path; + default = ./matrix-synapse-log_config.yaml; + description = '' + The file that holds the logging configuration. + ''; + }; + + media_store_path = mkOption { + type = types.path; + default = if lib.versionAtLeast config.system.stateVersion "22.05" + then "${cfg.dataDir}/media_store" + else "${cfg.dataDir}/media"; + description = '' + Directory where uploaded images and attachments are stored. + ''; + }; + + public_baseurl = mkOption { + type = types.nullOr types.str; + default = null; + example = "https://example.com:8448/"; + description = '' + The public-facing base URL for the client API (not including _matrix/...) + ''; + }; + + tls_certificate_path = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/acme/example.com/fullchain.pem"; + description = '' + PEM encoded X509 certificate for TLS. + You can replace the self-signed certificate that synapse + autogenerates on launch with your own SSL certificate + key pair + if you like. Any required intermediary certificates can be + appended after the primary certificate in hierarchical order. + ''; + }; + + tls_private_key_path = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/acme/example.com/key.pem"; + description = '' + PEM encoded private key for TLS. Specify null if synapse is not + speaking TLS directly. + ''; + }; + + presence.enabled = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to enable presence tracking. + + Presence tracking allows users to see the state (e.g online/offline) + of other local and remote users. + ''; + }; + + listeners = mkOption { + type = types.listOf (types.submodule { + options = { + port = mkOption { + type = types.port; + example = 8448; + description = '' + The port to listen for HTTP(S) requests on. + ''; + }; + + bind_addresses = mkOption { + type = types.listOf types.str; + default = [ + "::1" + "127.0.0.1" + ]; + example = literalExpression '' + [ + "::" + "0.0.0.0" + ] + ''; + description = '' + IP addresses to bind the listener to. + ''; + }; + + type = mkOption { + type = types.enum [ + "http" + "manhole" + "metrics" + "replication" + ]; + default = "http"; + example = "metrics"; + description = '' + The type of the listener, usually http. + ''; + }; + + tls = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Whether to enable TLS on the listener socket. + ''; + }; + + x_forwarded = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Use the X-Forwarded-For (XFF) header as the client IP and not the + actual client IP. + ''; + }; + + resources = mkOption { + type = types.listOf (types.submodule { + options = { + names = mkOption { + type = types.listOf (types.enum [ + "client" + "consent" + "federation" + "keys" + "media" + "metrics" + "openid" + "replication" + "static" + ]); + description = '' + List of resources to host on this listener. + ''; + example = [ + "client" + ]; + }; + compress = mkOption { + type = types.bool; + description = '' + Should synapse compress HTTP responses to clients that support it? + This should be disabled if running synapse behind a load balancer + that can do automatic compression. + ''; + }; + }; + }); + description = '' + List of HTTP resources to serve on this listener. + ''; + }; + }; + }); + default = [ { + port = 8008; + bind_addresses = [ "127.0.0.1" ]; + type = "http"; + tls = false; + x_forwarded = true; + resources = [ { + names = [ "client" ]; + compress = true; + } { + names = [ "federation" ]; + compress = false; + } ]; + } ]; + description = '' + List of ports that Synapse should listen on, their purpose and their configuration. + ''; + }; + + database.name = mkOption { + type = types.enum [ + "sqlite3" + "psycopg2" + ]; + default = if versionAtLeast config.system.stateVersion "18.03" + then "psycopg2" + else "sqlite3"; + defaultText = literalExpression '' + if versionAtLeast config.system.stateVersion "18.03" + then "psycopg2" + else "sqlite3" + ''; + description = '' + The database engine name. Can be sqlite3 or psycopg2. + ''; + }; + + database.args.database = mkOption { + type = types.str; + default = { + sqlite3 = "${cfg.dataDir}/homeserver.db"; + psycopg2 = "matrix-synapse"; + }.${cfg.settings.database.name}; + defaultText = literalExpression '' + { + sqlite3 = "''${${options.services.matrix-synapse.dataDir}}/homeserver.db"; + psycopg2 = "matrix-synapse"; + }.''${${options.services.matrix-synapse.settings}.database.name}; + ''; + description = '' + Name of the database when using the psycopg2 backend, + path to the database location when using sqlite3. + ''; + }; + + database.args.user = mkOption { + type = types.nullOr types.str; + default = { + sqlite3 = null; + psycopg2 = "matrix-synapse"; + }.${cfg.settings.database.name}; + description = '' + Username to connect with psycopg2, set to null + when using sqlite3. + ''; + }; + + url_preview_enabled = mkOption { + type = types.bool; + default = true; + example = false; + description = '' + Is the preview URL API enabled? If enabled, you *must* specify an + explicit url_preview_ip_range_blacklist of IPs that the spider is + denied from accessing. + ''; + }; + + url_preview_ip_range_blacklist = mkOption { + type = types.listOf types.str; + default = [ + "10.0.0.0/8" + "100.64.0.0/10" + "127.0.0.0/8" + "169.254.0.0/16" + "172.16.0.0/12" + "192.0.0.0/24" + "192.0.2.0/24" + "192.168.0.0/16" + "192.88.99.0/24" + "198.18.0.0/15" + "198.51.100.0/24" + "2001:db8::/32" + "203.0.113.0/24" + "224.0.0.0/4" + "::1/128" + "fc00::/7" + "fe80::/10" + "fec0::/10" + "ff00::/8" + ]; + description = '' + List of IP address CIDR ranges that the URL preview spider is denied + from accessing. + ''; + }; + + url_preview_ip_range_whitelist = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of IP address CIDR ranges that the URL preview spider is allowed + to access even if they are specified in url_preview_ip_range_blacklist. + ''; + }; + + url_preview_url_blacklist = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Optional list of URL matches that the URL preview spider is + denied from accessing. + ''; + }; + + max_upload_size = mkOption { + type = types.str; + default = "50M"; + example = "100M"; + description = '' + The largest allowed upload size in bytes + ''; + }; + + max_image_pixels = mkOption { + type = types.str; + default = "32M"; + example = "64M"; + description = '' + Maximum number of pixels that will be thumbnailed + ''; + }; + + dynamic_thumbnails = mkOption { + type = types.bool; + default = false; + example = true; + description = '' + Whether to generate new thumbnails on the fly to precisely match + the resolution requested by the client. If true then whenever + a new resolution is requested by the client the server will + generate a new thumbnail. If false the server will pick a thumbnail + from a precalculated list. + ''; + }; + + turn_uris = mkOption { + type = types.listOf types.str; + default = []; + example = [ + "turn:turn.example.com:3487?transport=udp" + "turn:turn.example.com:3487?transport=tcp" + "turns:turn.example.com:5349?transport=udp" + "turns:turn.example.com:5349?transport=tcp" + ]; + description = '' + The public URIs of the TURN server to give to clients + ''; + }; + turn_shared_secret = mkOption { + type = types.str; + default = ""; + example = literalExpression '' + config.services.coturn.static-auth-secret + ''; + description = '' + The shared secret used to compute passwords for the TURN server. + + Secrets should be passed in via <literal>extraConfigFiles</literal>! + ''; + }; + + trusted_key_servers = mkOption { + type = types.listOf (types.submodule { + options = { + server_name = mkOption { + type = types.str; + example = "matrix.org"; + description = '' + Hostname of the trusted server. + ''; + }; + + verify_keys = mkOption { + type = types.nullOr (types.attrsOf types.str); + default = null; + example = literalExpression '' + { + "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"; + } + ''; + description = '' + Attribute set from key id to base64 encoded public key. + + If specified synapse will check that the response is signed + by at least one of the given keys. + ''; + }; + }; + }); + default = [ { + server_name = "matrix.org"; + verify_keys = { + "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"; + }; + } ]; + description = '' + The trusted servers to download signing keys from. + ''; + }; + + app_service_config_files = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + A list of application service config file to use + ''; + }; + + }; + }; + }; + + extraConfigFiles = mkOption { + type = types.listOf types.path; + default = []; + description = '' + Extra config files to include. + + The configuration files will be included based on the command line + argument --config-path. This allows to configure secrets without + having to go through the Nix store, e.g. based on deployment keys if + NixOps is in use. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = hasLocalPostgresDB -> config.services.postgresql.enable; + message = '' + Cannot deploy matrix-synapse with a configuration for a local postgresql database + and a missing postgresql service. Since 20.03 it's mandatory to manually configure the + database (please read the thread in https://github.com/NixOS/nixpkgs/pull/80447 for + further reference). + + If you + - try to deploy a fresh synapse, you need to configure the database yourself. An example + for this can be found in <nixpkgs/nixos/tests/matrix-synapse.nix> + - update your existing matrix-synapse instance, you simply need to add `services.postgresql.enable = true` + to your configuration. + + For further information about this update, please read the release-notes of 20.03 carefully. + ''; + } + ]; + + services.matrix-synapse.configFile = configFile; + + users.users.matrix-synapse = { + group = "matrix-synapse"; + home = cfg.dataDir; + createHome = true; + shell = "${pkgs.bash}/bin/bash"; + uid = config.ids.uids.matrix-synapse; + }; + + users.groups.matrix-synapse = { + gid = config.ids.gids.matrix-synapse; + }; + + systemd.services.matrix-synapse = { + description = "Synapse Matrix homeserver"; + after = [ "network.target" ] ++ optional hasLocalPostgresDB "postgresql.service"; + wantedBy = [ "multi-user.target" ]; + preStart = '' + ${cfg.package}/bin/synapse_homeserver \ + --config-path ${configFile} \ + --keys-directory ${cfg.dataDir} \ + --generate-keys + ''; + environment = { + PYTHONPATH = makeSearchPathOutput "lib" cfg.package.python.sitePackages [ pluginsEnv ]; + } // optionalAttrs (cfg.withJemalloc) { + LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so"; + }; + serviceConfig = { + Type = "notify"; + User = "matrix-synapse"; + Group = "matrix-synapse"; + WorkingDirectory = cfg.dataDir; + ExecStartPre = [ ("+" + (pkgs.writeShellScript "matrix-synapse-fix-permissions" '' + chown matrix-synapse:matrix-synapse ${cfg.dataDir}/homeserver.signing.key + chmod 0600 ${cfg.dataDir}/homeserver.signing.key + '')) ]; + ExecStart = '' + ${cfg.package}/bin/synapse_homeserver \ + ${ concatMapStringsSep "\n " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) } + --keys-directory ${cfg.dataDir} + ''; + ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID"; + Restart = "on-failure"; + UMask = "0077"; + }; + }; + + environment.systemPackages = [ registerNewMatrixUser ]; + }; + + meta = { + buildDocsInSandbox = false; + doc = ./matrix-synapse.xml; + maintainers = teams.matrix.members; + }; + +} diff --git a/nixos/modules/services/matrix/matrix-synapse.xml b/nixos/modules/services/matrix/matrix-synapse.xml new file mode 100644 index 00000000000..cf33957d58e --- /dev/null +++ b/nixos/modules/services/matrix/matrix-synapse.xml @@ -0,0 +1,231 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xi="http://www.w3.org/2001/XInclude" + version="5.0" + xml:id="module-services-matrix"> + <title>Matrix</title> + <para> + <link xlink:href="https://matrix.org/">Matrix</link> is an open standard for + interoperable, decentralised, real-time communication over IP. It can be used + to power Instant Messaging, VoIP/WebRTC signalling, Internet of Things + communication - or anywhere you need a standard HTTP API for publishing and + subscribing to data whilst tracking the conversation history. + </para> + <para> + This chapter will show you how to set up your own, self-hosted Matrix + homeserver using the Synapse reference homeserver, and how to serve your own + copy of the Element web client. See the + <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try + Matrix Now!</link> overview page for links to Element Apps for Android and iOS, + desktop clients, as well as bridges to other networks and other projects + around Matrix. + </para> + <section xml:id="module-services-matrix-synapse"> + <title>Synapse Homeserver</title> + + <para> + <link xlink:href="https://github.com/matrix-org/synapse">Synapse</link> is + the reference homeserver implementation of Matrix from the core development + team at matrix.org. The following configuration example will set up a + synapse server for the <literal>example.org</literal> domain, served from + the host <literal>myhostname.example.org</literal>. For more information, + please refer to the + <link xlink:href="https://github.com/matrix-org/synapse#synapse-installation"> + installation instructions of Synapse </link>. +<programlisting> +{ pkgs, lib, ... }: +let + fqdn = + let + join = hostName: domain: hostName + lib.optionalString (domain != null) ".${domain}"; + in join config.networking.hostName config.networking.domain; +in { + networking = { + <link linkend="opt-networking.hostName">hostName</link> = "myhostname"; + <link linkend="opt-networking.domain">domain</link> = "example.org"; + }; + <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ]; + + <link linkend="opt-services.postgresql.enable">services.postgresql.enable</link> = true; + <link linkend="opt-services.postgresql.initialScript">services.postgresql.initialScript</link> = pkgs.writeText "synapse-init.sql" '' + CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse'; + CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" + TEMPLATE template0 + LC_COLLATE = "C" + LC_CTYPE = "C"; + ''; + + services.nginx = { + <link linkend="opt-services.nginx.enable">enable</link> = true; + # only recommendedProxySettings and recommendedGzipSettings are strictly required, + # but the rest make sense as well + <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true; + <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true; + <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true; + <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true; + + <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = { + # This host section can be placed on a different host than the rest, + # i.e. to delegate from the host being accessible as ${config.networking.domain} + # to another host actually running the Matrix homeserver. + "${config.networking.domain}" = { + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/server".extraConfig</link> = + let + # use 443 instead of the default 8448 port to unite + # the client-server and server-server port for simplicity + server = { "m.server" = "${fqdn}:443"; }; + in '' + add_header Content-Type application/json; + return 200 '${builtins.toJSON server}'; + ''; + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."= /.well-known/matrix/client".extraConfig</link> = + let + client = { + "m.homeserver" = { "base_url" = "https://${fqdn}"; }; + "m.identity_server" = { "base_url" = "https://vector.im"; }; + }; + # ACAO required to allow element-web on any URL to request this json file + in '' + add_header Content-Type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '${builtins.toJSON client}'; + ''; + }; + + # Reverse proxy for Matrix client-server and server-server communication + ${fqdn} = { + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + + # Or do a redirect instead of the 404, or whatever is appropriate for you. + # But do not put a Matrix Web client here! See the Element web section below. + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.extraConfig">locations."/".extraConfig</link> = '' + return 404; + ''; + + # forward all Matrix API calls to the synapse Matrix homeserver + locations."/_matrix" = { + <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.proxyPass">proxyPass</link> = "http://[::1]:8008"; # without a trailing / + }; + }; + }; + }; + services.matrix-synapse = { + <link linkend="opt-services.matrix-synapse.enable">enable</link> = true; + <link linkend="opt-services.matrix-synapse.settings.server_name">server_name</link> = config.networking.domain; + <link linkend="opt-services.matrix-synapse.settings.listeners">listeners</link> = [ + { + <link linkend="opt-services.matrix-synapse.settings.listeners._.port">port</link> = 8008; + <link linkend="opt-services.matrix-synapse.settings.listeners._.bind_addresses">bind_addresses</link> = [ "::1" ]; + <link linkend="opt-services.matrix-synapse.settings.listeners._.type">type</link> = "http"; + <link linkend="opt-services.matrix-synapse.settings.listeners._.tls">tls</link> = false; + <link linkend="opt-services.matrix-synapse.settings.listeners._.x_forwarded">x_forwarded</link> = true; + <link linkend="opt-services.matrix-synapse.settings.listeners._.resources">resources</link> = [ { + <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.names">names</link> = [ "client" ]; + <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = true; + } { + <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.names">names</link> = [ "federation" ]; + <link linkend="opt-services.matrix-synapse.settings.listeners._.resources._.compress">compress</link> = false; + } ]; + } + ]; + }; +} +</programlisting> + </para> + + <para> + If the <code>A</code> and <code>AAAA</code> DNS records on + <literal>example.org</literal> do not point on the same host as the records + for <code>myhostname.example.org</code>, you can easily move the + <code>/.well-known</code> virtualHost section of the code to the host that + is serving <literal>example.org</literal>, while the rest stays on + <literal>myhostname.example.org</literal> with no other changes required. + This pattern also allows to seamlessly move the homeserver from + <literal>myhostname.example.org</literal> to + <literal>myotherhost.example.org</literal> by only changing the + <code>/.well-known</code> redirection target. + </para> + + <para> + If you want to run a server with public registration by anybody, you can + then enable <literal><link linkend="opt-services.matrix-synapse.settings.enable_registration">services.matrix-synapse.settings.enable_registration</link> = + true;</literal>. Otherwise, or you can generate a registration secret with + <command>pwgen -s 64 1</command> and set it with + <option><link linkend="opt-services.matrix-synapse.settings.registration_shared_secret">services.matrix-synapse.settings.registration_shared_secret</link></option>. + To create a new user or admin, run the following after you have set the secret + and have rebuilt NixOS: +<screen> +<prompt>$ </prompt>nix run nixpkgs.matrix-synapse +<prompt>$ </prompt>register_new_matrix_user -k <replaceable>your-registration-shared-secret</replaceable> http://localhost:8008 +<prompt>New user localpart: </prompt><replaceable>your-username</replaceable> +<prompt>Password:</prompt> +<prompt>Confirm password:</prompt> +<prompt>Make admin [no]:</prompt> +Success! +</screen> + In the example, this would create a user with the Matrix Identifier + <literal>@your-username:example.org</literal>. Note that the registration + secret ends up in the nix store and therefore is world-readable by any user + on your machine, so it makes sense to only temporarily activate the + <link linkend="opt-services.matrix-synapse.settings.registration_shared_secret">registration_shared_secret</link> + option until a better solution for NixOS is in place. + </para> + </section> + <section xml:id="module-services-matrix-element-web"> + <title>Element (formerly known as Riot) Web Client</title> + + <para> + <link xlink:href="https://github.com/vector-im/riot-web/">Element Web</link> is + the reference web client for Matrix and developed by the core team at + matrix.org. Element was formerly known as Riot.im, see the + <link xlink:href="https://element.io/blog/welcome-to-element/">Element introductory blog post</link> + for more information. The following snippet can be optionally added to the code before + to complete the synapse installation with a web client served at + <code>https://element.myhostname.example.org</code> and + <code>https://element.example.org</code>. Alternatively, you can use the hosted + copy at <link xlink:href="https://app.element.io/">https://app.element.io/</link>, + or use other web clients or native client applications. Due to the + <literal>/.well-known</literal> urls set up done above, many clients should + fill in the required connection details automatically when you enter your + Matrix Identifier. See + <link xlink:href="https://matrix.org/docs/projects/try-matrix-now.html">Try + Matrix Now!</link> for a list of existing clients and their supported + featureset. +<programlisting> +{ + services.nginx.virtualHosts."element.${fqdn}" = { + <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true; + <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ + "element.${config.networking.domain}" + ]; + + <link linkend="opt-services.nginx.virtualHosts._name_.root">root</link> = pkgs.element-web.override { + conf = { + default_server_config."m.homeserver" = { + "base_url" = "https://${fqdn}"; + "server_name" = "${fqdn}"; + }; + }; + }; + }; +} +</programlisting> + </para> + + <para> + Note that the Element developers do not recommend running Element and your Matrix + homeserver on the same fully-qualified domain name for security reasons. In + the example, this means that you should not reuse the + <literal>myhostname.example.org</literal> virtualHost to also serve Element, + but instead serve it on a different subdomain, like + <literal>element.example.org</literal> in the example. See the + <link xlink:href="https://github.com/vector-im/riot-web#important-security-note">Element + Important Security Notes</link> for more information on this subject. + </para> + </section> +</chapter> diff --git a/nixos/modules/services/matrix/mjolnir.nix b/nixos/modules/services/matrix/mjolnir.nix new file mode 100644 index 00000000000..278924b05cf --- /dev/null +++ b/nixos/modules/services/matrix/mjolnir.nix @@ -0,0 +1,242 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.mjolnir; + + yamlConfig = { + inherit (cfg) dataPath managementRoom protectedRooms; + + accessToken = "@ACCESS_TOKEN@"; # will be replaced in "generateConfig" + homeserverUrl = + if cfg.pantalaimon.enable then + "http://${cfg.pantalaimon.options.listenAddress}:${toString cfg.pantalaimon.options.listenPort}" + else + cfg.homeserverUrl; + + rawHomeserverUrl = cfg.homeserverUrl; + + pantalaimon = { + inherit (cfg.pantalaimon) username; + + use = cfg.pantalaimon.enable; + password = "@PANTALAIMON_PASSWORD@"; # will be replaced in "generateConfig" + }; + }; + + moduleConfigFile = pkgs.writeText "module-config.yaml" ( + generators.toYAML { } (filterAttrs (_: v: v != null) + (fold recursiveUpdate { } [ yamlConfig cfg.settings ]))); + + # these config files will be merged one after the other to build the final config + configFiles = [ + "${pkgs.mjolnir}/share/mjolnir/config/default.yaml" + moduleConfigFile + ]; + + # this will generate the default.yaml file with all configFiles as inputs and + # replace all secret strings using replace-secret + generateConfig = pkgs.writeShellScript "mjolnir-generate-config" ( + let + yqEvalStr = concatImapStringsSep " * " (pos: _: "select(fileIndex == ${toString (pos - 1)})") configFiles; + yqEvalArgs = concatStringsSep " " configFiles; + in + '' + set -euo pipefail + + umask 077 + + # mjolnir will try to load a config from "./config/default.yaml" in the working directory + # -> let's place the generated config there + mkdir -p ${cfg.dataPath}/config + + # merge all config files into one, overriding settings of the previous one with the next config + # e.g. "eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' filea.yaml fileb.yaml" will merge filea.yaml with fileb.yaml + ${pkgs.yq-go}/bin/yq eval-all -P '${yqEvalStr}' ${yqEvalArgs} > ${cfg.dataPath}/config/default.yaml + + ${optionalString (cfg.accessTokenFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@ACCESS_TOKEN@' '${cfg.accessTokenFile}' ${cfg.dataPath}/config/default.yaml + ''} + ${optionalString (cfg.pantalaimon.passwordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@PANTALAIMON_PASSWORD@' '${cfg.pantalaimon.passwordFile}' ${cfg.dataPath}/config/default.yaml + ''} + '' + ); +in +{ + options.services.mjolnir = { + enable = mkEnableOption "Mjolnir, a moderation tool for Matrix"; + + homeserverUrl = mkOption { + type = types.str; + default = "https://matrix.org"; + description = '' + Where the homeserver is located (client-server URL). + + If <literal>pantalaimon.enable</literal> is <literal>true</literal>, this option will become the homeserver to which <literal>pantalaimon</literal> connects. + The listen address of <literal>pantalaimon</literal> will then become the <literal>homeserverUrl</literal> of <literal>mjolnir</literal>. + ''; + }; + + accessTokenFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + File containing the matrix access token for the <literal>mjolnir</literal> user. + ''; + }; + + pantalaimon = mkOption { + description = '' + <literal>pantalaimon</literal> options (enables E2E Encryption support). + + This will create a <literal>pantalaimon</literal> instance with the name "mjolnir". + ''; + default = { }; + type = types.submodule { + options = { + enable = mkEnableOption '' + If true, accessToken is ignored and the username/password below will be + used instead. The access token of the bot will be stored in the dataPath. + ''; + + username = mkOption { + type = types.str; + description = "The username to login with."; + }; + + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + File containing the matrix password for the <literal>mjolnir</literal> user. + ''; + }; + + options = mkOption { + type = types.submodule (import ./pantalaimon-options.nix); + default = { }; + description = '' + passthrough additional options to the <literal>pantalaimon</literal> service. + ''; + }; + }; + }; + }; + + dataPath = mkOption { + type = types.path; + default = "/var/lib/mjolnir"; + description = '' + The directory the bot should store various bits of information in. + ''; + }; + + managementRoom = mkOption { + type = types.str; + default = "#moderators:example.org"; + description = '' + The room ID where people can use the bot. The bot has no access controls, so + anyone in this room can use the bot - secure your room! + This should be a room alias or room ID - not a matrix.to URL. + Note: <literal>mjolnir</literal> is fairly verbose - expect a lot of messages from it. + ''; + }; + + protectedRooms = mkOption { + type = types.listOf types.str; + default = [ ]; + example = literalExpression '' + [ + "https://matrix.to/#/#yourroom:example.org" + "https://matrix.to/#/#anotherroom:example.org" + ] + ''; + description = '' + A list of rooms to protect (matrix.to URLs). + ''; + }; + + settings = mkOption { + default = { }; + type = (pkgs.formats.yaml { }).type; + example = literalExpression '' + { + autojoinOnlyIfManager = true; + automaticallyRedactForReasons = [ "spam" "advertising" ]; + } + ''; + description = '' + Additional settings (see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">mjolnir default config</link> for available settings). These settings will override settings made by the module config. + ''; + }; + }; + + config = mkIf config.services.mjolnir.enable { + assertions = [ + { + assertion = !(cfg.pantalaimon.enable && cfg.pantalaimon.passwordFile == null); + message = "Specify pantalaimon.passwordFile"; + } + { + assertion = !(cfg.pantalaimon.enable && cfg.accessTokenFile != null); + message = "Do not specify accessTokenFile when using pantalaimon"; + } + { + assertion = !(!cfg.pantalaimon.enable && cfg.accessTokenFile == null); + message = "Specify accessTokenFile when not using pantalaimon"; + } + ]; + + services.pantalaimon-headless.instances."mjolnir" = mkIf cfg.pantalaimon.enable + { + homeserver = cfg.homeserverUrl; + } // cfg.pantalaimon.options; + + systemd.services.mjolnir = { + description = "mjolnir - a moderation tool for Matrix"; + wants = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + after = [ "network-online.target" ] ++ optionals (cfg.pantalaimon.enable) [ "pantalaimon-mjolnir.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = ''${pkgs.mjolnir}/bin/mjolnir''; + ExecStartPre = [ generateConfig ]; + WorkingDirectory = cfg.dataPath; + StateDirectory = "mjolnir"; + StateDirectoryMode = "0700"; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + NoNewPrivileges = true; + PrivateDevices = true; + User = "mjolnir"; + Restart = "on-failure"; + + /* TODO: wait for #102397 to be resolved. Then load secrets from $CREDENTIALS_DIRECTORY+"/NAME" + DynamicUser = true; + LoadCredential = [] ++ + optionals (cfg.accessTokenFile != null) [ + "access_token:${cfg.accessTokenFile}" + ] ++ + optionals (cfg.pantalaimon.passwordFile != null) [ + "pantalaimon_password:${cfg.pantalaimon.passwordFile}" + ]; + */ + }; + }; + + users = { + users.mjolnir = { + group = "mjolnir"; + isSystemUser = true; + }; + groups.mjolnir = { }; + }; + }; + + meta = { + doc = ./mjolnir.xml; + maintainers = with maintainers; [ jojosch ]; + }; +} diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml new file mode 100644 index 00000000000..b07abe33979 --- /dev/null +++ b/nixos/modules/services/matrix/mjolnir.xml @@ -0,0 +1,134 @@ +<chapter xmlns="http://docbook.org/ns/docbook" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:xi="http://www.w3.org/2001/XInclude" + version="5.0" + xml:id="module-services-mjolnir"> + <title>Mjolnir (Matrix Moderation Tool)</title> + <para> + This chapter will show you how to set up your own, self-hosted + <link xlink:href="https://github.com/matrix-org/mjolnir">Mjolnir</link> + instance. + </para> + <para> + As an all-in-one moderation tool, it can protect your server from + malicious invites, spam messages, and whatever else you don't want. + In addition to server-level protection, Mjolnir is great for communities + wanting to protect their rooms without having to use their personal + accounts for moderation. + </para> + <para> + The bot by default includes support for bans, redactions, anti-spam, + server ACLs, room directory changes, room alias transfers, account + deactivation, room shutdown, and more. + </para> + <para> + See the <link xlink:href="https://github.com/matrix-org/mjolnir#readme">README</link> + page and the <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md">Moderator's guide</link> + for additional instructions on how to setup and use Mjolnir. + </para> + <para> + For <link linkend="opt-services.mjolnir.settings">additional settings</link> + see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">the default configuration</link>. + </para> + <section xml:id="module-services-mjolnir-setup"> + <title>Mjolnir Setup</title> + <para> + First create a new Room which will be used as a management room for Mjolnir. In + this room, Mjolnir will log possible errors and debugging information. You'll + need to set this Room-ID in <link linkend="opt-services.mjolnir.managementRoom">services.mjolnir.managementRoom</link>. + </para> + <para> + Next, create a new user for Mjolnir on your homeserver, if not present already. + </para> + <para> + The Mjolnir Matrix user expects to be free of any rate limiting. + See <link xlink:href="https://github.com/matrix-org/synapse/issues/6286">Synapse #6286</link> + for an example on how to achieve this. + </para> + <para> + If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc. + you'll need to make the Mjolnir user a Matrix server admin. + </para> + <para> + Now invite the Mjolnir user to the management room. + </para> + <para> + It is recommended to use <link xlink:href="https://github.com/matrix-org/pantalaimon">Pantalaimon</link>, + so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room. + </para> + <para> + To enable the Pantalaimon E2E Proxy for mjolnir, enable + <link linkend="opt-services.mjolnir.pantalaimon.enable">services.mjolnir.pantalaimon</link>. This will + autoconfigure a new Pantalaimon instance, which will connect to the homeserver + set in <link linkend="opt-services.mjolnir.homeserverUrl">services.mjolnir.homeserverUrl</link> and Mjolnir itself + will be configured to connect to the new Pantalaimon instance. + </para> +<programlisting> +{ + services.mjolnir = { + enable = true; + <link linkend="opt-services.mjolnir.homeserverUrl">homeserverUrl</link> = "https://matrix.domain.tld"; + <link linkend="opt-services.mjolnir.pantalaimon">pantalaimon</link> = { + <link linkend="opt-services.mjolnir.pantalaimon.enable">enable</link> = true; + <link linkend="opt-services.mjolnir.pantalaimon.username">username</link> = "mjolnir"; + <link linkend="opt-services.mjolnir.pantalaimon.passwordFile">passwordFile</link> = "/run/secrets/mjolnir-password"; + }; + <link linkend="opt-services.mjolnir.protectedRooms">protectedRooms</link> = [ + "https://matrix.to/#/!xxx:domain.tld" + ]; + <link linkend="opt-services.mjolnir.managementRoom">managementRoom</link> = "!yyy:domain.tld"; + }; +} +</programlisting> + <section xml:id="module-services-mjolnir-setup-ems"> + <title>Element Matrix Services (EMS)</title> + <para> + If you are using a managed <link xlink:href="https://ems.element.io/">"Element Matrix Services (EMS)"</link> + server, you will need to consent to the terms and conditions. Upon startup, an error + log entry with a URL to the consent page will be generated. + </para> + </section> + </section> + + <section xml:id="module-services-mjolnir-matrix-synapse-antispam"> + <title>Synapse Antispam Module</title> + <para> + A Synapse module is also available to apply the same rulesets the bot + uses across an entire homeserver. + </para> + <para> + To use the Antispam Module, add <package>matrix-synapse-plugins.matrix-synapse-mjolnir-antispam</package> + to the Synapse plugin list and enable the <literal>mjolnir.Module</literal> module. + </para> +<programlisting> +{ + services.matrix-synapse = { + plugins = with pkgs; [ + matrix-synapse-plugins.matrix-synapse-mjolnir-antispam + ]; + extraConfig = '' + modules: + - module: mjolnir.Module + config: + # Prevent servers/users in the ban lists from inviting users on this + # server to rooms. Default true. + block_invites: true + # Flag messages sent by servers/users in the ban lists as spam. Currently + # this means that spammy messages will appear as empty to users. Default + # false. + block_messages: false + # Remove users from the user directory search by filtering matrix IDs and + # display names by the entries in the user ban list. Default false. + block_usernames: false + # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir, + # this list cannot be room aliases or permalinks. This server is expected + # to already be joined to the room - Mjolnir will not automatically join + # these rooms. + ban_lists: + - "!roomid:example.org" + ''; + }; +} +</programlisting> + </section> +</chapter> diff --git a/nixos/modules/services/matrix/pantalaimon-options.nix b/nixos/modules/services/matrix/pantalaimon-options.nix new file mode 100644 index 00000000000..035c57540d0 --- /dev/null +++ b/nixos/modules/services/matrix/pantalaimon-options.nix @@ -0,0 +1,70 @@ +{ config, lib, name, ... }: + +with lib; +{ + options = { + dataPath = mkOption { + type = types.path; + default = "/var/lib/pantalaimon-${name}"; + description = '' + The directory where <literal>pantalaimon</literal> should store its state such as the database file. + ''; + }; + + logLevel = mkOption { + type = types.enum [ "info" "warning" "error" "debug" ]; + default = "warning"; + description = '' + Set the log level of the daemon. + ''; + }; + + homeserver = mkOption { + type = types.str; + example = "https://matrix.org"; + description = '' + The URI of the homeserver that the <literal>pantalaimon</literal> proxy should + forward requests to, without the matrix API path but including + the http(s) schema. + ''; + }; + + ssl = mkOption { + type = types.bool; + default = true; + description = '' + Whether or not SSL verification should be enabled for outgoing + connections to the homeserver. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "localhost"; + description = '' + The address where the daemon will listen to client connections + for this homeserver. + ''; + }; + + listenPort = mkOption { + type = types.port; + default = 8009; + description = '' + The port where the daemon will listen to client connections for + this homeserver. Note that the listen address/port combination + needs to be unique between different homeservers. + ''; + }; + + extraSettings = mkOption { + type = types.attrs; + default = { }; + description = '' + Extra configuration options. See + <link xlink:href="https://github.com/matrix-org/pantalaimon/blob/master/docs/man/pantalaimon.5.md">pantalaimon(5)</link> + for available options. + ''; + }; + }; +} diff --git a/nixos/modules/services/matrix/pantalaimon.nix b/nixos/modules/services/matrix/pantalaimon.nix new file mode 100644 index 00000000000..63b40099ca5 --- /dev/null +++ b/nixos/modules/services/matrix/pantalaimon.nix @@ -0,0 +1,70 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.pantalaimon-headless; + + iniFmt = pkgs.formats.ini { }; + + mkConfigFile = name: instanceConfig: iniFmt.generate "pantalaimon.conf" { + Default = { + LogLevel = instanceConfig.logLevel; + Notifications = false; + }; + + ${name} = (recursiveUpdate + { + Homeserver = instanceConfig.homeserver; + ListenAddress = instanceConfig.listenAddress; + ListenPort = instanceConfig.listenPort; + SSL = instanceConfig.ssl; + + # Set some settings to prevent user interaction for headless operation + IgnoreVerification = true; + UseKeyring = false; + } + instanceConfig.extraSettings + ); + }; + + mkPantalaimonService = name: instanceConfig: + nameValuePair "pantalaimon-${name}" { + description = "pantalaimon instance ${name} - E2EE aware proxy daemon for matrix clients"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = ''${pkgs.pantalaimon-headless}/bin/pantalaimon --config ${mkConfigFile name instanceConfig} --data-path ${instanceConfig.dataPath}''; + Restart = "on-failure"; + DynamicUser = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + StateDirectory = "pantalaimon-${name}"; + }; + }; +in +{ + options.services.pantalaimon-headless.instances = mkOption { + default = { }; + type = types.attrsOf (types.submodule (import ./pantalaimon-options.nix)); + description = '' + Declarative instance config. + + Note: to use pantalaimon interactively, e.g. for a Matrix client which does not + support End-to-end encryption (like <literal>fractal</literal>), refer to the home-manager module. + ''; + }; + + config = mkIf (config.services.pantalaimon-headless.instances != { }) + { + systemd.services = mapAttrs' mkPantalaimonService config.services.pantalaimon-headless.instances; + }; + + meta = { + maintainers = with maintainers; [ jojosch ]; + }; +} |