{ domain, ... }: { pkgs, lib, config, machineName, ... }: let inherit (config) networking; inherit (config.services) nginx; srv = "cryptpad"; # CryptPad serves static assets over these two domains. # `main_domain` is what users will enter in their address bar. # Privileged computation such as key management is handled in this scope # UI content is loaded via the `sandbox_domain`. # "Content Security Policy" headers prevent content loaded via the sandbox # from accessing privileged information. # These variables must be different to take advantage of CryptPad's sandboxing techniques. # In the event of an XSS vulnerability in CryptPad's front-end code # this will limit the amount of information accessible to attackers. main_domain = "${srv}.${domain}"; sandbox_domain = "${srv}-sandbox.${domain}"; # CryptPad's dynamic content (websocket traffic and encrypted blobs) # can be served over separate domains. Using dedicated domains (or subdomains) # for these purposes allows you to move them to a separate machine at a later date # if you find that a single machine cannot handle all of your users. # If you don't use dedicated domains, this can be the same as $main_domain # If you do, they'll be added as exceptions to any rules which block connections to remote domains. api_domain = "${srv}-api.${domain}"; files_domain = "${srv}-files.${domain}"; # CSS can be dynamically set inline, loaded from the same domain, or from $main_domain styleSrc = "'unsafe-inline' 'self' ${main_domain}"; # connect-src restricts URLs which can be loaded using script interfaces connectSrc = "'self' https://${main_domain} ${main_domain} https://${api_domain} blob: wss://${api_domain} ${api_domain} ${files_domain}"; # fonts can be loaded from data-URLs or the main domain fontSrc = "'self' data: ${main_domain}"; # images can be loaded from anywhere, though we'd like to deprecate this as it allows the use of images for tracking imgSrc = "'self' data: * blob: ${main_domain}"; # frame-src specifies valid sources for nested browsing contexts. # this prevents loading any iframes from anywhere other than the sandbox domain frameSrc = "'self' ${sandbox_domain} blob:"; # specifies valid sources for loading media using video or audio mediaSrc = "'self' data: * blob: ${main_domain}"; # defines valid sources for webworkers and nested browser contexts # deprecated in favour of worker-src and frame-src childSrc = "https://${main_domain}"; # specifies valid sources for Worker, SharedWorker, or ServiceWorker scripts. # supercedes child-src but is unfortunately not yet universally supported. workerSrc = "https://${main_domain}"; # add_header clear all parent add_header # and thus must be repeated in sub-blocks add_headers = '' ${nginx.configs.https_add_headers} add_header Access-Control-Allow-Origin "*"; add_header Cache-Control $cacheControl; add_header X-Frame-Options ""; add_header Content-Security-Policy "default-src 'none'; child-src ${childSrc}; worker-src ${workerSrc}; media-src ${mediaSrc}; style-src ${styleSrc}; script-src $scriptSrc; connect-src ${connectSrc}; font-src ${fontSrc}; img-src ${imgSrc}; frame-src ${frameSrc};"; ''; cryptpad_root = "/var/lib/nginx/cryptpad"; in { services.cryptpad = { enable = true; # DOC: https://github.com/xwiki-labs/cryptpad/blob/master/config/config.example.js configFile = pkgs.writeText "config.js" '' module.exports = { httpUnsafeOrigin: 'https://${main_domain}/', httpSafeOrigin: "https://${sandbox_domain}/", httpAddress: '::1', httpPort: 3000, httpSafePort: 3001, maxWorkers: 1, /* ===================== * Admin * ===================== */ adminKeys: [ "https://cryptpad.sourcephile.fr/user/#/1/julm/s9y2aE8C5INMZSrR2yQbWBNcGpB8nSLZGFVhGHE8Nn8=", ], // supportMailboxPublicKey: "", removeDonateButton: true, disableAnonymousStore: true, disableCrowdfundingMessages: true, adminEmail: 'root+cryptpad@sourcephile.fr', blockDailyCheck: true, defaultStorageLimit: 1024 * 1024 * 1024, /* ===================== * STORAGE * ===================== */ //inactiveTime: 90, // days //archiveRetentionTime: 15, maxUploadSize: 256 * 1024 * 1024, /* customLimits: { "https://my.awesome.website/user/#/1/cryptpad-user1/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=": { limit: 20 * 1024 * 1024 * 1024, plan: 'insider', note: 'storage space donated by my.awesome.website' }, "https://my.awesome.website/user/#/1/cryptpad-user2/GdflkgdlkjeworijfkldfsdflkjeEAsdlEnkbx1vVOo=": { limit: 10 * 1024 * 1024 * 1024, plan: 'insider', note: 'storage space donated by my.awesome.website' } }, */ //premiumUploadSize: 100 * 1024 * 1024, /* ===================== * DATABASE VOLUMES * ===================== */ filePath: './datastore/', archivePath: './data/archive', pinPath: './data/pins', taskPath: './data/tasks', blockPath: './block', blobPath: './blob', blobStagingPath: './data/blobstage', logPath: './data/logs', /* ===================== * Debugging * ===================== */ logToStdout: false, logLevel: 'info', logFeedback: false, verbose: false, }; ''; }; fileSystems."/var/lib/private/cryptpad" = { device = "${machineName}/var/cryptpad"; fsType = "zfs"; }; services.sanoid.datasets = { "${machineName}/var/cryptpad" = { use_template = [ "local" ]; daily = 31; }; }; services.syncoid.commands = { "${machineName}/var/cryptpad" = { sendOptions = "raw"; target = "backup@mermet.${networking.domain}:rpool/backup/${machineName}/var/cryptpad"; }; }; systemd.services.nginx = { #after = [ "cryptpad.service" ]; #wants = [ "cryptpad.service" ]; serviceConfig = { BindReadOnlyPaths = [ "/var/lib/private/cryptpad:${cryptpad_root}" ]; LogsDirectory = lib.mkForce [ "nginx/${domain}/${srv}" ]; }; }; services.nginx.virtualHosts.${main_domain} = { serverAliases = [ sandbox_domain ]; listen = [ { addr="0.0.0.0"; port = 443; ssl = true; } { addr="[::]"; port = 443; ssl = true; } ]; useACMEHost = domain; forceSSL = true; # DOC: https://github.com/xwiki-labs/cryptpad/blob/master/docs/example.nginx.conf root = "${pkgs.cryptpad}/lib/node_modules/cryptpad"; # The nodejs process can handle all traffic whether accessed over websocket or as static assets # We prefer to serve static content from nginx directly and to leave the API server to handle # the dynamic content that only it can manage. This is primarily an optimization locations."^~ /cryptpad_websocket" = { proxyPass = "http://[::1]:3000"; proxyWebsockets = true; }; locations."^~ /customize.dist/" = { # This is needed in order to prevent infinite recursion between /customize/ and the root }; # try to load customizeable content via /customize/ and fall back to the default content # located at /customize.dist/ # This is what allows you to override behaviour. locations."^~ /customize/".extraConfig = '' rewrite ^/customize/(.*)$ $1 break; try_files /customize/$uri /customize.dist/$uri; ''; # /api/config is loaded once per page load and is used to retrieve # the caching variable which is applied to every other resource # which is loaded during that session. locations."= /api/config" = { proxyPass = "http://[::1]:3000"; }; # encrypted blobs are immutable and are thus cached for a year locations."^~ /blob/" = { root = cryptpad_root; extraConfig = '' ${add_headers} if ($request_method = 'OPTIONS') { ${add_headers} add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'application/octet-stream; charset=utf-8'; add_header 'Content-Length' 0; return 204; } add_header Cache-Control max-age=31536000; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; try_files $uri =404; ''; }; # The "block-store" serves encrypted payloads containing users' drive keys # these payloads are unlocked via login credentials. # They are mutable and are thus never cached. # They're small enough that it doesn't matter, in any case. locations."^~ /block/" = { root = cryptpad_root; extraConfig = '' ${add_headers} add_header Cache-Control max-age=0; try_files $uri =404; ''; }; # ugly hack: disable registration /* locations."/register" = { deny all; } */ # The nodejs server has some built-in forwarding rules to prevent # URLs like /pad from resulting in a 404. This simply adds a trailing slash # to a variety of applications. locations."~ ^/(register|login|settings|user|pad|drive|poll|slide|code|whiteboard|file|media|profile|contacts|todo|filepicker|debug|kanban|sheet|support|admin|notifications|teams)$".extraConfig = '' rewrite ^(.*)$ $1/ redirect; ''; extraConfig = '' access_log /var/log/nginx/${domain}/${srv}/access.log json buffer=32k; error_log /var/log/nginx/${domain}/${srv}/error.log warn; index index.html; error_page 404 /customize.dist/404.html; # add_header will not set any header if it is emptystring set $cacheControl ""; # any static assets loaded with "ver=" in their URL will be cached for a year if ($args ~ ver=) { set $cacheControl max-age=31536000; } set $unsafe 0; # the following assets are loaded via the sandbox domain # they unfortunately still require exceptions to the sandboxing to work correctly. if ($uri = "/pad/inner.html") { set $unsafe 1; } if ($uri = "/sheet/inner.html") { set $unsafe 1; } if ($uri ~ ^\/common\/onlyoffice\/.*\/index\.html.*$) { set $unsafe 1; } # Everything except the sandbox domain is a privileged scope, # as they might be used to handle keys if ($host != "${sandbox_domain}") { set $unsafe 0; } # script-src specifies valid sources for javascript, including inline handlers set $scriptSrc "'self' resource: ${main_domain}"; # privileged contexts allow a few more rights than unprivileged contexts, # though limits are still applied if ($unsafe) { set $scriptSrc "'self' 'unsafe-eval' 'unsafe-inline' resource: ${main_domain}"; } ${add_headers} # Finally, serve anything the above exceptions don't govern. try_files /www/$uri /www/$uri/index.html /customize/$uri; ''; }; }