grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / cryptpad.nix
blob770eefc00739556cd7d4401fb63f759d3498b441
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
8 let
9   cfg = config.services.cryptpad;
11   inherit (lib)
12     mkIf
13     mkMerge
14     mkOption
15     strings
16     types
17     ;
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   '';
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;
35   options.services.cryptpad = {
36     enable = lib.mkEnableOption "cryptpad";
38     package = lib.mkPackageOption pkgs "cryptpad" { };
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     };
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   };
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             "~@chown"
199             "~@keyring"
200             "~@memlock"
201             "~@privileged"
202             "~@resources"
203             "~@setuid"
204             "~@timer"
205           ];
206           UMask = "0077";
207         };
208         confinement = {
209           enable = true;
210           binSh = null;
211           mode = "chroot-only";
212         };
213       };
214     }
215     # block external network access if not phoning home and
216     # binding to localhost (default)
217     (mkIf
218       (
219         cfg.settings.blockDailyCheck
220         && (builtins.elem cfg.settings.httpAddress [
221           "127.0.0.1"
222           "::1"
223         ])
224       )
225       {
226         systemd.services.cryptpad = {
227           serviceConfig = {
228             IPAddressAllow = [ "localhost" ];
229             IPAddressDeny = [ "any" ];
230           };
231         };
232       }
233     )
234     # .. conversely allow DNS & TLS if telemetry is explicitly enabled
235     (mkIf (!cfg.settings.blockDailyCheck) {
236       systemd.services.cryptpad = {
237         serviceConfig = {
238           BindReadOnlyPaths = [
239             "-/etc/resolv.conf"
240             "-/run/systemd"
241             "/etc/hosts"
242             "/etc/ssl/certs/ca-certificates.crt"
243           ];
244         };
245       };
246     })
248     (mkIf cfg.configureNginx {
249       assertions = [
250         {
251           assertion = cfg.settings.httpUnsafeOrigin != "";
252           message = "services.cryptpad.settings.httpUnsafeOrigin is required";
253         }
254         {
255           assertion = strings.hasPrefix "https://" cfg.settings.httpUnsafeOrigin;
256           message = "services.cryptpad.settings.httpUnsafeOrigin must start with https://";
257         }
258         {
259           assertion =
260             cfg.settings.httpSafeOrigin == null || strings.hasPrefix "https://" cfg.settings.httpSafeOrigin;
261           message = "services.cryptpad.settings.httpSafeOrigin must start with https:// (or be unset)";
262         }
263       ];
264       services.nginx = {
265         enable = true;
266         recommendedTlsSettings = true;
267         recommendedProxySettings = true;
268         recommendedOptimisation = true;
269         recommendedGzipSettings = true;
271         virtualHosts = mkMerge [
272           {
273             "${mainDomain}" = {
274               serverAliases = lib.optionals (cfg.settings.httpSafeOrigin != null) [ sandboxDomain ];
275               enableACME = lib.mkDefault true;
276               forceSSL = true;
277               locations."/" = {
278                 proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.httpPort}";
279                 extraConfig = ''
280                   client_max_body_size 150m;
281                 '';
282               };
283               locations."/cryptpad_websocket" = {
284                 proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.websocketPort}";
285                 proxyWebsockets = true;
286               };
287             };
288           }
289         ];
290       };
291     })
292   ]);