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 <lassulus@lassul.us>
This commit is contained in:
@@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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" ''
|
||||
<?php
|
||||
${optionalString requiresReadSecretFunction ''
|
||||
function nix_read_secret($file) {
|
||||
if (!file_exists($file)) {
|
||||
throw new \RuntimeException(sprintf(
|
||||
"Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
|
||||
. "exist! Please make sure that the file exists and has appropriate "
|
||||
. "permissions for user & group 'nextcloud'!",
|
||||
$file
|
||||
function nix_read_secret($credential_name) {
|
||||
$credentials_directory = getenv("CREDENTIALS_DIRECTORY");
|
||||
if (!$credentials_directory) {
|
||||
error_log(sprintf(
|
||||
"Cannot read credential '%s' passed by NixOS, \$CREDENTIALS_DIRECTORY is not set!",
|
||||
$credential_name
|
||||
));
|
||||
exit(1);
|
||||
}
|
||||
return trim(file_get_contents($file));
|
||||
|
||||
$credential_path = $credentials_directory . "/" . $credential_name;
|
||||
if (!is_readable($credential_path)) {
|
||||
error_log(sprintf(
|
||||
"Cannot read credential '%s' passed by NixOS, it does not exist or is not readable!",
|
||||
$credential_path,
|
||||
));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return trim(file_get_contents($credential_path));
|
||||
}''}
|
||||
function nix_decode_json_file($file, $error) {
|
||||
if (!file_exists($file)) {
|
||||
@@ -179,12 +228,7 @@ let
|
||||
${optionalString (c.dbhost != null) "'dbhost' => '${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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user