Self-host your own digital island
1{ 2 lib, 3 config, 4 pkgs, 5 ... 6}: let 7 cfg = config.services.mautrix-signal; 8 dataDir = "/var/lib/mautrix-signal"; 9 registrationFile = "${dataDir}/signal-registration.yaml"; 10 settingsFile = "${dataDir}/config.json"; 11 settingsFileUnsubstituted = settingsFormat.generate "mautrix-signal-config-unsubstituted.json" cfg.settings; 12 settingsFormat = pkgs.formats.json {}; 13 appservicePort = 29328; 14 15 mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v); 16 defaultConfig = { 17 homeserver.address = "http://localhost:8448"; 18 signal = { 19 socket_path = config.services.signald.socketPath; 20 outgoing_attachment_dir = "/var/lib/signald/tmp"; 21 }; 22 appservice = { 23 hostname = "[::]"; 24 port = appservicePort; 25 database.type = "sqlite3"; 26 database.uri = "${dataDir}/mautrix-signal.db"; 27 id = "signal"; 28 bot.username = "signalbot"; 29 bot.displayname = "Signal Bridge Bot"; 30 as_token = ""; 31 hs_token = ""; 32 }; 33 bridge = { 34 username_template = "signal_{{.}}"; 35 double_puppet_server_map = {}; 36 login_shared_secret_map = {}; 37 permissions."*" = "relay"; 38 }; 39 logging = { 40 min_level = "info"; 41 writers = lib.singleton { 42 type = "stdout"; 43 format = "pretty-colored"; 44 time_format = " "; 45 }; 46 }; 47 }; 48 49in { 50 options.services.mautrix-signal = { 51 enable = lib.mkEnableOption (lib.mdDoc "mautrix-signal, a puppeting/relaybot bridge between Matrix and Signal."); 52 53 settings = lib.mkOption { 54 type = settingsFormat.type; 55 default = defaultConfig; 56 description = lib.mdDoc '' 57 {file}`config.yaml` configuration as a Nix attribute set. 58 Configuration options should match those described in 59 [example-config.yaml](https://github.com/mautrix/signal/blob/master/example-config.yaml). 60 ''; 61 example = { 62 appservice = { 63 database = { 64 type = "postgres"; 65 uri = "postgresql:///mautrix_signal?host=/run/postgresql"; 66 }; 67 id = "signal"; 68 ephemeral_events = false; 69 }; 70 bridge = { 71 history_sync = { 72 request_full_sync = true; 73 }; 74 private_chat_portal_meta = true; 75 mute_bridging = true; 76 encryption = { 77 allow = true; 78 default = true; 79 require = true; 80 }; 81 provisioning = { 82 shared_secret = "disable"; 83 }; 84 permissions = { 85 "example.com" = "user"; 86 }; 87 }; 88 }; 89 }; 90 91 serviceDependencies = lib.mkOption { 92 type = with lib.types; listOf str; 93 default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit; 94 defaultText = lib.literalExpression '' 95 optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnits 96 ''; 97 description = lib.mdDoc '' 98 List of Systemd services to require and wait for when starting the application service. 99 ''; 100 }; 101 }; 102 103 config = lib.mkIf cfg.enable { 104 105 services.signald.enable = true; 106 107 users.users.mautrix-signal = { 108 isSystemUser = true; 109 group = "mautrix-signal"; 110 home = dataDir; 111 description = "Mautrix-Signal bridge user"; 112 }; 113 114 users.groups.mautrix-signal = {}; 115 116 services.mautrix-signal.settings = lib.mkMerge (map mkDefaults [ 117 defaultConfig 118 # Note: this is defined here to avoid the docs depending on `config` 119 { homeserver.domain = config.services.matrix-synapse.settings.server_name; } 120 ]); 121 122 systemd.services.mautrix-signal = { 123 description = "Mautrix-Signal Service - A Signal bridge for Matrix"; 124 125 requires = [ "signald.service" ]; 126 # voice messages need `ffmpeg` 127 path = [ pkgs.ffmpeg ]; 128 129 wantedBy = ["multi-user.target"]; 130 wants = ["network-online.target"] ++ cfg.serviceDependencies; 131 after = ["network-online.target" "signald.service"] ++ cfg.serviceDependencies; 132 133 preStart = '' 134 # substitute the settings file by environment variables 135 # in this case read from EnvironmentFile 136 test -f '${settingsFile}' && rm -f '${settingsFile}' 137 old_umask=$(umask) 138 umask 0177 139 ${pkgs.envsubst}/bin/envsubst \ 140 -o '${settingsFile}' \ 141 -i '${settingsFileUnsubstituted}' 142 umask $old_umask 143 144 # generate the appservice's registration file if absent 145 if [ ! -f '${registrationFile}' ]; then 146 ${pkgs.mautrix-signal}/bin/mautrix-signal \ 147 --generate-registration \ 148 --config='${settingsFile}' \ 149 --registration='${registrationFile}' 150 fi 151 chmod 640 ${registrationFile} 152 153 umask 0177 154 ${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token 155 | .[0].appservice.hs_token = .[1].hs_token 156 | .[0]' '${settingsFile}' '${registrationFile}' \ 157 > '${settingsFile}.tmp' 158 mv '${settingsFile}.tmp' '${settingsFile}' 159 umask $old_umask 160 ''; 161 162 serviceConfig = { 163 SupplementaryGroups = [ "signald" ]; 164 User = "mautrix-signal"; 165 Group = "mautrix-signal"; 166 StateDirectory = baseNameOf dataDir; 167 WorkingDirectory = dataDir; 168 ExecStart = '' 169 ${pkgs.mautrix-signal}/bin/mautrix-signal \ 170 --config='${settingsFile}' \ 171 --registration='${registrationFile}' 172 ''; 173 LockPersonality = true; 174 MemoryDenyWriteExecute = true; 175 NoNewPrivileges = true; 176 PrivateDevices = true; 177 PrivateTmp = true; 178 PrivateUsers = true; 179 ProtectClock = true; 180 ProtectControlGroups = true; 181 ProtectHome = true; 182 ProtectHostname = true; 183 ProtectKernelLogs = true; 184 ProtectKernelModules = true; 185 ProtectKernelTunables = true; 186 ProtectSystem = "strict"; 187 Restart = "on-failure"; 188 RestartSec = "30s"; 189 RestrictRealtime = true; 190 RestrictSUIDSGID = true; 191 SystemCallArchitectures = "native"; 192 SystemCallErrorNumber = "EPERM"; 193 SystemCallFilter = ["@system-service"]; 194 Type = "simple"; 195 UMask = 0027; 196 }; 197 restartTriggers = [settingsFileUnsubstituted]; 198 }; 199 }; 200}