Self-host your own digital island
1{ config, pkgs, lib, ... }:
2
3with lib;
4let
5 cfg = config.eilean;
6 turnSharedSecretFile = "/run/matrix-synapse/turn-shared-secret";
7 domain = config.networking.domain;
8 subdomain = "matrix.${domain}";
9in {
10 options.eilean.matrix = {
11 enable = mkEnableOption "matrix";
12 turn = mkOption {
13 type = types.bool;
14 default = true;
15 };
16 registrationSecretFile = mkOption {
17 type = types.nullOr types.str;
18 default = null;
19 };
20 bridges = {
21 whatsapp = mkOption {
22 type = types.bool;
23 default = false;
24 description = "Enable WhatsApp bridge.";
25 };
26 signal = mkOption {
27 type = types.bool;
28 default = false;
29 description = "Enable Signal bridge.";
30 };
31 instagram = mkOption {
32 type = types.bool;
33 default = false;
34 description = "Enable Instagram bridge.";
35 };
36 messenger = mkOption {
37 type = types.bool;
38 default = false;
39 description = "Enable Facebook Messenger bridge.";
40 };
41 };
42 slidingSync = {
43 enable = mkEnableOption "sliding-sync";
44 secretFile = mkOption {
45 type = types.nullOr types.str;
46 default = null;
47 };
48 };
49 };
50
51 config = mkIf cfg.matrix.enable {
52 services.postgresql.enable = true;
53 services.postgresql.package = pkgs.postgresql_13;
54 services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
55 CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
56 CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
57 TEMPLATE template0
58 LC_COLLATE = "C"
59 LC_CTYPE = "C";
60 '';
61
62 security.acme-eon.nginxCerts = lib.mkIf cfg.acme-eon [ domain subdomain ];
63
64 services.nginx = {
65 enable = true;
66 # only recommendedProxySettings and recommendedGzipSettings are strictly required,
67 # but the rest make sense as well
68 recommendedTlsSettings = true;
69 recommendedOptimisation = true;
70 recommendedGzipSettings = true;
71 recommendedProxySettings = true;
72
73 virtualHosts = {
74 # This host section can be placed on a different host than the rest,
75 # i.e. to delegate from the host being accessible as ${domain}
76 # to another host actually running the Matrix homeserver.
77 "${domain}" = {
78 enableACME = lib.mkIf (!cfg.acme-eon) true;
79 forceSSL = true;
80
81 locations."= /.well-known/matrix/server".extraConfig = let
82 # use 443 instead of the default 8448 port to unite
83 # the client-server and server-server port for simplicity
84 server = { "m.server" = "${subdomain}:443"; };
85 in ''
86 default_type application/json;
87 return 200 '${builtins.toJSON server}';
88 '';
89 locations."= /.well-known/matrix/client".extraConfig = let
90 client = {
91 "m.homeserver" = { "base_url" = "https://${subdomain}"; };
92 "m.identity_server" = { "base_url" = "https://vector.im"; };
93 "org.matrix.msc3575.proxy" = if cfg.matrix.slidingSync.enable then {
94 "url" = "https://${subdomain}";
95 } else
96 { };
97 };
98 # ACAO required to allow element-web on any URL to request this json file
99 # set other headers due to https://github.com/yandex/gixy/blob/master/docs/en/plugins/addheaderredefinition.md
100 in ''
101 default_type application/json;
102 add_header Access-Control-Allow-Origin *;
103 add_header Strict-Transport-Security max-age=31536000 always;
104 add_header X-Frame-Options SAMEORIGIN always;
105 add_header X-Content-Type-Options nosniff always;
106 add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-src 'self'; frame-ancestors 'self'; form-action 'self';" always;
107 add_header Referrer-Policy 'same-origin';
108 return 200 '${builtins.toJSON client}';
109 '';
110 };
111
112 # Reverse proxy for Matrix client-server and server-server communication
113 "${subdomain}" = {
114 enableACME = lib.mkIf (!cfg.acme-eon) true;
115 forceSSL = true;
116
117 # Or do a redirect instead of the 404, or whatever is appropriate for you.
118 # But do not put a Matrix Web client here! See the Element web section below.
119 locations."/".extraConfig = ''
120 return 404;
121 '';
122
123 # forward all Matrix API calls to the synapse Matrix homeserver
124 locations."~ ^(\\/_matrix|\\/_synapse\\/client)" = {
125 proxyPass = "http://127.0.0.1:8008";
126 };
127 # forward all sliding sync API calls to the sliding sync server
128 locations."~ ^/(client/|_matrix/client/unstable/org.matrix.msc3575/sync)" =
129 mkIf cfg.matrix.slidingSync.enable {
130 priority = 100;
131 proxyPass = "http://127.0.0.1:8009";
132 };
133 };
134 };
135 };
136
137 services.matrix-synapse = {
138 enable = true;
139 settings = mkMerge [
140 {
141 server_name = domain;
142 enable_registration = true;
143 registration_requires_token = true;
144 registration_shared_secret_path = cfg.matrix.registrationSecretFile;
145 listeners = [{
146 port = 8008;
147 bind_addresses = [ "::1" "127.0.0.1" ];
148 type = "http";
149 tls = false;
150 x_forwarded = true;
151 resources = [{
152 names = [ "client" "federation" ];
153 compress = false;
154 }];
155 }];
156 max_upload_size = "100M";
157 app_service_config_files = (optional cfg.matrix.bridges.whatsapp
158 "/var/lib/mautrix-whatsapp/whatsapp-registration.yaml")
159 ++ (optional cfg.matrix.bridges.instagram
160 "/var/lib/mautrix-instagram/instagram-registration.yaml")
161 ++ (optional cfg.matrix.bridges.messenger
162 "/var/lib/mautrix-messenger/messenger-registration.yaml");
163 }
164 (mkIf cfg.matrix.turn {
165 turn_uris = with config.services.coturn; [
166 "turn:${realm}:3478?transport=udp"
167 "turn:${realm}:3478?transport=tcp"
168 "turns:${realm}:5349?transport=udp"
169 "turns:${realm}:5349?transport=tcp"
170 ];
171 turn_user_lifetime = "1h";
172 })
173 ];
174 extraConfigFiles = mkIf cfg.matrix.turn ([ turnSharedSecretFile ]);
175 };
176
177 systemd.services.matrix-synapse-turn-shared-secret-generator =
178 mkIf cfg.matrix.turn {
179 description = "Generate matrix synapse turn shared secret config file";
180 script = ''
181 mkdir -p "$(dirname '${turnSharedSecretFile}')"
182 echo "turn_shared_secret: $(cat '${config.services.coturn.static-auth-secret-file}')" > '${turnSharedSecretFile}'
183 chmod 770 '${turnSharedSecretFile}'
184 chown ${config.systemd.services.matrix-synapse.serviceConfig.User}:${config.systemd.services.matrix-synapse.serviceConfig.Group} '${turnSharedSecretFile}'
185 '';
186 serviceConfig.Type = "oneshot";
187 serviceConfig.RemainAfterExit = true;
188 after = [ "coturn-static-auth-secret-generator.service" ];
189 requires = [ "coturn-static-auth-secret-generator.service" ];
190 };
191 systemd.services."matrix-synapse".after = mkIf cfg.matrix.turn
192 [ "matrix-synapse-turn-shared-secret-generator.service" ];
193 systemd.services."matrix-synapse".requires = mkIf cfg.matrix.turn
194 [ "matrix-synapse-turn-shared-secret-generator.service" ];
195
196 systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups =
197 # remove after https://github.com/NixOS/nixpkgs/pull/311681/files
198 (optional cfg.matrix.bridges.whatsapp
199 config.systemd.services.mautrix-whatsapp.serviceConfig.Group)
200 ++ (optional cfg.matrix.bridges.instagram
201 config.systemd.services.mautrix-instagram.serviceConfig.Group)
202 ++ (optional cfg.matrix.bridges.messenger
203 config.systemd.services.mautrix-messenger.serviceConfig.Group);
204
205 services.mautrix-whatsapp = mkIf cfg.matrix.bridges.whatsapp {
206 enable = true;
207 settings.homeserver.address = "https://${subdomain}";
208 settings.homeserver.domain = domain;
209 settings.appservice.hostname = "localhost";
210 settings.appservice.address = "http://localhost:29318";
211 settings.bridge.personal_filtering_spaces = true;
212 settings.bridge.history_sync.backfill = false;
213 settings.bridge.permissions."@${config.eilean.username}:${domain}" =
214 "admin";
215 settings.bridge.encryption.allow = true;
216 settings.bridge.encryption.default = true;
217 };
218 # using https://github.com/NixOS/nixpkgs/pull/277368
219 services.mautrix-signal = mkIf cfg.matrix.bridges.signal {
220 enable = true;
221 settings.homeserver.address = "https://${subdomain}";
222 settings.homeserver.domain = domain;
223 settings.appservice.hostname = "localhost";
224 settings.appservice.address = "http://localhost:29328";
225 settings.bridge.personal_filtering_spaces = true;
226 settings.bridge.permissions."@${config.eilean.username}:${domain}" =
227 "admin";
228 settings.bridge.encryption.allow = true;
229 settings.bridge.encryption.default = true;
230 };
231 # TODO replace with upstreamed mautrix-meta
232 services.mautrix-instagram = mkIf cfg.matrix.bridges.instagram {
233 enable = true;
234 settings.homeserver.address = "https://${subdomain}";
235 settings.homeserver.domain = domain;
236 settings.appservice.hostname = "localhost";
237 settings.appservice.address = "http://localhost:29319";
238 settings.bridge.personal_filtering_spaces = true;
239 settings.bridge.backfill.enabled = false;
240 settings.bridge.permissions."@${config.eilean.username}:${domain}" =
241 "admin";
242 settings.bridge.encryption.allow = true;
243 settings.bridge.encryption.default = true;
244 };
245 services.mautrix-messenger = mkIf cfg.matrix.bridges.messenger {
246 enable = true;
247 settings.homeserver.address = "https://${subdomain}";
248 settings.homeserver.domain = domain;
249 settings.appservice.hostname = "localhost";
250 settings.appservice.address = "http://localhost:29320";
251 settings.bridge.personal_filtering_spaces = true;
252 settings.bridge.backfill.enabled = false;
253 settings.bridge.permissions."@${config.eilean.username}:${domain}" =
254 "admin";
255 settings.bridge.encryption.allow = true;
256 settings.bridge.encryption.default = true;
257 };
258
259 eilean.turn.enable = mkIf cfg.matrix.turn true;
260
261 eilean.dns.enable = true;
262 eilean.services.dns.zones.${domain}.records = [{
263 name = "matrix";
264 type = "CNAME";
265 value = cfg.domainName;
266 }];
267
268 services.matrix-sliding-sync = mkIf cfg.matrix.slidingSync.enable {
269 enable = true;
270 environmentFile = cfg.matrix.slidingSync.secretFile;
271 settings = { SYNCV3_SERVER = "https://${subdomain}"; };
272 };
273 };
274}