From 9ffa0a5945cc7f3fc668ab4d353c3459166bcdef Mon Sep 17 00:00:00 2001 From: networkException Date: Sun, 22 Dec 2024 19:02:55 +0100 Subject: [PATCH 1/8] nixos/tests/nextcloud: test for notify_push in with-declarative-redis-and-secrets This patch adds a subtest and corresponding configuration to with-declarative-redis-and-secrets to test for nextcloud notify_push to be working, just as in with-postgresql-and-redis. As notify_push needs to connect to the database, including it in this test checks that it can read the dbpassFile properly. --- .../nextcloud/with-declarative-redis-and-secrets.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix index 74343a22b7ae..7ab58b037bd3 100644 --- a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix +++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix @@ -34,6 +34,13 @@ runTest ( redis = true; memcached = false; }; + notify_push = { + enable = true; + bendDomainToLocalhost = true; + logLevel = "debug"; + }; + extraAppsEnable = true; + extraApps.notify_push = config.services.nextcloud.package.packages.apps.notify_push; # This test also validates that we can use an "external" database database.createLocally = false; config = { @@ -96,6 +103,10 @@ runTest ( with subtest("non-empty redis cache"): # redis cache should not be empty nextcloud.fail('test 0 -lt "$(redis-cli --pass secret --json KEYS "*" | jq "len")"') + + with subtest("notify-push"): + client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &") + nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${config.adminuser}\"") ''; } ) From 598ba3922cc17aadb35d19534285fbd09ab2fcac Mon Sep 17 00:00:00 2001 From: networkException Date: Sun, 22 Dec 2024 19:12:11 +0100 Subject: [PATCH 2/8] nixos/nextcloud: use writeShellApplication for nextcloud-occ This patch replaces the use of writeScriptBin for the nextcloud-occ script with writeShellApplication, enabling shell checking. This patch also updates various invocations of the script to use lib.getExe. --- nixos/modules/services/web-apps/nextcloud.nix | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index aa9f074a6178..1e628e0ef7cb 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -85,18 +85,21 @@ let "-dmemory_limit=${cfg.cli.memoryLimit}" ]); - occ = pkgs.writeScriptBin "nextcloud-occ" '' - #! ${pkgs.runtimeShell} - cd ${webroot} - sudo=exec - if [[ "$USER" != nextcloud ]]; then - sudo='exec /run/wrappers/bin/sudo -u nextcloud' - fi - $sudo ${pkgs.coreutils}/bin/env \ - NEXTCLOUD_CONFIG_DIR="${datadir}/config" \ - ${phpCli} \ - occ "$@" - ''; + occ = pkgs.writeShellApplication { + name = "nextcloud-occ"; + + text = '' + cd ${webroot} + sudo="exec" + if [[ "$USER" != nextcloud ]]; then + sudo='exec /run/wrappers/bin/sudo -u nextcloud' + fi + $sudo ${pkgs.coreutils}/bin/env \ + NEXTCLOUD_CONFIG_DIR="${datadir}/config" \ + ${phpCli} \ + occ "$@" + ''; + }; inherit (config.system) stateVersion; @@ -965,12 +968,12 @@ in { in '' ${mkExport dbpass} ${mkExport adminpass} - ${occ}/bin/nextcloud-occ maintenance:install \ + ${lib.getExe occ} maintenance:install \ ${installFlags} ''; occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0 (i: v: '' - ${occ}/bin/nextcloud-occ config:system:set trusted_domains \ + ${lib.getExe occ} config:system:set trusted_domains \ ${toString i} --value="${toString v}" '') ([ cfg.hostName ] ++ cfg.settings.trusted_domains)); @@ -1014,13 +1017,13 @@ in { ${occInstallCmd} fi - ${occ}/bin/nextcloud-occ upgrade + ${lib.getExe occ} upgrade - ${occ}/bin/nextcloud-occ config:system:delete trusted_domains + ${lib.getExe occ} config:system:delete trusted_domains ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) '' # Try to enable apps - ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)} + ${lib.getExe occ} app:enable ${concatStringsSep " " (attrNames cfg.extraApps)} ''} ${occSetTrustedDomainsCmd} @@ -1046,7 +1049,7 @@ in { after = [ "nextcloud-setup.service" ]; serviceConfig = { Type = "oneshot"; - ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; + ExecStart = "${lib.getExe occ} app:update --all"; User = "nextcloud"; }; startAt = cfg.autoUpdateApps.startAt; @@ -1055,9 +1058,9 @@ in { after = [ "nextcloud-setup.service" ]; environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; script = '' - ${occ}/bin/nextcloud-occ db:add-missing-columns - ${occ}/bin/nextcloud-occ db:add-missing-indices - ${occ}/bin/nextcloud-occ db:add-missing-primary-keys + ${lib.getExe occ} db:add-missing-columns + ${lib.getExe occ} db:add-missing-indices + ${lib.getExe occ} db:add-missing-primary-keys ''; serviceConfig = { Type = "exec"; From e6b078981b0b6a0a45468d6881a06e957e2e57e6 Mon Sep 17 00:00:00 2001 From: networkException Date: Sun, 22 Dec 2024 19:15:48 +0100 Subject: [PATCH 3/8] nixos/nextcloud: move systemd service overrides for phpfpm-nextcloud closer to phpfpm config --- nixos/modules/services/web-apps/nextcloud.nix | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 1e628e0ef7cb..a0bc8618e6c1 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -929,11 +929,6 @@ in { ]; systemd.services = { - # When upgrading the Nextcloud package, Nextcloud can report errors such as - # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly" - # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround). - phpfpm-nextcloud.restartTriggers = [ webroot overrideConfig ]; - nextcloud-setup = let c = cfg.config; occInstallCmd = let @@ -1068,6 +1063,13 @@ in { ExecCondition = "${phpCli} -f ${webroot}/occ status -e"; }; }; + + phpfpm-nextcloud = { + # When upgrading the Nextcloud package, Nextcloud can report errors such as + # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly" + # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround). + restartTriggers = [ webroot overrideConfig ]; + }; }; services.phpfpm = { From 2ce1e841032eac4913f2cd3dce416da3d5c799ef Mon Sep 17 00:00:00 2001 From: networkException Date: Sun, 22 Dec 2024 19:18:15 +0100 Subject: [PATCH 4/8] nixos/nextcloud: use LoadCredential to read secrets This patch adds support for using systemd's LoadCredential feature to read various secret files used by nextcloud service units. Previously credentials had to be readable by the nextcloud user, this is now no longer required. The nextcloud-occ wrapper script has been adjusted to use systemd-run for loading credentials when being called from outside a service. In detail this change touches various details of the module: - The nix_read_secret() php function now takes the name of a file relative to the path specified in the CREDENTIALS_DIRECTORY environment variable. - The nix_read_secret() now exits with error code 1 instead of throwing a RuntimeException as this will properly error out the nextcloud-occ script - Only the nextcloud-setup service unit has the adminpass credential added in addition to the other credentials - Uses of ExecCondition= in nextcloud-cron and nextcloud-update-db have been replaced by a shell conditional as ExecCondition currently doesn't support credentials - The phpfpm-nextcloud service now runs a preStart script to make the credentials it gets readable by the nextcloud user as the unit runs as root but the php process itself as nextcloud. - To invoke occ notify_push:setup when using nextcloud notify_push a new service has been added that replaces the preStart script in nextcloud-notify_push.service. This has been done as the main executable only needs the database password credential. Co-authored-by: lassulus --- .../web-apps/nextcloud-notify_push.nix | 119 +++++++------ nixos/modules/services/web-apps/nextcloud.nix | 165 +++++++++++++----- 2 files changed, 184 insertions(+), 100 deletions(-) diff --git a/nixos/modules/services/web-apps/nextcloud-notify_push.nix b/nixos/modules/services/web-apps/nextcloud-notify_push.nix index 8d539a3fb0f7..3243e5125f19 100644 --- a/nixos/modules/services/web-apps/nextcloud-notify_push.nix +++ b/nixos/modules/services/web-apps/nextcloud-notify_push.nix @@ -78,60 +78,73 @@ in ); config = lib.mkIf cfg.enable { - systemd.services.nextcloud-notify_push = { - description = "Push daemon for Nextcloud clients"; - documentation = [ "https://github.com/nextcloud/notify_push" ]; - after = [ - "phpfpm-nextcloud.service" - "redis-nextcloud.service" - ]; - wantedBy = [ "multi-user.target" ]; - environment = { - NEXTCLOUD_URL = cfg.nextcloudUrl; - SOCKET_PATH = cfg.socketPath; - DATABASE_PREFIX = cfg.dbtableprefix; - LOG = cfg.logLevel; + systemd.services = { + nextcloud-notify_push = { + description = "Push daemon for Nextcloud clients"; + documentation = [ "https://github.com/nextcloud/notify_push" ]; + after = [ + "phpfpm-nextcloud.service" + "redis-nextcloud.service" + ]; + wantedBy = [ "multi-user.target" ]; + environment = { + NEXTCLOUD_URL = cfg.nextcloudUrl; + SOCKET_PATH = cfg.socketPath; + DATABASE_PREFIX = cfg.dbtableprefix; + LOG = cfg.logLevel; + }; + script = + let + dbType = if cfg.dbtype == "pgsql" then "postgresql" else cfg.dbtype; + dbUser = lib.optionalString (cfg.dbuser != null) cfg.dbuser; + dbPass = lib.optionalString (cfg.dbpassFile != null) ":$DATABASE_PASSWORD"; + dbHostHasPrefix = prefix: lib.hasPrefix prefix (toString cfg.dbhost); + isPostgresql = dbType == "postgresql"; + isMysql = dbType == "mysql"; + isSocket = (isPostgresql && dbHostHasPrefix "/") || (isMysql && dbHostHasPrefix "localhost:/"); + dbHost = lib.optionalString (cfg.dbhost != null) ( + if isSocket then lib.optionalString isMysql "@localhost" else "@${cfg.dbhost}" + ); + dbOpts = lib.optionalString (cfg.dbhost != null && isSocket) ( + if isPostgresql then + "?host=${cfg.dbhost}" + else if isMysql then + "?socket=${lib.removePrefix "localhost:" cfg.dbhost}" + else + throw "unsupported dbtype" + ); + dbName = lib.optionalString (cfg.dbname != null) "/${cfg.dbname}"; + dbUrl = "${dbType}://${dbUser}${dbPass}${dbHost}${dbName}${dbOpts}"; + in + lib.optionalString (cfg.dbpassFile != null) '' + export DATABASE_PASSWORD="$(<"$CREDENTIALS_DIRECTORY/dbpass")" + '' + + '' + export DATABASE_URL="${dbUrl}" + exec ${cfg.package}/bin/notify_push '${cfgN.datadir}/config/config.php' + ''; + serviceConfig = { + User = "nextcloud"; + Group = "nextcloud"; + RuntimeDirectory = [ "nextcloud-notify_push" ]; + Restart = "on-failure"; + RestartSec = "5s"; + Type = "notify"; + LoadCredential = lib.optional (cfg.dbpassFile != null) "dbpass:${cfg.dbpassFile}"; + }; }; - postStart = '' - ${cfgN.occ}/bin/nextcloud-occ notify_push:setup ${cfg.nextcloudUrl}/push - ''; - script = - let - dbType = if cfg.dbtype == "pgsql" then "postgresql" else cfg.dbtype; - dbUser = lib.optionalString (cfg.dbuser != null) cfg.dbuser; - dbPass = lib.optionalString (cfg.dbpassFile != null) ":$DATABASE_PASSWORD"; - dbHostHasPrefix = prefix: lib.hasPrefix prefix (toString cfg.dbhost); - isPostgresql = dbType == "postgresql"; - isMysql = dbType == "mysql"; - isSocket = (isPostgresql && dbHostHasPrefix "/") || (isMysql && dbHostHasPrefix "localhost:/"); - dbHost = lib.optionalString (cfg.dbhost != null) ( - if isSocket then lib.optionalString isMysql "@localhost" else "@${cfg.dbhost}" - ); - dbOpts = lib.optionalString (cfg.dbhost != null && isSocket) ( - if isPostgresql then - "?host=${cfg.dbhost}" - else if isMysql then - "?socket=${lib.removePrefix "localhost:" cfg.dbhost}" - else - throw "unsupported dbtype" - ); - dbName = lib.optionalString (cfg.dbname != null) "/${cfg.dbname}"; - dbUrl = "${dbType}://${dbUser}${dbPass}${dbHost}${dbName}${dbOpts}"; - in - lib.optionalString (dbPass != "") '' - export DATABASE_PASSWORD="$(<"${cfg.dbpassFile}")" - '' - + '' - export DATABASE_URL="${dbUrl}" - exec ${cfg.package}/bin/notify_push '${cfgN.datadir}/config/config.php' - ''; - serviceConfig = { - User = "nextcloud"; - Group = "nextcloud"; - RuntimeDirectory = [ "nextcloud-notify_push" ]; - Restart = "on-failure"; - RestartSec = "5s"; - Type = "notify"; + + nextcloud-notify_push_setup = { + wantedBy = [ "multi-user.target" ]; + requiredBy = [ "nextcloud-notify_push.service" ]; + after = [ "nextcloud-notify_push.service" ]; + serviceConfig = { + Type = "oneshot"; + User = "nextcloud"; + Group = "nextcloud"; + ExecStart = "${lib.getExe cfgN.occ} notify_push:setup ${cfg.nextcloudUrl}/push"; + LoadCredential = config.systemd.services.nextcloud-cron.serviceConfig.LoadCredential; + }; }; }; diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index a0bc8618e6c1..e4dadb1a3178 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -85,19 +85,58 @@ let "-dmemory_limit=${cfg.cli.memoryLimit}" ]); + # NOTE: The credentials required by all services at runtime, not including things like the + # admin password which is only needed by the setup service. + runtimeSystemdCredentials = [] + ++ (lib.optional (cfg.config.dbpassFile != null) "dbpass:${cfg.config.dbpassFile}") + ++ (lib.optional (cfg.config.objectstore.s3.enable) "s3_secret:${cfg.config.objectstore.s3.secretFile}") + ++ (lib.optional (cfg.config.objectstore.s3.sseCKeyFile != null) "s3_sse_c_key:${cfg.config.objectstore.s3.sseCKeyFile}"); + + requiresRuntimeSystemdCredentials = (lib.length runtimeSystemdCredentials) != 0; + occ = pkgs.writeShellApplication { name = "nextcloud-occ"; - text = '' + text = let + command = '' + ${lib.getExe' pkgs.coreutils "env"} \ + NEXTCLOUD_CONFIG_DIR="${datadir}/config" \ + ${phpCli} \ + occ "$@" + ''; + in '' cd ${webroot} - sudo="exec" - if [[ "$USER" != nextcloud ]]; then - sudo='exec /run/wrappers/bin/sudo -u nextcloud' + + # NOTE: This is templated at eval time + requiresRuntimeSystemdCredentials=${lib.boolToString requiresRuntimeSystemdCredentials} + + # NOTE: This wrapper is both used in the internal nextcloud service units + # and by users outside a service context for administration. As such, + # when there's an existing CREDENTIALS_DIRECTORY, we inherit it for use + # in the nix_read_secret() php function. + # When there's no CREDENTIALS_DIRECTORY we try to use systemd-run to + # load the credentials just as in a service unit. + # NOTE: If there are no credentials that are required at runtime then there's no need + # to load any credentials. + if [[ $requiresRuntimeSystemdCredentials == true && -z "''${CREDENTIALS_DIRECTORY:-}" ]]; then + exec ${lib.getExe' config.systemd.package "systemd-run"} \ + ${lib.escapeShellArgs (map (credential: "--property=LoadCredential=${credential}") runtimeSystemdCredentials)} \ + --uid=nextcloud \ + --same-dir \ + --pty \ + --wait \ + --collect \ + --service-type=exec \ + --quiet \ + ${command} + elif [[ "$USER" != nextcloud ]]; then + exec /run/wrappers/bin/sudo \ + --preserve-env=CREDENTIALS_DIRECTORY \ + --user=nextcloud \ + ${command} + else + exec ${command} fi - $sudo ${pkgs.coreutils}/bin/env \ - NEXTCLOUD_CONFIG_DIR="${datadir}/config" \ - ${phpCli} \ - occ "$@" ''; }; @@ -123,13 +162,13 @@ let 'bucket' => '${s3.bucket}', 'autocreate' => ${boolToString s3.autocreate}, 'key' => '${s3.key}', - 'secret' => nix_read_secret('${s3.secretFile}'), + 'secret' => nix_read_secret('s3_secret'), ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"} ${optionalString (s3.port != null) "'port' => ${toString s3.port},"} 'use_ssl' => ${boolToString s3.useSsl}, ${optionalString (s3.region != null) "'region' => '${s3.region}',"} 'use_path_style' => ${boolToString s3.usePathStyle}, - ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"} + ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('s3_sse_c_key'),"} ], ] ''; @@ -146,16 +185,26 @@ let in pkgs.writeText "nextcloud-config.php" '' '${c.dbhost}',"} ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"} ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"} - ${optionalString (c.dbpassFile != null) '' - 'dbpassword' => nix_read_secret( - "${c.dbpassFile}" - ), - '' - } + ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('dbpass'),"} 'dbtype' => '${c.dbtype}', ${objectstoreConfig} ]; @@ -480,9 +524,8 @@ in { adminpassFile = mkOption { type = types.str; description = '' - The full path to a file that contains the admin's password. Must be - readable by user `nextcloud`. The password is set only in the initial - setup of Nextcloud by the systemd service `nextcloud-setup.service`. + The full path to a file that contains the admin's password. The password is + set only in the initial setup of Nextcloud by the systemd service `nextcloud-setup.service`. ''; }; objectstore = { @@ -520,8 +563,7 @@ in { type = types.str; example = "/var/nextcloud-objectstore-s3-secret"; description = '' - The full path to a file that contains the access secret. Must be - readable by user `nextcloud`. + The full path to a file that contains the access secret. ''; }; hostname = mkOption { @@ -582,8 +624,6 @@ in { openssl rand 32 | base64 ``` - Must be readable by user `nextcloud`. - [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html ''; }; @@ -939,12 +979,12 @@ in { dbpass = { arg = "DBPASS"; value = if c.dbpassFile != null - then ''"$(<"${toString c.dbpassFile}")"'' + then ''"$(<"$CREDENTIALS_DIRECTORY/dbpass")"'' else ''""''; }; adminpass = { arg = "ADMINPASS"; - value = ''"$(<"${toString c.adminpassFile}")"''; + value = ''"$(<"$CREDENTIALS_DIRECTORY/adminpass")"''; }; installFlags = concatStringsSep " \\\n " (mapAttrsToList (k: v: "${k} ${toString v}") { @@ -982,20 +1022,12 @@ in { restartTriggers = [ overrideConfig ]; script = '' ${optionalString (c.dbpassFile != null) '' - if [ ! -r "${c.dbpassFile}" ]; then - echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..." - exit 1 - fi - if [ -z "$(<${c.dbpassFile})" ]; then + if [ -z "$(<$CREDENTIALS_DIRECTORY/dbpass)" ]; then echo "dbpassFile ${c.dbpassFile} is empty!" exit 1 fi ''} - if [ ! -r "${c.adminpassFile}" ]; then - echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..." - exit 1 - fi - if [ -z "$(<${c.adminpassFile})" ]; then + if [ -z "$(<$CREDENTIALS_DIRECTORY/adminpass)" ]; then echo "adminpassFile ${c.adminpassFile} is empty!" exit 1 fi @@ -1025,19 +1057,32 @@ in { ''; serviceConfig.Type = "oneshot"; serviceConfig.User = "nextcloud"; + serviceConfig.LoadCredential = [ "adminpass:${cfg.config.adminpassFile}" ] ++ runtimeSystemdCredentials; # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent # an automatic creation of the database user. environment.NC_setup_create_db_user = lib.mkIf (nextcloudGreaterOrEqualThan "26") "false"; }; nextcloud-cron = { after = [ "nextcloud-setup.service" ]; + # NOTE: In contrast to the occ wrapper script running phpCli directly will not + # set NEXTCLOUD_CONFIG_DIR by itself currently. environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; + script = '' + # NOTE: This early returns the script when nextcloud is in maintenance mode + # or needs `occ upgrade`. Using ExecCondition= is not possible here + # because it doesn't work with systemd credentials. + if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then + echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." + exit 0 + fi + + ${phpCli} -f ${webroot}/cron.php + ''; serviceConfig = { Type = "exec"; User = "nextcloud"; - ExecCondition = "${phpCli} -f ${webroot}/occ status -e"; - ExecStart = "${phpCli} -f ${webroot}/cron.php"; KillMode = "process"; + LoadCredential = runtimeSystemdCredentials; }; }; nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable { @@ -1046,13 +1091,21 @@ in { Type = "oneshot"; ExecStart = "${lib.getExe occ} app:update --all"; User = "nextcloud"; + LoadCredential = runtimeSystemdCredentials; }; startAt = cfg.autoUpdateApps.startAt; }; nextcloud-update-db = { after = [ "nextcloud-setup.service" ]; - environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; script = '' + # NOTE: This early returns the script when nextcloud is in maintenance mode + # or needs `occ upgrade`. Using ExecCondition= is not possible here + # because it doesn't work with systemd credentials. + if [[ $(${lib.getExe occ} status --output=json | ${lib.getExe pkgs.jq} '. | if .maintenance or .needsDbUpgrade then "skip" else "" end' --raw-output) == "skip" ]]; then + echo "Nextcloud is in maintenance mode or needs DB upgrade, exiting." + exit 0 + fi + ${lib.getExe occ} db:add-missing-columns ${lib.getExe occ} db:add-missing-indices ${lib.getExe occ} db:add-missing-primary-keys @@ -1060,7 +1113,7 @@ in { serviceConfig = { Type = "exec"; User = "nextcloud"; - ExecCondition = "${phpCli} -f ${webroot}/occ status -e"; + LoadCredential = runtimeSystemdCredentials; }; }; @@ -1069,6 +1122,23 @@ in { # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly" # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround). restartTriggers = [ webroot overrideConfig ]; + } // lib.optionalAttrs requiresRuntimeSystemdCredentials { + serviceConfig.LoadCredential = runtimeSystemdCredentials; + + # FIXME: We use a hack to make the credential files readable by the nextcloud + # user by copying them somewhere else and overriding CREDENTIALS_DIRECTORY + # for php. This is currently necessary as the unit runs as root. + serviceConfig.RuntimeDirectory = lib.mkForce "phpfpm phpfpm-nextcloud"; + preStart = '' + umask 0077 + + # NOTE: Runtime directories for this service are currently preserved + # between restarts. + rm -rf /run/phpfpm-nextcloud/credentials/ + mkdir -p /run/phpfpm-nextcloud/credentials/ + cp "$CREDENTIALS_DIRECTORY"/* /run/phpfpm-nextcloud/credentials/ + chown -R nextcloud:nextcloud /run/phpfpm-nextcloud/credentials/ + ''; }; }; @@ -1078,6 +1148,7 @@ in { group = "nextcloud"; phpPackage = phpPackage; phpEnv = { + CREDENTIALS_DIRECTORY = "/run/phpfpm-nextcloud/credentials/"; NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; }; From 18de1c264ec53c07da9241b2a07775ff2cc8434a Mon Sep 17 00:00:00 2001 From: networkException Date: Sun, 22 Dec 2024 21:36:32 +0100 Subject: [PATCH 5/8] nixos/tests/nextcloud: use lib instead of pkgs.lib wherever trivial --- nixos/tests/nextcloud/basic.nix | 4 ++-- .../nextcloud/with-declarative-redis-and-secrets.nix | 8 ++++---- nixos/tests/nextcloud/with-mysql-and-memcached.nix | 4 ++-- nixos/tests/nextcloud/with-objectstore.nix | 2 +- nixos/tests/nextcloud/with-postgresql-and-redis.nix | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nixos/tests/nextcloud/basic.nix b/nixos/tests/nextcloud/basic.nix index 217f22714c1c..9faf20a2f5ad 100644 --- a/nixos/tests/nextcloud/basic.nix +++ b/nixos/tests/nextcloud/basic.nix @@ -8,10 +8,10 @@ with import ../../lib/testing-python.nix { inherit system pkgs; }; runTest ( - { config, ... }: + { config, lib, ... }: { inherit name; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ globin eqyiel diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix index 7ab58b037bd3..0ca4eab2c6e9 100644 --- a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix +++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix @@ -8,13 +8,13 @@ with import ../../lib/testing-python.nix { inherit system pkgs; }; runTest ( - { config, ... }: + { config, lib, ... }: let inherit (config) adminuser; in { inherit name; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ eqyiel ma27 @@ -77,7 +77,7 @@ runTest ( services.postgresql = { enable = true; }; - systemd.services.postgresql.postStart = pkgs.lib.mkAfter '' + systemd.services.postgresql.postStart = lib.mkAfter '' password=$(cat ${config.services.nextcloud.config.dbpassFile}) ${config.services.postgresql.package}/bin/psql <&2 &") + client.execute("${lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &") nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${config.adminuser}\"") ''; } diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix index 110047fe6610..55b1e5e5695f 100644 --- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix +++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix @@ -8,10 +8,10 @@ with import ../../lib/testing-python.nix { inherit system pkgs; }; runTest ( - { config, ... }: + { config, lib, ... }: { inherit name; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ eqyiel ]; }; diff --git a/nixos/tests/nextcloud/with-objectstore.nix b/nixos/tests/nextcloud/with-objectstore.nix index 5f24fc48851c..5a2a4a6fd0f1 100644 --- a/nixos/tests/nextcloud/with-objectstore.nix +++ b/nixos/tests/nextcloud/with-objectstore.nix @@ -20,7 +20,7 @@ runTest ( in { inherit name; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ onny ma27 diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix index 17cdd38330a1..99708f9a8db4 100644 --- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix +++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix @@ -8,10 +8,10 @@ with import ../../lib/testing-python.nix { inherit system pkgs; }; runTest ( - { config, ... }: + { config, lib, ... }: { inherit name; - meta = with pkgs.lib.maintainers; { + meta = with lib.maintainers; { maintainers = [ eqyiel ma27 @@ -68,7 +68,7 @@ runTest ( test-helpers.extraTests = '' with subtest("notify-push"): - client.execute("${pkgs.lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &") + client.execute("${lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &") nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${config.adminuser}\"") with subtest("Redis is used for caching"): From 549d8a6d44b9a4462428c932286a9c16b7975f54 Mon Sep 17 00:00:00 2001 From: networkException Date: Mon, 23 Dec 2024 00:46:32 +0100 Subject: [PATCH 6/8] nixos/tests/nextcloud: fix redis cache non empty tests This patch changes the implementation of the subtests to check for redis' cache being non empty to only run redis-cli and jq in a shell and assert the returned length in python. This fixes jq "len" simply not compiling and makes sure regressions get noticed. --- nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix | 4 +++- nixos/tests/nextcloud/with-postgresql-and-redis.nix | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix index 0ca4eab2c6e9..40d231c21cac 100644 --- a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix +++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix @@ -102,7 +102,9 @@ runTest ( test-helpers.extraTests = '' with subtest("non-empty redis cache"): # redis cache should not be empty - nextcloud.fail('test 0 -lt "$(redis-cli --pass secret --json KEYS "*" | jq "len")"') + assert nextcloud.succeed('redis-cli --pass secret --json KEYS "*" | jq length').strip() != "0", """ + redis-cli for keys * returned 0 entries + """ with subtest("notify-push"): client.execute("${lib.getExe pkgs.nextcloud-notify_push.passthru.test_client} http://nextcloud ${config.adminuser} ${config.adminpass} >&2 &") diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix index 99708f9a8db4..520aedf5688f 100644 --- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix +++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix @@ -29,6 +29,7 @@ runTest ( ... }: { + environment.systemPackages = [ pkgs.jq ]; services.nextcloud = { caching = { apcu = false; @@ -73,7 +74,9 @@ runTest ( with subtest("Redis is used for caching"): # redis cache should not be empty - nextcloud.fail('test "[]" = "$(redis-cli --json KEYS "*")"') + assert nextcloud.succeed('redis-cli --json KEYS "*" | jq length').strip() != "0", """ + redis-cli for keys * returned 0 entries + """ with subtest("No code is returned when requesting PHP files (regression test)"): nextcloud.fail("curl -f http://nextcloud/nix-apps/notes/lib/AppInfo/Application.php") From 8583a0de6f3d57e4fc2d64b59de1ccdb7ee3301e Mon Sep 17 00:00:00 2001 From: networkException Date: Wed, 22 Jan 2025 17:24:29 +0100 Subject: [PATCH 7/8] nixos/nextcloud: document systemd credentials as a backwards incompatible change in the 25.05 release notes This patch adds a release note entry to the 25.05 release about the use of systemd credentials to read in secrets. It's part of the backward incompatibilities section as changes to the behavior of `nextcloud-occ` might break existing scripts. --- nixos/doc/manual/release-notes/rl-2505.section.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index cb005a1e9b96..01ebcaa15533 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -216,6 +216,8 @@ - `hardware.pulseaudio` has been renamed to `services.pulseaudio`. The deprecated option names will continue to work, but causes a warning. +- `services.nextcloud` now uses systemd's credential mechanism to read in secret files. The `nextcloud-occ` wrapper script implements this using `systemd-run`, as such it now also requires root privileges or `$CREDENTIALS_DIRECTORY` set where running it as user `nextcloud` was enough previously. + - `minetest` has been renamed to `luanti` to match the upstream name change but aliases have been added. The new name hasn't resulted in many changes as of yet but older references to minetest should be sunset. See the [new name announcement](https://blog.minetest.net/2024/10/13/Introducing-Our-New-Name/) for more details. - `racket_7_9` has been removed, as it is insecure. It is recommended to use Racket 8 instead. From 432d274c810c706f5c54ebd43f619d52c9b187a1 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Wed, 5 Mar 2025 17:08:52 +0100 Subject: [PATCH 8/8] nixos/nextcloud-notify_push: use RestartMode=direct `nextcloud-notify_push.service` requires `nextcloud-notify_push-setup.service`. If the latter fails (e.g. because of Nextcloud not being there yet), the push service would also fail with result 'dependency'. RestartMode=direct doesn't put a unit into failed state IF it's about to be restarted again. That way, `nextcloud-notify_push` will await several restart attempts. Only if the unit fails due to a rate-limit (i.e. too many restarts), the push service will also fail. If the startup is still too slow, it may make sense for administrators to configure higher intervals between the start attempts with RestartSec. --- nixos/modules/services/web-apps/nextcloud-notify_push.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/modules/services/web-apps/nextcloud-notify_push.nix b/nixos/modules/services/web-apps/nextcloud-notify_push.nix index 3243e5125f19..52accfe65ac5 100644 --- a/nixos/modules/services/web-apps/nextcloud-notify_push.nix +++ b/nixos/modules/services/web-apps/nextcloud-notify_push.nix @@ -144,6 +144,8 @@ in Group = "nextcloud"; ExecStart = "${lib.getExe cfgN.occ} notify_push:setup ${cfg.nextcloudUrl}/push"; LoadCredential = config.systemd.services.nextcloud-cron.serviceConfig.LoadCredential; + RestartMode = "direct"; + Restart = "on-failure"; }; }; };