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}