lol

nixos/cryptpad: init

This is a full rewrite independent of the previously removed cryptpad
module, managing cryptpad's config in RFC0042 along with a shiny test.

Upstream cryptpad provides two nginx configs, with many optimizations
and complex settings; this uses the easier variant for now but
improvements (e.g. serving blocks and js files directly through nginx)
should be possible with a bit of work and care about http headers.

the /checkup page of cryptpad passes all tests except HSTS, we don't
seem to have any nginx config with HSTS enabled in nixpkgs so leave this
as is for now.

Co-authored-by: Pol Dellaiera <pol.dellaiera@protonmail.com>
Co-authored-by: Michael Smith <shmitty@protonmail.com>

+287 -1
+2
nixos/doc/manual/release-notes/rl-2411.section.md
··· 32 32 33 33 - [Localsend](https://localsend.org/), an open source cross-platform alternative to AirDrop. Available as [programs.localsend](#opt-programs.localsend.enable). 34 34 35 + - [cryptpad](https://cryptpad.org/), a privacy-oriented collaborative platform (docs/drive/etc), has been added back. Available as [services.cryptpad](#opt-services.cryptpad.enable). 36 + 35 37 - [realm](https://github.com/zhboner/realm), a simple, high performance relay server written in rust. Available as [services.realm.enable](#opt-services.realm.enable). 36 38 37 39 - [Playerctld](https://github.com/altdesktop/playerctl), a daemon to track media player activity. Available as [services.playerctld](option.html#opt-services.playerctld).
+1
nixos/modules/module-list.nix
··· 1371 1371 ./services/web-apps/convos.nix 1372 1372 ./services/web-apps/crabfit.nix 1373 1373 ./services/web-apps/davis.nix 1374 + ./services/web-apps/cryptpad.nix 1374 1375 ./services/web-apps/dex.nix 1375 1376 ./services/web-apps/discourse.nix 1376 1377 ./services/web-apps/documize.nix
-1
nixos/modules/rename.nix
··· 116 116 (mkRemovedOptionModule [ "services" "virtuoso" ] "The corresponding package was removed from nixpkgs.") 117 117 (mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.") 118 118 (mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.") 119 - (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.") 120 119 (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.") 121 120 (mkRemovedOptionModule [ "services" "prayer" ] "The corresponding package was removed from nixpkgs.") 122 121 (mkRemovedOptionModule [ "services" "restya-board" ] "The corresponding package was removed from nixpkgs.")
+215
nixos/modules/services/web-apps/cryptpad.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + 8 + let 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 + 33 + in 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 + ] ++ (if ! cfg.settings.blockDailyCheck then [ 145 + # allow DNS & TLS if telemetry is explicitly enabled 146 + "-/etc/resolv.conf" 147 + "-/run/systemd" 148 + "/etc/hosts" 149 + "/etc/ssl/certs/ca-certificates.crt" 150 + ] else []); 151 + DynamicUser = true; 152 + Environment = [ 153 + "CRYPTPAD_CONFIG=${cryptpadConfigFile}" 154 + "HOME=%S/cryptpad" 155 + ]; 156 + ExecStart = lib.getExe cfg.package; 157 + PrivateTmp = true; 158 + Restart = "always"; 159 + StateDirectory = "cryptpad"; 160 + WorkingDirectory = "%S/cryptpad"; 161 + }; 162 + confinement = { 163 + enable = true; 164 + binSh = null; 165 + mode = "chroot-only"; 166 + }; 167 + }; 168 + } 169 + 170 + (mkIf cfg.configureNginx { 171 + assertions = [ 172 + { 173 + assertion = cfg.settings.httpUnsafeOrigin != ""; 174 + message = "services.cryptpad.settings.httpUnsafeOrigin is required"; 175 + } 176 + { 177 + assertion = strings.hasPrefix "https://" cfg.settings.httpUnsafeOrigin; 178 + message = "services.cryptpad.settings.httpUnsafeOrigin must start with https://"; 179 + } 180 + { 181 + assertion = 182 + cfg.settings.httpSafeOrigin == null || strings.hasPrefix "https://" cfg.settings.httpSafeOrigin; 183 + message = "services.cryptpad.settings.httpSafeOrigin must start with https:// (or be unset)"; 184 + } 185 + ]; 186 + services.nginx = { 187 + enable = true; 188 + recommendedTlsSettings = true; 189 + recommendedProxySettings = true; 190 + recommendedOptimisation = true; 191 + recommendedGzipSettings = true; 192 + 193 + virtualHosts = mkMerge [ 194 + { 195 + "${mainDomain}" = { 196 + serverAliases = lib.optionals (cfg.settings.httpSafeOrigin != null) [ sandboxDomain ]; 197 + enableACME = lib.mkDefault true; 198 + forceSSL = true; 199 + locations."/" = { 200 + proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.httpPort}"; 201 + extraConfig = '' 202 + client_max_body_size 150m; 203 + ''; 204 + }; 205 + locations."/cryptpad_websocket" = { 206 + proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.websocketPort}"; 207 + proxyWebsockets = true; 208 + }; 209 + }; 210 + } 211 + ]; 212 + }; 213 + }) 214 + ]); 215 + }
+1
nixos/tests/all-tests.nix
··· 235 235 couchdb = handleTest ./couchdb.nix {}; 236 236 crabfit = handleTest ./crabfit.nix {}; 237 237 cri-o = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cri-o.nix {}; 238 + cryptpad = runTest ./cryptpad.nix; 238 239 cups-pdf = handleTest ./cups-pdf.nix {}; 239 240 curl-impersonate = handleTest ./curl-impersonate.nix {}; 240 241 custom-ca = handleTest ./custom-ca.nix {};
+68
nixos/tests/cryptpad.nix
··· 1 + { pkgs, ... }: 2 + let 3 + certs = pkgs.runCommand "cryptpadSelfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' 4 + mkdir -p $out 5 + cd $out 6 + openssl req -x509 -newkey rsa:4096 \ 7 + -keyout key.pem -out cert.pem -nodes -days 3650 \ 8 + -subj '/CN=cryptpad.localhost' \ 9 + -addext 'subjectAltName = DNS.1:cryptpad.localhost, DNS.2:cryptpad-sandbox.localhost' 10 + ''; 11 + # data sniffed from cryptpad's /checkup network trace, seems to be re-usable 12 + test_write_data = pkgs.writeText "cryptpadTestData" '' 13 + {"command":"WRITE_BLOCK","content":{"publicKey":"O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik=","signature":"aXcM9SMO59lwA7q7HbYB+AnzymmxSyy/KhkG/cXIBVzl8v+kkPWXmFuWhcuKfRF8yt3Zc3ktIsHoFyuyDSAwAA==","ciphertext":"AFwCIfBHKdFzDKjMg4cu66qlJLpP+6Yxogbl3o9neiQou5P8h8yJB8qgnQ=="},"publicKey":"O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik=","nonce":"bitSbJMNSzOsg98nEzN80a231PCkBQeH"} 14 + ''; 15 + in 16 + { 17 + name = "cryptpad"; 18 + meta = with pkgs.lib.maintainers; { 19 + maintainers = [ martinetd ]; 20 + }; 21 + 22 + nodes.machine = { 23 + services.cryptpad = { 24 + enable = true; 25 + configureNginx = true; 26 + settings = { 27 + httpUnsafeOrigin = "https://cryptpad.localhost"; 28 + httpSafeOrigin = "https://cryptpad-sandbox.localhost"; 29 + }; 30 + }; 31 + services.nginx = { 32 + virtualHosts."cryptpad.localhost" = { 33 + enableACME = false; 34 + sslCertificate = "${certs}/cert.pem"; 35 + sslCertificateKey = "${certs}/key.pem"; 36 + }; 37 + }; 38 + security = { 39 + pki.certificateFiles = [ "${certs}/cert.pem" ]; 40 + }; 41 + }; 42 + 43 + testScript = '' 44 + machine.wait_for_unit("cryptpad.service") 45 + machine.wait_for_unit("nginx.service") 46 + machine.wait_for_open_port(3000) 47 + 48 + # test home page 49 + machine.succeed("curl --fail https://cryptpad.localhost -o /tmp/cryptpad_home.html") 50 + machine.succeed("grep -F 'CryptPad: Collaboration suite' /tmp/cryptpad_home.html") 51 + 52 + # test scripts/build.js actually generated customize content from config 53 + machine.succeed("grep -F 'meta property=\"og:url\" content=\"https://cryptpad.localhost/index.html' /tmp/cryptpad_home.html") 54 + 55 + # make sure child pages are accessible (e.g. check nginx try_files paths) 56 + machine.succeed( 57 + "grep -oE '/(customize|components)[^\"]*' /tmp/cryptpad_home.html" 58 + " | while read -r page; do" 59 + " curl -O --fail https://cryptpad.localhost$page || exit;" 60 + " done") 61 + 62 + # test some API (e.g. check cryptpad main process) 63 + machine.succeed("curl --fail -d @${test_write_data} -H 'Content-Type: application/json' https://cryptpad.localhost/api/auth") 64 + 65 + # test telemetry has been disabled 66 + machine.fail("journalctl -u cryptpad | grep TELEMETRY"); 67 + ''; 68 + }