Merge pull request #251687 from martinetd/cryptpad

nixos/cryptpad: init, cryptpad: init at 2024.6.0
This commit is contained in:
Pol Dellaiera 2024-07-26 09:21:51 +02:00 committed by GitHub
commit ceda66b310
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 559 additions and 1 deletions

View File

@ -48,6 +48,8 @@
- [Localsend](https://localsend.org/), an open source cross-platform alternative to AirDrop. Available as [programs.localsend](#opt-programs.localsend.enable).
- [cryptpad](https://cryptpad.org/), a privacy-oriented collaborative platform (docs/drive/etc), has been added back. Available as [services.cryptpad](#opt-services.cryptpad.enable).
- [realm](https://github.com/zhboner/realm), a simple, high performance relay server written in rust. Available as [services.realm.enable](#opt-services.realm.enable).
- [Gotenberg](https://gotenberg.dev), an API server for converting files to PDFs that can be used alongside Paperless-ngx. Available as [services.gotenberg](options.html#opt-services.gotenberg).

View File

@ -1378,6 +1378,7 @@
./services/web-apps/convos.nix
./services/web-apps/crabfit.nix
./services/web-apps/davis.nix
./services/web-apps/cryptpad.nix
./services/web-apps/dex.nix
./services/web-apps/discourse.nix
./services/web-apps/documize.nix

View File

@ -117,7 +117,6 @@ in
(mkRemovedOptionModule [ "services" "virtuoso" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.")
(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.")
(mkRemovedOptionModule [ "services" "prayer" ] "The corresponding package was removed from nixpkgs.")
(mkRemovedOptionModule [ "services" "restya-board" ] "The corresponding package was removed from nixpkgs.")

View File

@ -0,0 +1,293 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.cryptpad;
inherit (lib)
mkIf
mkMerge
mkOption
strings
types
;
# The Cryptpad configuration file isn't JSON, but a JavaScript source file that assigns a JSON value
# to a variable.
cryptpadConfigFile = builtins.toFile "cryptpad_config.js" ''
module.exports = ${builtins.toJSON cfg.settings}
'';
# Derive domain names for Nginx configuration from Cryptpad configuration
mainDomain = strings.removePrefix "https://" cfg.settings.httpUnsafeOrigin;
sandboxDomain =
if cfg.settings.httpSafeOrigin == null then
mainDomain
else
strings.removePrefix "https://" cfg.settings.httpSafeOrigin;
in
{
options.services.cryptpad = {
enable = lib.mkEnableOption "cryptpad";
package = lib.mkPackageOption pkgs "cryptpad" { };
configureNginx = mkOption {
description = ''
Configure Nginx as a reverse proxy for Cryptpad.
Note that this makes some assumptions on your setup, and sets settings that will
affect other virtualHosts running on your Nginx instance, if any.
Alternatively you can configure a reverse-proxy of your choice.
'';
type = types.bool;
default = false;
};
settings = mkOption {
description = ''
Cryptpad configuration settings.
See https://github.com/cryptpad/cryptpad/blob/main/config/config.example.js for a more extensive
reference documentation.
Test your deployed instance through `https://<domain>/checkup/`.
'';
type = types.submodule {
freeformType = (pkgs.formats.json { }).type;
options = {
httpUnsafeOrigin = mkOption {
type = types.str;
example = "https://cryptpad.example.com";
default = "";
description = "This is the URL that users will enter to load your instance";
};
httpSafeOrigin = mkOption {
type = types.nullOr types.str;
example = "https://cryptpad-ui.example.com. Apparently optional but recommended.";
description = "Cryptpad sandbox URL";
};
httpAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address on which the Node.js server should listen";
};
httpPort = mkOption {
type = types.int;
default = 3000;
description = "Port on which the Node.js server should listen";
};
websocketPort = mkOption {
type = types.int;
default = 3003;
description = "Port for the websocket that needs to be separate";
};
maxWorkers = mkOption {
type = types.nullOr types.int;
default = null;
description = "Number of child processes, defaults to number of cores available";
};
adminKeys = mkOption {
type = types.listOf types.str;
default = [ ];
description = "List of public signing keys of users that can access the admin panel";
example = [ "[cryptpad-user1@my.awesome.website/YZgXQxKR0Rcb6r6CmxHPdAGLVludrAF2lEnkbx1vVOo=]" ];
};
logToStdout = mkOption {
type = types.bool;
default = true;
description = "Controls whether log output should go to stdout of the systemd service";
};
logLevel = mkOption {
type = types.str;
default = "info";
description = "Controls log level";
};
blockDailyCheck = mkOption {
type = types.bool;
default = true;
description = ''
Disable telemetry. This setting is only effective if the 'Disable server telemetry'
setting in the admin menu has been untouched, and will be ignored by cryptpad once
that option is set either way.
Note that due to the service confinement, just enabling the option in the admin
menu will not be able to resolve DNS and fail; this setting must be set as well.
'';
};
installMethod = mkOption {
type = types.str;
default = "nixos";
description = ''
Install method is listed in telemetry if you agree to it through the consentToContact
setting in the admin panel.
'';
};
};
};
};
};
config = mkIf cfg.enable (mkMerge [
{
systemd.services.cryptpad = {
description = "Cryptpad service";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
serviceConfig = {
BindReadOnlyPaths = [
cryptpadConfigFile
# apparently needs proc for workers management
"/proc"
"/dev/urandom"
];
DynamicUser = true;
Environment = [
"CRYPTPAD_CONFIG=${cryptpadConfigFile}"
"HOME=%S/cryptpad"
];
ExecStart = lib.getExe cfg.package;
Restart = "always";
StateDirectory = "cryptpad";
WorkingDirectory = "%S/cryptpad";
# security way too many numerous options, from the systemd-analyze security output
# at end of test: block everything except
# - SystemCallFiters=@resources is required for node
# - MemoryDenyWriteExecute for node JIT
# - RestrictAddressFamilies=~AF_(INET|INET6) / PrivateNetwork to bind to sockets
# - IPAddressDeny likewise allow localhost if binding to localhost or any otherwise
# - PrivateUsers somehow service doesn't start with that
# - DeviceAllow (char-rtc r added by ProtectClock)
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = "";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RuntimeDirectoryMode = "700";
SocketBindAllow = [
"tcp:${builtins.toString cfg.settings.httpPort}"
"tcp:${builtins.toString cfg.settings.websocketPort}"
];
SocketBindDeny = [ "any" ];
StateDirectoryMode = "0700";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@pkey"
"@system-service"
"~@chown"
"~@keyring"
"~@memlock"
"~@privileged"
"~@resources"
"~@setuid"
"~@timer"
];
UMask = "0077";
};
confinement = {
enable = true;
binSh = null;
mode = "chroot-only";
};
};
}
# block external network access if not phoning home and
# binding to localhost (default)
(mkIf
(
cfg.settings.blockDailyCheck
&& (builtins.elem cfg.settings.httpAddress [
"127.0.0.1"
"::1"
])
)
{
systemd.services.cryptpad = {
serviceConfig = {
IPAddressAllow = [ "localhost" ];
IPAddressDeny = [ "any" ];
};
};
}
)
# .. conversely allow DNS & TLS if telemetry is explicitly enabled
(mkIf (!cfg.settings.blockDailyCheck) {
systemd.services.cryptpad = {
serviceConfig = {
BindReadOnlyPaths = [
"-/etc/resolv.conf"
"-/run/systemd"
"/etc/hosts"
"/etc/ssl/certs/ca-certificates.crt"
];
};
};
})
(mkIf cfg.configureNginx {
assertions = [
{
assertion = cfg.settings.httpUnsafeOrigin != "";
message = "services.cryptpad.settings.httpUnsafeOrigin is required";
}
{
assertion = strings.hasPrefix "https://" cfg.settings.httpUnsafeOrigin;
message = "services.cryptpad.settings.httpUnsafeOrigin must start with https://";
}
{
assertion =
cfg.settings.httpSafeOrigin == null || strings.hasPrefix "https://" cfg.settings.httpSafeOrigin;
message = "services.cryptpad.settings.httpSafeOrigin must start with https:// (or be unset)";
}
];
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedProxySettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
virtualHosts = mkMerge [
{
"${mainDomain}" = {
serverAliases = lib.optionals (cfg.settings.httpSafeOrigin != null) [ sandboxDomain ];
enableACME = lib.mkDefault true;
forceSSL = true;
locations."/" = {
proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.httpPort}";
extraConfig = ''
client_max_body_size 150m;
'';
};
locations."/cryptpad_websocket" = {
proxyPass = "http://${cfg.settings.httpAddress}:${builtins.toString cfg.settings.websocketPort}";
proxyWebsockets = true;
};
};
}
];
};
})
]);
}

View File

@ -235,6 +235,7 @@ in {
couchdb = handleTest ./couchdb.nix {};
crabfit = handleTest ./crabfit.nix {};
cri-o = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cri-o.nix {};
cryptpad = runTest ./cryptpad.nix;
cups-pdf = handleTest ./cups-pdf.nix {};
curl-impersonate = handleTest ./curl-impersonate.nix {};
custom-ca = handleTest ./custom-ca.nix {};

71
nixos/tests/cryptpad.nix Normal file
View File

@ -0,0 +1,71 @@
{ pkgs, ... }:
let
certs = pkgs.runCommand "cryptpadSelfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
mkdir -p $out
cd $out
openssl req -x509 -newkey rsa:4096 \
-keyout key.pem -out cert.pem -nodes -days 3650 \
-subj '/CN=cryptpad.localhost' \
-addext 'subjectAltName = DNS.1:cryptpad.localhost, DNS.2:cryptpad-sandbox.localhost'
'';
# data sniffed from cryptpad's /checkup network trace, seems to be re-usable
test_write_data = pkgs.writeText "cryptpadTestData" ''
{"command":"WRITE_BLOCK","content":{"publicKey":"O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik=","signature":"aXcM9SMO59lwA7q7HbYB+AnzymmxSyy/KhkG/cXIBVzl8v+kkPWXmFuWhcuKfRF8yt3Zc3ktIsHoFyuyDSAwAA==","ciphertext":"AFwCIfBHKdFzDKjMg4cu66qlJLpP+6Yxogbl3o9neiQou5P8h8yJB8qgnQ=="},"publicKey":"O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik=","nonce":"bitSbJMNSzOsg98nEzN80a231PCkBQeH"}
'';
in
{
name = "cryptpad";
meta = with pkgs.lib.maintainers; {
maintainers = [ martinetd ];
};
nodes.machine = {
services.cryptpad = {
enable = true;
configureNginx = true;
settings = {
httpUnsafeOrigin = "https://cryptpad.localhost";
httpSafeOrigin = "https://cryptpad-sandbox.localhost";
};
};
services.nginx = {
virtualHosts."cryptpad.localhost" = {
enableACME = false;
sslCertificate = "${certs}/cert.pem";
sslCertificateKey = "${certs}/key.pem";
};
};
security = {
pki.certificateFiles = [ "${certs}/cert.pem" ];
};
};
testScript = ''
machine.wait_for_unit("cryptpad.service")
machine.wait_for_unit("nginx.service")
machine.wait_for_open_port(3000)
# test home page
machine.succeed("curl --fail https://cryptpad.localhost -o /tmp/cryptpad_home.html")
machine.succeed("grep -F 'CryptPad: Collaboration suite' /tmp/cryptpad_home.html")
# test scripts/build.js actually generated customize content from config
machine.succeed("grep -F 'meta property=\"og:url\" content=\"https://cryptpad.localhost/index.html' /tmp/cryptpad_home.html")
# make sure child pages are accessible (e.g. check nginx try_files paths)
machine.succeed(
"grep -oE '/(customize|components)[^\"]*' /tmp/cryptpad_home.html"
" | while read -r page; do"
" curl -O --fail https://cryptpad.localhost$page || exit;"
" done")
# test some API (e.g. check cryptpad main process)
machine.succeed("curl --fail -d @${test_write_data} -H 'Content-Type: application/json' https://cryptpad.localhost/api/auth")
# test telemetry has been disabled
machine.fail("journalctl -u cryptpad | grep TELEMETRY");
# for future improvements
machine.log(machine.execute("systemd-analyze security cryptpad.service")[1])
'';
}

View File

@ -0,0 +1,56 @@
From 4bf0be64fe51a9c9fd9e410ada15251378b743bf Mon Sep 17 00:00:00 2001
From: Dominique Martinet <asmadeus@codewreck.org>
Date: Sat, 26 Aug 2023 09:28:59 +0900
Subject: [PATCH] env.js: fix httpSafePort handling
It has been clarified that this is only a dev option that should not be
used in production, but setting the value in config was still ignored,
so fix the init code to consider the config value and make it clear that
this port is not bound if safeOrigin is set.
---
config/config.example.js | 3 ++-
lib/env.js | 5 +++--
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/config/config.example.js b/config/config.example.js
index 7c8184c6c2f6..77263643c354 100644
--- a/config/config.example.js
+++ b/config/config.example.js
@@ -89,8 +89,9 @@ module.exports = {
/* httpSafePort purpose is to emulate another origin for the sandbox when
* you don't have two domains at hand (i.e. when httpSafeOrigin not defined).
- * It is meant to be used only in case where you are working on a local
+ * It is meant to be used only in case where you are working on a local
* development instance. The default value is your httpPort + 1.
+ * Setting this to 0 or setting httpSafeOrigin disables this listener.
*
*/
//httpSafePort: 3001,
diff --git a/lib/env.js b/lib/env.js
index d3748750f21e..f0660cba3e11 100644
--- a/lib/env.js
+++ b/lib/env.js
@@ -74,8 +74,9 @@ module.exports.create = function (config) {
if (typeof(config.httpSafeOrigin) !== 'string') {
NO_SANDBOX = true;
- if (typeof(config.httpSafePort) !== 'number') { httpSafePort = httpPort + 1; }
httpSafeOrigin = deriveSandboxOrigin(httpUnsafeOrigin, httpSafePort);
+ // only set if httpSafeOrigin isn't set.
+ httpSafePort = isValidPort(config.httpSafePort) ? config.httpSafePort : (httpPort + 1);
} else {
httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
}
@@ -115,7 +116,7 @@ module.exports.create = function (config) {
permittedEmbedders: typeof(permittedEmbedders) === 'string' && permittedEmbedders? permittedEmbedders: httpSafeOrigin,
removeDonateButton: config.removeDonateButton,
- httpPort: isValidPort(config.httpPort)? config.httpPort: 3000,
+ httpPort: httpPort,
httpAddress: typeof(config.httpAddress) === 'string'? config.httpAddress: 'localhost',
websocketPath: config.externalWebsocketURL,
logIP: config.logIP,
--
2.45.2

View File

@ -0,0 +1,135 @@
{
buildNpmPackage,
fetchFromGitHub,
lib,
makeBinaryWrapper,
nixosTests,
nodejs,
rdfind,
}:
let
version = "2024.6.0";
# nix version of install-onlyoffice.sh
# a later version could rebuild from sdkjs/web-apps as per
# https://github.com/cryptpad/onlyoffice-builds/blob/main/build.sh
onlyoffice_build =
rev: hash:
fetchFromGitHub {
inherit rev hash;
owner = "cryptpad";
repo = "onlyoffice-builds";
};
onlyoffice_install = oo: ''
oo_dir="$out_cryptpad/www/common/onlyoffice/dist/${oo.subdir}"
cp -a "${onlyoffice_build oo.rev oo.hash}/." "$oo_dir"
chmod -R +w "$oo_dir"
echo "${oo.rev}" > "$oo_dir/.commit"
'';
onlyoffice_versions = [
{
subdir = "v1";
rev = "4f370beb";
hash = "sha256-TE/99qOx4wT2s0op9wi+SHwqTPYq/H+a9Uus9Zj4iSY=";
}
{
subdir = "v2b";
rev = "d9da72fd";
hash = "sha256-SiRDRc2vnLwCVnvtk+C8PKw7IeuSzHBaJmZHogRe3hQ=";
}
{
subdir = "v4";
rev = "6ebc6938";
hash = "sha256-eto1+8Tk/s3kbUCpbUh8qCS8EOq700FYG1/KiHyynaA=";
}
{
subdir = "v5";
rev = "88a356f0";
hash = "sha256-8j1rlAyHlKx6oAs2pIhjPKcGhJFj6ZzahOcgenyeOCc=";
}
{
subdir = "v6";
rev = "abd8a309";
hash = "sha256-BZdExj2q/bqUD3k9uluOot2dlrWKA+vpad49EdgXKww=";
}
{
subdir = "v7";
rev = "9d8b914a";
hash = "sha256-M+rPJ/Xo2olhqB5ViynGRaesMLLfG/1ltUoLnepMPnM=";
}
];
in
buildNpmPackage {
inherit version;
pname = "cryptpad";
src = fetchFromGitHub {
owner = "cryptpad";
repo = "cryptpad";
rev = version;
hash = "sha256-huIhhnjatkaVfm1zDeqi88EX/nAUBQ0onPNOwn7hrX4=";
};
npmDepsHash = "sha256-Oh1fBvP7OXC+VDiH3D+prHmi8pRrxld06n30sqw5apY=";
nativeBuildInputs = [
makeBinaryWrapper
rdfind
];
patches = [
# fix httpSafePort setting
# https://github.com/cryptpad/cryptpad/pull/1571
./0001-env.js-fix-httpSafePort-handling.patch
];
# cryptpad build tries to write in cache dir
makeCacheWritable = true;
# 'npm build run' (scripts/build.js) generates a customize directory, but:
# - that is not installed by npm install
# - it embeds values from config into the directory, so needs to be
# run before starting the server (it's just a few quick replaces)
# Skip it here.
dontNpmBuild = true;
postInstall = ''
out_cryptpad="$out/lib/node_modules/cryptpad"
# 'npm run install:components' (scripts/copy-component.js) copies
# required node modules to www/component in the build tree...
# Move to install directory manually.
npm run install:components
mv www/components "$out_cryptpad/www/"
# install OnlyOffice (install-onlyoffice.sh without network)
mkdir -p "$out_cryptpad/www/common/onlyoffice/dist"
${lib.concatMapStringsSep "\n" onlyoffice_install onlyoffice_versions}
rdfind -makehardlinks true -makeresultsfile false "$out_cryptpad/www/common/onlyoffice/dist"
# cryptpad assumes it runs in the source directory and also outputs
# its state files there, which is not exactly great for us.
# There are relative paths everywhere so just substituing source paths
# is difficult and will likely break on a future update, instead we
# make links to the required source directories before running.
# The build.js step populates 'customize' from customize.dist and config;
# one would normally want to re-run it after modifying config but since it
# would overwrite user modifications only run it if there is no customize
# directory.
makeWrapper "${lib.getExe nodejs}" "$out/bin/cryptpad" \
--add-flags "$out_cryptpad/server.js" \
--run "for d in customize.dist lib www; do ln -sf \"$out_cryptpad/\$d\" .; done" \
--run "if ! [ -d customize ]; then \"${lib.getExe nodejs}\" \"$out_cryptpad/scripts/build.js\"; fi"
'';
passthru.tests.cryptpad = nixosTests.cryptpad;
meta = {
description = "Collaborative office suite, end-to-end encrypted and open-source.";
homepage = "https://cryptpad.org/";
license = lib.licenses.agpl3Plus;
mainProgram = "cryptpad";
maintainers = with lib.maintainers; [ martinetd ];
};
}