at 25.11-pre 10 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.cryptpad; 10 11 inherit (lib) 12 mkIf 13 mkMerge 14 mkOption 15 strings 16 types 17 ; 18 19 # The Cryptpad configuration file isn't JSON, but a JavaScript source file that assigns a JSON value 20 # to a variable. 21 cryptpadConfigFile = builtins.toFile "cryptpad_config.js" '' 22 module.exports = ${builtins.toJSON cfg.settings} 23 ''; 24 25 # Derive domain names for Nginx configuration from Cryptpad configuration 26 mainDomain = strings.removePrefix "https://" cfg.settings.httpUnsafeOrigin; 27 sandboxDomain = 28 if cfg.settings.httpSafeOrigin == null then 29 mainDomain 30 else 31 strings.removePrefix "https://" cfg.settings.httpSafeOrigin; 32 33in 34{ 35 options.services.cryptpad = { 36 enable = lib.mkEnableOption "cryptpad"; 37 38 package = lib.mkPackageOption pkgs "cryptpad" { }; 39 40 configureNginx = mkOption { 41 description = '' 42 Configure Nginx as a reverse proxy for Cryptpad. 43 Note that this makes some assumptions on your setup, and sets settings that will 44 affect other virtualHosts running on your Nginx instance, if any. 45 Alternatively you can configure a reverse-proxy of your choice. 46 ''; 47 type = types.bool; 48 default = false; 49 }; 50 51 settings = mkOption { 52 description = '' 53 Cryptpad configuration settings. 54 See <https://github.com/cryptpad/cryptpad/blob/main/config/config.example.js> for a more extensive 55 reference documentation. 56 Test your deployed instance through `https://<domain>/checkup/`. 57 ''; 58 type = types.submodule { 59 freeformType = (pkgs.formats.json { }).type; 60 options = { 61 httpUnsafeOrigin = mkOption { 62 type = types.str; 63 example = "https://cryptpad.example.com"; 64 default = ""; 65 description = "This is the URL that users will enter to load your instance"; 66 }; 67 httpSafeOrigin = mkOption { 68 type = types.nullOr types.str; 69 example = "https://cryptpad-ui.example.com. Apparently optional but recommended."; 70 description = "Cryptpad sandbox URL"; 71 }; 72 httpAddress = mkOption { 73 type = types.str; 74 default = "127.0.0.1"; 75 description = "Address on which the Node.js server should listen"; 76 }; 77 httpPort = mkOption { 78 type = types.int; 79 default = 3000; 80 description = "Port on which the Node.js server should listen"; 81 }; 82 websocketPort = mkOption { 83 type = types.int; 84 default = 3003; 85 description = "Port for the websocket that needs to be separate"; 86 }; 87 maxWorkers = mkOption { 88 type = types.nullOr types.int; 89 default = null; 90 description = "Number of child processes, defaults to number of cores available"; 91 }; 92 adminKeys = mkOption { 93 type = types.listOf types.str; 94 default = [ ]; 95 description = "List of public signing keys of users that can access the admin panel"; 96 example = [ "[cryptpad-user1@my.awesome.website/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=]" ]; 97 }; 98 logToStdout = mkOption { 99 type = types.bool; 100 default = true; 101 description = "Controls whether log output should go to stdout of the systemd service"; 102 }; 103 logLevel = mkOption { 104 type = types.str; 105 default = "info"; 106 description = "Controls log level"; 107 }; 108 blockDailyCheck = mkOption { 109 type = types.bool; 110 default = true; 111 description = '' 112 Disable telemetry. This setting is only effective if the 'Disable server telemetry' 113 setting in the admin menu has been untouched, and will be ignored by cryptpad once 114 that option is set either way. 115 Note that due to the service confinement, just enabling the option in the admin 116 menu will not be able to resolve DNS and fail; this setting must be set as well. 117 ''; 118 }; 119 installMethod = mkOption { 120 type = types.str; 121 default = "nixos"; 122 description = '' 123 Install method is listed in telemetry if you agree to it through the consentToContact 124 setting in the admin panel. 125 ''; 126 }; 127 }; 128 }; 129 }; 130 }; 131 132 config = mkIf cfg.enable (mkMerge [ 133 { 134 systemd.services.cryptpad = { 135 description = "Cryptpad service"; 136 wantedBy = [ "multi-user.target" ]; 137 after = [ "networking.target" ]; 138 serviceConfig = { 139 BindReadOnlyPaths = [ 140 cryptpadConfigFile 141 # apparently needs proc for workers management 142 "/proc" 143 "/dev/urandom" 144 ]; 145 DynamicUser = true; 146 Environment = [ 147 "CRYPTPAD_CONFIG=${cryptpadConfigFile}" 148 "HOME=%S/cryptpad" 149 ]; 150 ExecStart = lib.getExe cfg.package; 151 Restart = "always"; 152 StateDirectory = "cryptpad"; 153 WorkingDirectory = "%S/cryptpad"; 154 # security way too many numerous options, from the systemd-analyze security output 155 # at end of test: block everything except 156 # - SystemCallFiters=@resources is required for node 157 # - MemoryDenyWriteExecute for node JIT 158 # - RestrictAddressFamilies=~AF_(INET|INET6) / PrivateNetwork to bind to sockets 159 # - IPAddressDeny likewise allow localhost if binding to localhost or any otherwise 160 # - PrivateUsers somehow service doesn't start with that 161 # - DeviceAllow (char-rtc r added by ProtectClock) 162 AmbientCapabilities = ""; 163 CapabilityBoundingSet = ""; 164 DeviceAllow = ""; 165 LockPersonality = true; 166 NoNewPrivileges = true; 167 PrivateDevices = true; 168 PrivateTmp = true; 169 ProcSubset = "pid"; 170 ProtectClock = true; 171 ProtectControlGroups = true; 172 ProtectHome = true; 173 ProtectHostname = true; 174 ProtectKernelLogs = true; 175 ProtectKernelModules = true; 176 ProtectKernelTunables = true; 177 ProtectProc = "invisible"; 178 ProtectSystem = "strict"; 179 RemoveIPC = true; 180 RestrictAddressFamilies = [ 181 "AF_INET" 182 "AF_INET6" 183 ]; 184 RestrictNamespaces = true; 185 RestrictRealtime = true; 186 RestrictSUIDSGID = true; 187 RuntimeDirectoryMode = "700"; 188 SocketBindAllow = [ 189 "tcp:${builtins.toString cfg.settings.httpPort}" 190 "tcp:${builtins.toString cfg.settings.websocketPort}" 191 ]; 192 SocketBindDeny = [ "any" ]; 193 StateDirectoryMode = "0700"; 194 SystemCallArchitectures = "native"; 195 SystemCallFilter = [ 196 "@pkey" 197 "@system-service" 198 # /!\ order matters: @privileged contains @chown, so we need 199 # @privileged negated before we re-list @chown for libuv copy 200 "~@privileged" 201 "~@chown:EPERM" 202 "~@keyring" 203 "~@memlock" 204 "~@resources" 205 "~@setuid" 206 "~@timer" 207 ]; 208 UMask = "0077"; 209 }; 210 confinement = { 211 enable = true; 212 binSh = null; 213 mode = "chroot-only"; 214 }; 215 }; 216 } 217 # block external network access if not phoning home and 218 # binding to localhost (default) 219 (mkIf 220 ( 221 cfg.settings.blockDailyCheck 222 && (builtins.elem cfg.settings.httpAddress [ 223 "127.0.0.1" 224 "::1" 225 ]) 226 ) 227 { 228 systemd.services.cryptpad = { 229 serviceConfig = { 230 IPAddressAllow = [ "localhost" ]; 231 IPAddressDeny = [ "any" ]; 232 }; 233 }; 234 } 235 ) 236 # .. conversely allow DNS & TLS if telemetry is explicitly enabled 237 (mkIf (!cfg.settings.blockDailyCheck) { 238 systemd.services.cryptpad = { 239 serviceConfig = { 240 BindReadOnlyPaths = [ 241 "-/etc/resolv.conf" 242 "-/run/systemd" 243 "/etc/hosts" 244 "${config.security.pki.caBundle}:/etc/ssl/certs/ca-certificates.crt" 245 ]; 246 }; 247 }; 248 }) 249 250 (mkIf cfg.configureNginx { 251 assertions = [ 252 { 253 assertion = cfg.settings.httpUnsafeOrigin != ""; 254 message = "services.cryptpad.settings.httpUnsafeOrigin is required"; 255 } 256 { 257 assertion = strings.hasPrefix "https://" cfg.settings.httpUnsafeOrigin; 258 message = "services.cryptpad.settings.httpUnsafeOrigin must start with https://"; 259 } 260 { 261 assertion = 262 cfg.settings.httpSafeOrigin == null || strings.hasPrefix "https://" cfg.settings.httpSafeOrigin; 263 message = "services.cryptpad.settings.httpSafeOrigin must start with https:// (or be unset)"; 264 } 265 ]; 266 services.nginx = { 267 enable = true; 268 recommendedTlsSettings = true; 269 recommendedProxySettings = true; 270 recommendedOptimisation = true; 271 recommendedGzipSettings = true; 272 273 virtualHosts = mkMerge [ 274 { 275 "${mainDomain}" = { 276 serverAliases = lib.optionals (cfg.settings.httpSafeOrigin != null) [ sandboxDomain ]; 277 enableACME = lib.mkDefault true; 278 forceSSL = true; 279 locations."/" = { 280 proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.httpPort}"; 281 extraConfig = '' 282 client_max_body_size 150m; 283 ''; 284 }; 285 locations."/cryptpad_websocket" = { 286 proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.websocketPort}"; 287 proxyWebsockets = true; 288 }; 289 }; 290 } 291 ]; 292 }; 293 }) 294 ]); 295}