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"; };