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