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}