1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.matrix-synapse;
7 logConfigFile = pkgs.writeText "log_config.yaml" cfg.logConfig;
8 mkResource = r: ''{names: ${builtins.toJSON r.names}, compress: ${if r.compress then "true" else "false"}}'';
9 mkListener = l: ''{port: ${toString l.port}, bind_address: "${l.bind_address}", type: ${l.type}, tls: ${if l.tls then "true" else "false"}, x_forwarded: ${if l.x_forwarded then "true" else "false"}, resources: [${concatStringsSep "," (map mkResource l.resources)}]}'';
10 configFile = pkgs.writeText "homeserver.yaml" ''
11tls_certificate_path: "${cfg.tls_certificate_path}"
12${optionalString (cfg.tls_private_key_path != null) ''
13tls_private_key_path: "${cfg.tls_private_key_path}"
14''}
15tls_dh_params_path: "${cfg.tls_dh_params_path}"
16no_tls: ${if cfg.no_tls then "true" else "false"}
17${optionalString (cfg.bind_port != null) ''
18bind_port: ${toString cfg.bind_port}
19''}
20${optionalString (cfg.unsecure_port != null) ''
21unsecure_port: ${toString cfg.unsecure_port}
22''}
23${optionalString (cfg.bind_host != null) ''
24bind_host: "${cfg.bind_host}"
25''}
26server_name: "${cfg.server_name}"
27pid_file: "/var/run/matrix-synapse.pid"
28web_client: ${if cfg.web_client then "true" else "false"}
29${optionalString (cfg.public_baseurl != null) ''
30public_baseurl: "${cfg.public_baseurl}"
31''}
32listeners: [${concatStringsSep "," (map mkListener cfg.listeners)}]
33database: {
34 name: "${cfg.database_type}",
35 args: {
36 ${concatStringsSep ",\n " (
37 mapAttrsToList (n: v: "\"${n}\": ${v}") cfg.database_args
38 )}
39 }
40}
41event_cache_size: "${cfg.event_cache_size}"
42verbose: ${cfg.verbose}
43log_file: "/var/log/matrix-synapse/homeserver.log"
44log_config: "${logConfigFile}"
45rc_messages_per_second: ${cfg.rc_messages_per_second}
46rc_message_burst_count: ${cfg.rc_message_burst_count}
47federation_rc_window_size: ${cfg.federation_rc_window_size}
48federation_rc_sleep_limit: ${cfg.federation_rc_sleep_limit}
49federation_rc_sleep_delay: ${cfg.federation_rc_sleep_delay}
50federation_rc_reject_limit: ${cfg.federation_rc_reject_limit}
51federation_rc_concurrent: ${cfg.federation_rc_concurrent}
52media_store_path: "/var/lib/matrix-synapse/media"
53uploads_path: "/var/lib/matrix-synapse/uploads"
54max_upload_size: "${cfg.max_upload_size}"
55max_image_pixels: "${cfg.max_image_pixels}"
56dynamic_thumbnails: ${if cfg.dynamic_thumbnails then "true" else "false"}
57url_preview_enabled: False
58recaptcha_private_key: "${cfg.recaptcha_private_key}"
59recaptcha_public_key: "${cfg.recaptcha_public_key}"
60enable_registration_captcha: ${if cfg.enable_registration_captcha then "true" else "false"}
61turn_uris: ${builtins.toJSON cfg.turn_uris}
62turn_shared_secret: "${cfg.turn_shared_secret}"
63enable_registration: ${if cfg.enable_registration then "true" else "false"}
64${optionalString (cfg.registration_shared_secret != null) ''
65registration_shared_secret: "${cfg.registration_shared_secret}"
66''}
67recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify"
68turn_user_lifetime: "${cfg.turn_user_lifetime}"
69user_creation_max_duration: ${cfg.user_creation_max_duration}
70bcrypt_rounds: ${cfg.bcrypt_rounds}
71allow_guest_access: {if cfg.allow_guest_access then "true" else "false"}
72enable_metrics: ${if cfg.enable_metrics then "true" else "false"}
73report_stats: ${if cfg.report_stats then "true" else "false"}
74signing_key_path: "/var/lib/matrix-synapse/homeserver.signing.key"
75key_refresh_interval: "${cfg.key_refresh_interval}"
76perspectives:
77 servers: {
78 ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
79 "${n}": {
80 "verify_keys": {
81 ${concatStringsSep "},\n" (mapAttrsToList (n: v: ''
82 "${n}": {
83 "key": "${v}"
84 }'') v)}
85 }
86 '') cfg.servers)}
87 }
88 }
89app_service_config_files: ${builtins.toJSON cfg.app_service_config_files}
90
91${cfg.extraConfig}
92'';
93in {
94 options = {
95 services.matrix-synapse = {
96 enable = mkEnableOption "matrix.org synapse";
97 package = mkOption {
98 type = types.package;
99 default = pkgs.matrix-synapse;
100 defaultText = "pkgs.matrix-synapse";
101 description = ''
102 Overridable attribute of the matrix synapse server package to use.
103 '';
104 };
105 no_tls = mkOption {
106 type = types.bool;
107 default = false;
108 description = ''
109 Don't bind to the https port
110 '';
111 };
112 bind_port = mkOption {
113 type = types.nullOr types.int;
114 default = null;
115 example = 8448;
116 description = ''
117 DEPRECATED: Use listeners instead.
118 The port to listen for HTTPS requests on.
119 For when matrix traffic is sent directly to synapse.
120 '';
121 };
122 unsecure_port = mkOption {
123 type = types.nullOr types.int;
124 default = null;
125 example = 8008;
126 description = ''
127 DEPRECATED: Use listeners instead.
128 The port to listen for HTTP requests on.
129 For when matrix traffic passes through loadbalancer that unwraps TLS.
130 '';
131 };
132 bind_host = mkOption {
133 type = types.nullOr types.str;
134 default = null;
135 description = ''
136 DEPRECATED: Use listeners instead.
137 Local interface to listen on.
138 The empty string will cause synapse to listen on all interfaces.
139 '';
140 };
141 tls_certificate_path = mkOption {
142 type = types.str;
143 default = "/var/lib/matrix-synapse/homeserver.tls.crt";
144 description = ''
145 PEM encoded X509 certificate for TLS.
146 You can replace the self-signed certificate that synapse
147 autogenerates on launch with your own SSL certificate + key pair
148 if you like. Any required intermediary certificates can be
149 appended after the primary certificate in hierarchical order.
150 '';
151 };
152 tls_private_key_path = mkOption {
153 type = types.nullOr types.str;
154 default = "/var/lib/matrix-synapse/homeserver.tls.key";
155 example = null;
156 description = ''
157 PEM encoded private key for TLS. Specify null if synapse is not
158 speaking TLS directly.
159 '';
160 };
161 tls_dh_params_path = mkOption {
162 type = types.str;
163 default = "/var/lib/matrix-synapse/homeserver.tls.dh";
164 description = ''
165 PEM dh parameters for ephemeral keys
166 '';
167 };
168 server_name = mkOption {
169 type = types.str;
170 example = "example.com";
171 description = ''
172 The domain name of the server, with optional explicit port.
173 This is used by remote servers to connect to this server,
174 e.g. matrix.org, localhost:8080, etc.
175 This is also the last part of your UserID.
176 '';
177 };
178 web_client = mkOption {
179 type = types.bool;
180 default = false;
181 description = ''
182 Whether to serve a web client from the HTTP/HTTPS root resource.
183 '';
184 };
185 public_baseurl = mkOption {
186 type = types.nullOr types.str;
187 default = null;
188 example = "https://example.com:8448/";
189 description = ''
190 The public-facing base URL for the client API (not including _matrix/...)
191 '';
192 };
193 listeners = mkOption {
194 type = types.listOf (types.submodule {
195 options = {
196 port = mkOption {
197 type = types.int;
198 example = 8448;
199 description = ''
200 The port to listen for HTTP(S) requests on.
201 '';
202 };
203 bind_address = mkOption {
204 type = types.str;
205 default = "";
206 example = "203.0.113.42";
207 description = ''
208 Local interface to listen on.
209 The empty string will cause synapse to listen on all interfaces.
210 '';
211 };
212 type = mkOption {
213 type = types.str;
214 default = "http";
215 description = ''
216 Type of listener.
217 '';
218 };
219 tls = mkOption {
220 type = types.bool;
221 default = true;
222 description = ''
223 Whether to listen for HTTPS connections rather than HTTP.
224 '';
225 };
226 x_forwarded = mkOption {
227 type = types.bool;
228 default = false;
229 description = ''
230 Use the X-Forwarded-For (XFF) header as the client IP and not the
231 actual client IP.
232 '';
233 };
234 resources = mkOption {
235 type = types.listOf (types.submodule {
236 options = {
237 names = mkOption {
238 type = types.listOf types.str;
239 description = ''
240 List of resources to host on this listener.
241 '';
242 example = ["client" "webclient" "federation"];
243 };
244 compress = mkOption {
245 type = types.bool;
246 description = ''
247 Should synapse compress HTTP responses to clients that support it?
248 This should be disabled if running synapse behind a load balancer
249 that can do automatic compression.
250 '';
251 };
252 };
253 });
254 description = ''
255 List of HTTP resources to serve on this listener.
256 '';
257 };
258 };
259 });
260 default = [{
261 port = 8448;
262 bind_address = "";
263 type = "http";
264 tls = true;
265 x_forwarded = false;
266 resources = [
267 { names = ["client" "webclient"]; compress = true; }
268 { names = ["federation"]; compress = false; }
269 ];
270 }];
271 description = ''
272 List of ports that Synapse should listen on, their purpose and their configuration.
273 '';
274 };
275 verbose = mkOption {
276 type = types.str;
277 default = "0";
278 description = "Logging verbosity level.";
279 };
280 rc_messages_per_second = mkOption {
281 type = types.str;
282 default = "0.2";
283 description = "Number of messages a client can send per second";
284 };
285 rc_message_burst_count = mkOption {
286 type = types.str;
287 default = "10.0";
288 description = "Number of message a client can send before being throttled";
289 };
290 federation_rc_window_size = mkOption {
291 type = types.str;
292 default = "1000";
293 description = "The federation window size in milliseconds";
294 };
295 federation_rc_sleep_limit = mkOption {
296 type = types.str;
297 default = "10";
298 description = ''
299 The number of federation requests from a single server in a window
300 before the server will delay processing the request.
301 '';
302 };
303 federation_rc_sleep_delay = mkOption {
304 type = types.str;
305 default = "500";
306 description = ''
307 The duration in milliseconds to delay processing events from
308 remote servers by if they go over the sleep limit.
309 '';
310 };
311 federation_rc_reject_limit = mkOption {
312 type = types.str;
313 default = "50";
314 description = ''
315 The maximum number of concurrent federation requests allowed
316 from a single server
317 '';
318 };
319 federation_rc_concurrent = mkOption {
320 type = types.str;
321 default = "3";
322 description = "The number of federation requests to concurrently process from a single server";
323 };
324 database_type = mkOption {
325 type = types.enum [ "sqlite3" "psycopg2" ];
326 default = "sqlite3";
327 description = ''
328 The database engine name. Can be sqlite or psycopg2.
329 '';
330 };
331 database_args = mkOption {
332 type = types.attrs;
333 default = {
334 database = "/var/lib/matrix-synapse/homeserver.db";
335 };
336 description = ''
337 Arguments to pass to the engine.
338 '';
339 };
340 event_cache_size = mkOption {
341 type = types.str;
342 default = "10K";
343 description = "Number of events to cache in memory.";
344 };
345 recaptcha_private_key = mkOption {
346 type = types.str;
347 default = "";
348 description = ''
349 This Home Server's ReCAPTCHA private key.
350 '';
351 };
352 recaptcha_public_key = mkOption {
353 type = types.str;
354 default = "";
355 description = ''
356 This Home Server's ReCAPTCHA public key.
357 '';
358 };
359 enable_registration_captcha = mkOption {
360 type = types.bool;
361 default = false;
362 description = ''
363 Enables ReCaptcha checks when registering, preventing signup
364 unless a captcha is answered. Requires a valid ReCaptcha
365 public/private key.
366 '';
367 };
368 turn_uris = mkOption {
369 type = types.listOf types.str;
370 default = [];
371 description = ''
372 The public URIs of the TURN server to give to clients
373 '';
374 };
375 turn_shared_secret = mkOption {
376 type = types.str;
377 default = "";
378 description = ''
379 The shared secret used to compute passwords for the TURN server
380 '';
381 };
382 turn_user_lifetime = mkOption {
383 type = types.str;
384 default = "1h";
385 description = "How long generated TURN credentials last";
386 };
387 enable_registration = mkOption {
388 type = types.bool;
389 default = false;
390 description = ''
391 Enable registration for new users.
392 '';
393 };
394 registration_shared_secret = mkOption {
395 type = types.nullOr types.str;
396 default = null;
397 description = ''
398 If set, allows registration by anyone who also has the shared
399 secret, even if registration is otherwise disabled.
400 '';
401 };
402 enable_metrics = mkOption {
403 type = types.bool;
404 default = false;
405 description = ''
406 Enable collection and rendering of performance metrics
407 '';
408 };
409 report_stats = mkOption {
410 type = types.bool;
411 default = false;
412 description = ''
413 '';
414 };
415 servers = mkOption {
416 type = types.attrsOf (types.attrsOf types.str);
417 default = {
418 "matrix.org" = {
419 "ed25519:auto" = "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw";
420 };
421 };
422 description = ''
423 The trusted servers to download signing keys from.
424 '';
425 };
426 max_upload_size = mkOption {
427 type = types.str;
428 default = "10M";
429 description = "The largest allowed upload size in bytes";
430 };
431 max_image_pixels = mkOption {
432 type = types.str;
433 default = "32M";
434 description = "Maximum number of pixels that will be thumbnailed";
435 };
436 dynamic_thumbnails = mkOption {
437 type = types.bool;
438 default = false;
439 description = ''
440 Whether to generate new thumbnails on the fly to precisely match
441 the resolution requested by the client. If true then whenever
442 a new resolution is requested by the client the server will
443 generate a new thumbnail. If false the server will pick a thumbnail
444 from a precalculated list.
445 '';
446 };
447 user_creation_max_duration = mkOption {
448 type = types.str;
449 default = "1209600000";
450 description = ''
451 Sets the expiry for the short term user creation in
452 milliseconds. The default value is two weeks.
453 '';
454 };
455 bcrypt_rounds = mkOption {
456 type = types.str;
457 default = "12";
458 description = ''
459 Set the number of bcrypt rounds used to generate password hash.
460 Larger numbers increase the work factor needed to generate the hash.
461 '';
462 };
463 allow_guest_access = mkOption {
464 type = types.bool;
465 default = false;
466 description = ''
467 Allows users to register as guests without a password/email/etc, and
468 participate in rooms hosted on this server which have been made
469 accessible to anonymous users.
470 '';
471 };
472 key_refresh_interval = mkOption {
473 type = types.str;
474 default = "1d";
475 description = ''
476 How long key response published by this server is valid for.
477 Used to set the valid_until_ts in /key/v2 APIs.
478 Determines how quickly servers will query to check which keys
479 are still valid.
480 '';
481 };
482 app_service_config_files = mkOption {
483 type = types.listOf types.path;
484 default = [ ];
485 description = ''
486 A list of application service config file to use
487 '';
488 };
489 extraConfig = mkOption {
490 type = types.lines;
491 default = "";
492 description = ''
493 Extra config options for matrix-synapse.
494 '';
495 };
496 logConfig = mkOption {
497 type = types.lines;
498 default = readFile ./matrix-synapse-log_config.yaml;
499 description = ''
500 A yaml python logging config file
501 '';
502 };
503 };
504 };
505
506 config = mkIf cfg.enable {
507 users.extraUsers = [
508 { name = "matrix-synapse";
509 group = "matrix-synapse";
510 home = "/var/lib/matrix-synapse";
511 createHome = true;
512 shell = "${pkgs.bash}/bin/bash";
513 uid = config.ids.uids.matrix-synapse;
514 } ];
515
516 users.extraGroups = [
517 { name = "matrix-synapse";
518 gid = config.ids.gids.matrix-synapse;
519 } ];
520
521 systemd.services.matrix-synapse = {
522 after = [ "network.target" ];
523 wantedBy = [ "multi-user.target" ];
524 preStart = ''
525 if ! test -e /var/lib/matrix-synapse; then
526 mkdir -p /var/lib/matrix-synapse
527 chmod 700 /var/lib/matrix-synapse
528 chown -R matrix-synapse:matrix-synapse /var/lib/matrix-synapse
529 ${cfg.package}/bin/homeserver --config-path ${configFile} --keys-directory /var/lib/matrix-synapse/ --generate-keys
530 fi
531 '';
532 serviceConfig = {
533 Type = "simple";
534 User = "matrix-synapse";
535 Group = "matrix-synapse";
536 WorkingDirectory = "/var/lib/matrix-synapse";
537 PermissionsStartOnly = true;
538 ExecStart = "${cfg.package}/bin/homeserver --config-path ${configFile}";
539 };
540 };
541 };
542}