1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.matrix-synapse;
7 pg = config.services.postgresql;
8 usePostgresql = cfg.database_type == "psycopg2";
9 logConfigFile = pkgs.writeText "log_config.yaml" cfg.logConfig;
10 mkResource = r: ''{names: ${builtins.toJSON r.names}, compress: ${boolToString r.compress}}'';
11 mkListener = l: ''{port: ${toString l.port}, bind_address: "${l.bind_address}", type: ${l.type}, tls: ${boolToString l.tls}, x_forwarded: ${boolToString l.x_forwarded}, resources: [${concatStringsSep "," (map mkResource l.resources)}]}'';
12 configFile = pkgs.writeText "homeserver.yaml" ''
13${optionalString (cfg.tls_certificate_path != null) ''
14tls_certificate_path: "${cfg.tls_certificate_path}"
15''}
16${optionalString (cfg.tls_private_key_path != null) ''
17tls_private_key_path: "${cfg.tls_private_key_path}"
18''}
19${optionalString (cfg.tls_dh_params_path != null) ''
20tls_dh_params_path: "${cfg.tls_dh_params_path}"
21''}
22no_tls: ${boolToString cfg.no_tls}
23${optionalString (cfg.bind_port != null) ''
24bind_port: ${toString cfg.bind_port}
25''}
26${optionalString (cfg.unsecure_port != null) ''
27unsecure_port: ${toString cfg.unsecure_port}
28''}
29${optionalString (cfg.bind_host != null) ''
30bind_host: "${cfg.bind_host}"
31''}
32server_name: "${cfg.server_name}"
33pid_file: "/var/run/matrix-synapse.pid"
34web_client: ${boolToString cfg.web_client}
35${optionalString (cfg.public_baseurl != null) ''
36public_baseurl: "${cfg.public_baseurl}"
37''}
38listeners: [${concatStringsSep "," (map mkListener cfg.listeners)}]
39database: {
40 name: "${cfg.database_type}",
41 args: {
42 ${concatStringsSep ",\n " (
43 mapAttrsToList (n: v: "\"${n}\": ${builtins.toJSON v}") cfg.database_args
44 )}
45 }
46}
47event_cache_size: "${cfg.event_cache_size}"
48verbose: ${cfg.verbose}
49log_config: "${logConfigFile}"
50rc_messages_per_second: ${cfg.rc_messages_per_second}
51rc_message_burst_count: ${cfg.rc_message_burst_count}
52federation_rc_window_size: ${cfg.federation_rc_window_size}
53federation_rc_sleep_limit: ${cfg.federation_rc_sleep_limit}
54federation_rc_sleep_delay: ${cfg.federation_rc_sleep_delay}
55federation_rc_reject_limit: ${cfg.federation_rc_reject_limit}
56federation_rc_concurrent: ${cfg.federation_rc_concurrent}
57media_store_path: "${cfg.dataDir}/media"
58uploads_path: "${cfg.dataDir}/uploads"
59max_upload_size: "${cfg.max_upload_size}"
60max_image_pixels: "${cfg.max_image_pixels}"
61dynamic_thumbnails: ${boolToString cfg.dynamic_thumbnails}
62url_preview_enabled: ${boolToString cfg.url_preview_enabled}
63${optionalString (cfg.url_preview_enabled == true) ''
64url_preview_ip_range_blacklist: ${builtins.toJSON cfg.url_preview_ip_range_blacklist}
65url_preview_ip_range_whitelist: ${builtins.toJSON cfg.url_preview_ip_range_whitelist}
66url_preview_url_blacklist: ${builtins.toJSON cfg.url_preview_url_blacklist}
67''}
68recaptcha_private_key: "${cfg.recaptcha_private_key}"
69recaptcha_public_key: "${cfg.recaptcha_public_key}"
70enable_registration_captcha: ${boolToString cfg.enable_registration_captcha}
71turn_uris: ${builtins.toJSON cfg.turn_uris}
72turn_shared_secret: "${cfg.turn_shared_secret}"
73enable_registration: ${boolToString cfg.enable_registration}
74${optionalString (cfg.registration_shared_secret != null) ''
75registration_shared_secret: "${cfg.registration_shared_secret}"
76''}
77recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify"
78turn_user_lifetime: "${cfg.turn_user_lifetime}"
79user_creation_max_duration: ${cfg.user_creation_max_duration}
80bcrypt_rounds: ${cfg.bcrypt_rounds}
81allow_guest_access: ${boolToString cfg.allow_guest_access}
82trusted_third_party_id_servers: ${builtins.toJSON cfg.trusted_third_party_id_servers}
83room_invite_state_types: ${builtins.toJSON cfg.room_invite_state_types}
84${optionalString (cfg.macaroon_secret_key != null) ''
85 macaroon_secret_key: "${cfg.macaroon_secret_key}"
86''}
87expire_access_token: ${boolToString cfg.expire_access_token}
88enable_metrics: ${boolToString cfg.enable_metrics}
89report_stats: ${boolToString cfg.report_stats}
90signing_key_path: "${cfg.dataDir}/homeserver.signing.key"
91key_refresh_interval: "${cfg.key_refresh_interval}"
92perspectives:
93 servers: {
94 ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
95 "${n}": {
96 "verify_keys": {
97 ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
98 "${n}": {
99 "key": "${v}"
100 }'') v)}
101 }
102 '') cfg.servers)}
103 }
104 }
105app_service_config_files: ${builtins.toJSON cfg.app_service_config_files}
106
107${cfg.extraConfig}
108'';
109in {
110 options = {
111 services.matrix-synapse = {
112 enable = mkEnableOption "matrix.org synapse";
113 package = mkOption {
114 type = types.package;
115 default = pkgs.matrix-synapse;
116 defaultText = "pkgs.matrix-synapse";
117 description = ''
118 Overridable attribute of the matrix synapse server package to use.
119 '';
120 };
121 no_tls = mkOption {
122 type = types.bool;
123 default = false;
124 description = ''
125 Don't bind to the https port
126 '';
127 };
128 bind_port = mkOption {
129 type = types.nullOr types.int;
130 default = null;
131 example = 8448;
132 description = ''
133 DEPRECATED: Use listeners instead.
134 The port to listen for HTTPS requests on.
135 For when matrix traffic is sent directly to synapse.
136 '';
137 };
138 unsecure_port = mkOption {
139 type = types.nullOr types.int;
140 default = null;
141 example = 8008;
142 description = ''
143 DEPRECATED: Use listeners instead.
144 The port to listen for HTTP requests on.
145 For when matrix traffic passes through loadbalancer that unwraps TLS.
146 '';
147 };
148 bind_host = mkOption {
149 type = types.nullOr types.str;
150 default = null;
151 description = ''
152 DEPRECATED: Use listeners instead.
153 Local interface to listen on.
154 The empty string will cause synapse to listen on all interfaces.
155 '';
156 };
157 tls_certificate_path = mkOption {
158 type = types.nullOr types.str;
159 default = null;
160 example = "${cfg.dataDir}/homeserver.tls.crt";
161 description = ''
162 PEM encoded X509 certificate for TLS.
163 You can replace the self-signed certificate that synapse
164 autogenerates on launch with your own SSL certificate + key pair
165 if you like. Any required intermediary certificates can be
166 appended after the primary certificate in hierarchical order.
167 '';
168 };
169 tls_private_key_path = mkOption {
170 type = types.nullOr types.str;
171 default = null;
172 example = "${cfg.dataDir}/homeserver.tls.key";
173 description = ''
174 PEM encoded private key for TLS. Specify null if synapse is not
175 speaking TLS directly.
176 '';
177 };
178 tls_dh_params_path = mkOption {
179 type = types.nullOr types.str;
180 default = null;
181 example = "${cfg.dataDir}/homeserver.tls.dh";
182 description = ''
183 PEM dh parameters for ephemeral keys
184 '';
185 };
186 server_name = mkOption {
187 type = types.str;
188 example = "example.com";
189 default = config.networking.hostName;
190 description = ''
191 The domain name of the server, with optional explicit port.
192 This is used by remote servers to connect to this server,
193 e.g. matrix.org, localhost:8080, etc.
194 This is also the last part of your UserID.
195 '';
196 };
197 web_client = mkOption {
198 type = types.bool;
199 default = false;
200 description = ''
201 Whether to serve a web client from the HTTP/HTTPS root resource.
202 '';
203 };
204 public_baseurl = mkOption {
205 type = types.nullOr types.str;
206 default = null;
207 example = "https://example.com:8448/";
208 description = ''
209 The public-facing base URL for the client API (not including _matrix/...)
210 '';
211 };
212 listeners = mkOption {
213 type = types.listOf (types.submodule {
214 options = {
215 port = mkOption {
216 type = types.int;
217 example = 8448;
218 description = ''
219 The port to listen for HTTP(S) requests on.
220 '';
221 };
222 bind_address = mkOption {
223 type = types.str;
224 default = "";
225 example = "203.0.113.42";
226 description = ''
227 Local interface to listen on.
228 The empty string will cause synapse to listen on all interfaces.
229 '';
230 };
231 type = mkOption {
232 type = types.str;
233 default = "http";
234 description = ''
235 Type of listener.
236 '';
237 };
238 tls = mkOption {
239 type = types.bool;
240 default = true;
241 description = ''
242 Whether to listen for HTTPS connections rather than HTTP.
243 '';
244 };
245 x_forwarded = mkOption {
246 type = types.bool;
247 default = false;
248 description = ''
249 Use the X-Forwarded-For (XFF) header as the client IP and not the
250 actual client IP.
251 '';
252 };
253 resources = mkOption {
254 type = types.listOf (types.submodule {
255 options = {
256 names = mkOption {
257 type = types.listOf types.str;
258 description = ''
259 List of resources to host on this listener.
260 '';
261 example = ["client" "webclient" "federation"];
262 };
263 compress = mkOption {
264 type = types.bool;
265 description = ''
266 Should synapse compress HTTP responses to clients that support it?
267 This should be disabled if running synapse behind a load balancer
268 that can do automatic compression.
269 '';
270 };
271 };
272 });
273 description = ''
274 List of HTTP resources to serve on this listener.
275 '';
276 };
277 };
278 });
279 default = [{
280 port = 8448;
281 bind_address = "";
282 type = "http";
283 tls = true;
284 x_forwarded = false;
285 resources = [
286 { names = ["client" "webclient"]; compress = true; }
287 { names = ["federation"]; compress = false; }
288 ];
289 }];
290 description = ''
291 List of ports that Synapse should listen on, their purpose and their configuration.
292 '';
293 };
294 verbose = mkOption {
295 type = types.str;
296 default = "0";
297 description = "Logging verbosity level.";
298 };
299 rc_messages_per_second = mkOption {
300 type = types.str;
301 default = "0.2";
302 description = "Number of messages a client can send per second";
303 };
304 rc_message_burst_count = mkOption {
305 type = types.str;
306 default = "10.0";
307 description = "Number of message a client can send before being throttled";
308 };
309 federation_rc_window_size = mkOption {
310 type = types.str;
311 default = "1000";
312 description = "The federation window size in milliseconds";
313 };
314 federation_rc_sleep_limit = mkOption {
315 type = types.str;
316 default = "10";
317 description = ''
318 The number of federation requests from a single server in a window
319 before the server will delay processing the request.
320 '';
321 };
322 federation_rc_sleep_delay = mkOption {
323 type = types.str;
324 default = "500";
325 description = ''
326 The duration in milliseconds to delay processing events from
327 remote servers by if they go over the sleep limit.
328 '';
329 };
330 federation_rc_reject_limit = mkOption {
331 type = types.str;
332 default = "50";
333 description = ''
334 The maximum number of concurrent federation requests allowed
335 from a single server
336 '';
337 };
338 federation_rc_concurrent = mkOption {
339 type = types.str;
340 default = "3";
341 description = "The number of federation requests to concurrently process from a single server";
342 };
343 database_type = mkOption {
344 type = types.enum [ "sqlite3" "psycopg2" ];
345 default = if versionAtLeast config.system.stateVersion "18.03"
346 then "psycopg2"
347 else "sqlite3";
348 description = ''
349 The database engine name. Can be sqlite or psycopg2.
350 '';
351 };
352 create_local_database = mkOption {
353 type = types.bool;
354 default = true;
355 description = ''
356 Whether to create a local database automatically.
357 '';
358 };
359 database_name = mkOption {
360 type = types.str;
361 default = "matrix-synapse";
362 description = "Database name.";
363 };
364 database_user = mkOption {
365 type = types.str;
366 default = "matrix-synapse";
367 description = "Database user name.";
368 };
369 database_args = mkOption {
370 type = types.attrs;
371 default = {
372 sqlite3 = { database = "${cfg.dataDir}/homeserver.db"; };
373 psycopg2 = {
374 user = cfg.database_user;
375 database = cfg.database_name;
376 };
377 }."${cfg.database_type}";
378 description = ''
379 Arguments to pass to the engine.
380 '';
381 };
382 event_cache_size = mkOption {
383 type = types.str;
384 default = "10K";
385 description = "Number of events to cache in memory.";
386 };
387 url_preview_enabled = mkOption {
388 type = types.bool;
389 default = false;
390 description = ''
391 Is the preview URL API enabled? If enabled, you *must* specify an
392 explicit url_preview_ip_range_blacklist of IPs that the spider is
393 denied from accessing.
394 '';
395 };
396 url_preview_ip_range_blacklist = mkOption {
397 type = types.listOf types.str;
398 default = [];
399 description = ''
400 List of IP address CIDR ranges that the URL preview spider is denied
401 from accessing.
402 '';
403 };
404 url_preview_ip_range_whitelist = mkOption {
405 type = types.listOf types.str;
406 default = [];
407 description = ''
408 List of IP address CIDR ranges that the URL preview spider is allowed
409 to access even if they are specified in
410 url_preview_ip_range_blacklist.
411 '';
412 };
413 url_preview_url_blacklist = mkOption {
414 type = types.listOf types.str;
415 default = [
416 "127.0.0.0/8"
417 "10.0.0.0/8"
418 "172.16.0.0/12"
419 "192.168.0.0/16"
420 "100.64.0.0/10"
421 "169.254.0.0/16"
422 ];
423 description = ''
424 Optional list of URL matches that the URL preview spider is
425 denied from accessing.
426 '';
427 };
428 recaptcha_private_key = mkOption {
429 type = types.str;
430 default = "";
431 description = ''
432 This Home Server's ReCAPTCHA private key.
433 '';
434 };
435 recaptcha_public_key = mkOption {
436 type = types.str;
437 default = "";
438 description = ''
439 This Home Server's ReCAPTCHA public key.
440 '';
441 };
442 enable_registration_captcha = mkOption {
443 type = types.bool;
444 default = false;
445 description = ''
446 Enables ReCaptcha checks when registering, preventing signup
447 unless a captcha is answered. Requires a valid ReCaptcha
448 public/private key.
449 '';
450 };
451 turn_uris = mkOption {
452 type = types.listOf types.str;
453 default = [];
454 description = ''
455 The public URIs of the TURN server to give to clients
456 '';
457 };
458 turn_shared_secret = mkOption {
459 type = types.str;
460 default = "";
461 description = ''
462 The shared secret used to compute passwords for the TURN server
463 '';
464 };
465 turn_user_lifetime = mkOption {
466 type = types.str;
467 default = "1h";
468 description = "How long generated TURN credentials last";
469 };
470 enable_registration = mkOption {
471 type = types.bool;
472 default = false;
473 description = ''
474 Enable registration for new users.
475 '';
476 };
477 registration_shared_secret = mkOption {
478 type = types.nullOr types.str;
479 default = null;
480 description = ''
481 If set, allows registration by anyone who also has the shared
482 secret, even if registration is otherwise disabled.
483 '';
484 };
485 enable_metrics = mkOption {
486 type = types.bool;
487 default = false;
488 description = ''
489 Enable collection and rendering of performance metrics
490 '';
491 };
492 report_stats = mkOption {
493 type = types.bool;
494 default = false;
495 description = ''
496 '';
497 };
498 servers = mkOption {
499 type = types.attrsOf (types.attrsOf types.str);
500 default = {
501 "matrix.org" = {
502 "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
503 };
504 };
505 description = ''
506 The trusted servers to download signing keys from.
507 '';
508 };
509 max_upload_size = mkOption {
510 type = types.str;
511 default = "10M";
512 description = "The largest allowed upload size in bytes";
513 };
514 max_image_pixels = mkOption {
515 type = types.str;
516 default = "32M";
517 description = "Maximum number of pixels that will be thumbnailed";
518 };
519 dynamic_thumbnails = mkOption {
520 type = types.bool;
521 default = false;
522 description = ''
523 Whether to generate new thumbnails on the fly to precisely match
524 the resolution requested by the client. If true then whenever
525 a new resolution is requested by the client the server will
526 generate a new thumbnail. If false the server will pick a thumbnail
527 from a precalculated list.
528 '';
529 };
530 user_creation_max_duration = mkOption {
531 type = types.str;
532 default = "1209600000";
533 description = ''
534 Sets the expiry for the short term user creation in
535 milliseconds. The default value is two weeks.
536 '';
537 };
538 bcrypt_rounds = mkOption {
539 type = types.str;
540 default = "12";
541 description = ''
542 Set the number of bcrypt rounds used to generate password hash.
543 Larger numbers increase the work factor needed to generate the hash.
544 '';
545 };
546 allow_guest_access = mkOption {
547 type = types.bool;
548 default = false;
549 description = ''
550 Allows users to register as guests without a password/email/etc, and
551 participate in rooms hosted on this server which have been made
552 accessible to anonymous users.
553 '';
554 };
555 trusted_third_party_id_servers = mkOption {
556 type = types.listOf types.str;
557 default = ["matrix.org"];
558 description = ''
559 The list of identity servers trusted to verify third party identifiers by this server.
560 '';
561 };
562 room_invite_state_types = mkOption {
563 type = types.listOf types.str;
564 default = ["m.room.join_rules" "m.room.canonical_alias" "m.room.avatar" "m.room.name"];
565 description = ''
566 A list of event types that will be included in the room_invite_state
567 '';
568 };
569 macaroon_secret_key = mkOption {
570 type = types.nullOr types.str;
571 default = null;
572 description = ''
573 Secret key for authentication tokens
574 '';
575 };
576 expire_access_token = mkOption {
577 type = types.bool;
578 default = false;
579 description = ''
580 Whether to enable access token expiration.
581 '';
582 };
583 key_refresh_interval = mkOption {
584 type = types.str;
585 default = "1d";
586 description = ''
587 How long key response published by this server is valid for.
588 Used to set the valid_until_ts in /key/v2 APIs.
589 Determines how quickly servers will query to check which keys
590 are still valid.
591 '';
592 };
593 app_service_config_files = mkOption {
594 type = types.listOf types.path;
595 default = [ ];
596 description = ''
597 A list of application service config file to use
598 '';
599 };
600 extraConfig = mkOption {
601 type = types.lines;
602 default = "";
603 description = ''
604 Extra config options for matrix-synapse.
605 '';
606 };
607 extraConfigFiles = mkOption {
608 type = types.listOf types.path;
609 default = [];
610 description = ''
611 Extra config files to include.
612
613 The configuration files will be included based on the command line
614 argument --config-path. This allows to configure secrets without
615 having to go through the Nix store, e.g. based on deployment keys if
616 NixOPS is in use.
617 '';
618 };
619 logConfig = mkOption {
620 type = types.lines;
621 default = readFile ./matrix-synapse-log_config.yaml;
622 description = ''
623 A yaml python logging config file
624 '';
625 };
626 dataDir = mkOption {
627 type = types.str;
628 default = "/var/lib/matrix-synapse";
629 description = ''
630 The directory where matrix-synapse stores its stateful data such as
631 certificates, media and uploads.
632 '';
633 };
634 };
635 };
636
637 config = mkIf cfg.enable {
638 users.extraUsers = [
639 { name = "matrix-synapse";
640 group = "matrix-synapse";
641 home = cfg.dataDir;
642 createHome = true;
643 shell = "${pkgs.bash}/bin/bash";
644 uid = config.ids.uids.matrix-synapse;
645 } ];
646
647 users.extraGroups = [
648 { name = "matrix-synapse";
649 gid = config.ids.gids.matrix-synapse;
650 } ];
651
652 services.postgresql.enable = mkIf usePostgresql (mkDefault true);
653
654 systemd.services.matrix-synapse = {
655 description = "Synapse Matrix homeserver";
656 after = [ "network.target" "postgresql.service" ];
657 wantedBy = [ "multi-user.target" ];
658 preStart = ''
659 ${cfg.package}/bin/homeserver \
660 --config-path ${configFile} \
661 --keys-directory ${cfg.dataDir} \
662 --generate-keys
663 '' + optionalString (usePostgresql && cfg.create_local_database) ''
664 if ! test -e "${cfg.dataDir}/db-created"; then
665 ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
666 ${pg.package}/bin/createuser \
667 --login \
668 --no-createdb \
669 --no-createrole \
670 --encrypted \
671 ${cfg.database_user}
672 ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
673 ${pg.package}/bin/createdb \
674 --owner=${cfg.database_user} \
675 --encoding=UTF8 \
676 --lc-collate=C \
677 --lc-ctype=C \
678 --template=template0 \
679 ${cfg.database_name}
680 touch "${cfg.dataDir}/db-created"
681 fi
682 '';
683 serviceConfig = {
684 Type = "simple";
685 User = "matrix-synapse";
686 Group = "matrix-synapse";
687 WorkingDirectory = cfg.dataDir;
688 PermissionsStartOnly = true;
689 ExecStart = ''
690 ${cfg.package}/bin/homeserver \
691 ${ concatMapStringsSep "\n " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
692 --keys-directory ${cfg.dataDir}
693 '';
694 Restart = "on-failure";
695 };
696 };
697 };
698}