1
0
Fork 0

simplify backup of services by using user

This commit is contained in:
ibizaman 2024-08-23 22:47:50 +02:00
parent 2e8b3fb166
commit dd4a13ad43
13 changed files with 178 additions and 364 deletions

View file

@ -106,6 +106,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "lldap";
sourceDirectories = [ sourceDirectories = [
"/var/lib/lldap" "/var/lib/lldap"
]; ];
@ -139,10 +140,7 @@ in
group = "lldap"; group = "lldap";
isSystemUser = true; isSystemUser = true;
}; };
users.groups.lldap = {};
users.groups.lldap = {
members = [ "backup" ];
};
services.lldap = { services.lldap = {
enable = true; enable = true;

View file

@ -15,12 +15,9 @@ let
user = lib.mkOption { user = lib.mkOption {
description = '' description = ''
Unix user doing the backups. Unix user doing the backups. Must be the user owning the files to be backed up.
For Restic, the same user must be used for all instances.
''; '';
type = lib.types.str; type = lib.types.str;
default = cfg.user;
}; };
sourceDirectories = lib.mkOption { sourceDirectories = lib.mkOption {
@ -115,12 +112,6 @@ let
in in
{ {
options.shb.restic = { options.shb.restic = {
user = lib.mkOption {
description = "Unix user doing the backups.";
type = lib.types.str;
default = "backup";
};
instances = lib.mkOption { instances = lib.mkOption {
description = "Each instance is a backup setting"; description = "Each instance is a backup setting";
default = {}; default = {};
@ -159,34 +150,6 @@ in
let let
enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances; enabledInstances = lib.attrsets.filterAttrs (k: i: i.enable) cfg.instances;
in lib.mkMerge [ in lib.mkMerge [
{
assertions = [
{
assertion = lib.all (x: x.user == cfg.user) (lib.mapAttrsToList (n: v: v)cfg.instances);
message = "All Restic instances must have the same user as 'shb.restic.user'.";
}
{
assertion = lib.all (x: x.group == cfg.group) (lib.mapAttrsToList (n: v: v) cfg.instances);
message = "All Restic instances must have the same group as 'shb.restic.group'.";
}
];
users.users = {
${cfg.user} = {
name = cfg.user;
group = cfg.group;
home = lib.mkForce "/var/lib/${cfg.user}";
createHome = true;
isSystemUser = true;
extraGroups = [ "keys" ];
};
};
users.groups = {
${cfg.group} = {
name = cfg.group;
};
};
}
{ {
environment.systemPackages = lib.optionals (enabledInstances != {}) [ pkgs.restic ]; environment.systemPackages = lib.optionals (enabledInstances != {}) [ pkgs.restic ];
@ -204,7 +167,8 @@ in
let let
mkRepositorySettings = name: instance: repository: { mkRepositorySettings = name: instance: repository: {
"${name}_${repoSlugName repository.path}" = { "${name}_${repoSlugName repository.path}" = {
inherit (cfg) user; inherit (instance) user;
repository = repository.path; repository = repository.path;
paths = instance.sourceDirectories; paths = instance.sourceDirectories;
@ -244,12 +208,13 @@ in
Nice = cfg.performance.niceness; Nice = cfg.performance.niceness;
IOSchedulingClass = cfg.performance.ioSchedulingClass; IOSchedulingClass = cfg.performance.ioSchedulingClass;
IOSchedulingPriority = cfg.performance.ioPriority; IOSchedulingPriority = cfg.performance.ioPriority;
BindReadOnlyPaths = instance.sourceDirectories;
}; };
} }
(lib.attrsets.optionalAttrs (repository.secrets != {}) (lib.attrsets.optionalAttrs (repository.secrets != {})
{ {
serviceConfig.EnvironmentFile = [ serviceConfig.EnvironmentFile = [
"/run/secrets/restic/${serviceName}" "/run/secrets_restic/${serviceName}"
]; ];
after = [ "${serviceName}-pre.service" ]; after = [ "${serviceName}-pre.service" ];
requires = [ "${serviceName}-pre.service" ]; requires = [ "${serviceName}-pre.service" ];
@ -260,8 +225,9 @@ in
(let (let
script = shblib.genConfigOutOfBandSystemd { script = shblib.genConfigOutOfBandSystemd {
config = repository.secrets; config = repository.secrets;
configLocation = "/run/secrets/restic/${serviceName}"; configLocation = "/run/secrets_restic/${serviceName}";
generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; }); generator = name: v: pkgs.writeText "template" (lib.generators.toINIWithGlobalSection {} { globalSection = v; });
user = instance.user;
}; };
in in
{ {

View file

@ -277,20 +277,6 @@ let
]; ];
}; };
backup = name: {
systemd.tmpfiles.rules = [
"d '${config.shb.arr.${name}.dataDir}' 0750 ${config.services.${name}.user} ${config.services.${name}.group} - -"
];
users.groups.${name} = {
members = [ "backup" ];
};
systemd.services.${name}.serviceConfig = {
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
UMask = lib.mkForce "0027";
StateDirectoryMode = lib.mkForce "0750";
};
};
appOption = name: c: lib.nameValuePair name (lib.mkOption { appOption = name: c: lib.nameValuePair name (lib.mkOption {
description = "Configuration for ${name}"; description = "Configuration for ${name}";
default = {}; default = {};
@ -347,6 +333,7 @@ let
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = name;
sourceDirectories = [ sourceDirectories = [
cfg.${name}.dataDir cfg.${name}.dataDir
]; ];
@ -386,7 +373,6 @@ in
shb.nginx.vhosts = [ (vhosts {} cfg') ]; shb.nginx.vhosts = [ (vhosts {} cfg') ];
})) }))
(lib.mkIf cfg.radarr.enable (backup "radarr"))
(lib.mkIf cfg.sonarr.enable ( (lib.mkIf cfg.sonarr.enable (
let let
@ -416,7 +402,6 @@ in
shb.nginx.vhosts = [ (vhosts {} cfg') ]; shb.nginx.vhosts = [ (vhosts {} cfg') ];
})) }))
(lib.mkIf cfg.sonarr.enable (backup "sonarr"))
(lib.mkIf cfg.bazarr.enable ( (lib.mkIf cfg.bazarr.enable (
let let
@ -443,7 +428,6 @@ in
shb.nginx.vhosts = [ (vhosts {} cfg') ]; shb.nginx.vhosts = [ (vhosts {} cfg') ];
})) }))
(lib.mkIf cfg.bazarr.enable (backup "bazarr"))
(lib.mkIf cfg.readarr.enable ( (lib.mkIf cfg.readarr.enable (
let let
@ -465,7 +449,6 @@ in
shb.nginx.vhosts = [ (vhosts {} cfg') ]; shb.nginx.vhosts = [ (vhosts {} cfg') ];
})) }))
(lib.mkIf cfg.readarr.enable (backup "readarr"))
(lib.mkIf cfg.lidarr.enable ( (lib.mkIf cfg.lidarr.enable (
let let
@ -492,7 +475,6 @@ in
shb.nginx.vhosts = [ (vhosts {} cfg') ]; shb.nginx.vhosts = [ (vhosts {} cfg') ];
})) }))
(lib.mkIf cfg.lidarr.enable (backup "lidarr"))
(lib.mkIf cfg.jackett.enable ( (lib.mkIf cfg.jackett.enable (
let let
@ -503,6 +485,7 @@ in
enable = true; enable = true;
dataDir = "/var/lib/jackett"; dataDir = "/var/lib/jackett";
}; };
# TODO: avoid implicitly relying on the media group
users.users.jackett = { users.users.jackett = {
extraGroups = [ "media" ]; extraGroups = [ "media" ];
}; };
@ -516,6 +499,5 @@ in
extraBypassResources = [ "^/dl.*" ]; extraBypassResources = [ "^/dl.*" ];
} cfg') ]; } cfg') ];
})) }))
(lib.mkIf cfg.jackett.enable (backup "jackett"))
]; ];
} }

View file

@ -100,6 +100,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "audiobookshelf";
sourceDirectories = [ sourceDirectories = [
"/var/lib/audiobookshelf" "/var/lib/audiobookshelf"
]; ];
@ -162,17 +163,6 @@ in
]; ];
} }
]; ];
# We want audiobookshelf to create files in the media group and to make those files group readable.
users.users.audiobookshelf = {
extraGroups = [ "media" ];
};
systemd.services.audiobookshelfd.serviceConfig.Group = lib.mkForce "media";
systemd.services.audiobookshelfd.serviceConfig.UMask = lib.mkForce "0027";
# We backup the whole audiobookshelf directory and set permissions for the backup user accordingly.
users.groups.audiobookshelf.members = [ "backup" ];
users.groups.media.members = [ "backup" ];
} { } {
systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig; systemd.services.audiobookshelfd.serviceConfig = cfg.extraServiceConfig;
}]); }]);

View file

@ -251,6 +251,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "deluge";
sourceDirectories = [ sourceDirectories = [
cfg.dataDir cfg.dataDir
]; ];
@ -373,17 +374,6 @@ in
inherit (cfg) authEndpoint; inherit (cfg) authEndpoint;
})) }))
]; ];
# We want deluge to create files in the media group and to make those files group readable.
users.users.deluge = {
extraGroups = [ "media" ];
};
systemd.services.deluged.serviceConfig.Group = lib.mkForce "media";
systemd.services.deluged.serviceConfig.UMask = lib.mkForce "0027";
# We backup the whole deluge directory and set permissions for the backup user accordingly.
users.groups.deluge.members = [ "backup" ];
users.groups.media.members = [ "backup" ];
} { } {
systemd.services.deluged.serviceConfig = cfg.extraServiceConfig; systemd.services.deluged.serviceConfig = cfg.extraServiceConfig;
} (lib.mkIf (config.shb.deluge.prometheusScraperPasswordFile != null) { } (lib.mkIf (config.shb.deluge.prometheusScraperPasswordFile != null) {

View file

@ -80,6 +80,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "grocy";
sourceDirectories = [ sourceDirectories = [
cfg.dataDir cfg.dataDir
]; ];
@ -115,10 +116,6 @@ in
sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert; sslCertificate = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.cert;
sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key; sslCertificateKey = lib.mkIf (!(isNull cfg.ssl)) cfg.ssl.paths.key;
}; };
# We backup the whole grocy directory and set permissions for the backup user accordingly.
users.groups.grocy.members = [ "backup" ];
users.groups.media.members = [ "backup" ];
} { } {
systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig; systemd.services.grocyd.serviceConfig = cfg.extraServiceConfig;
}]); }]);

View file

@ -72,6 +72,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "hledger";
sourceDirectories = [ sourceDirectories = [
cfg.dataDir cfg.dataDir
]; ];

View file

@ -154,6 +154,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "hass";
# No need for backup hooks as we use an hourly automation job in home assistant directly with a cron job. # No need for backup hooks as we use an hourly automation job in home assistant directly with a cron job.
sourceDirectories = [ sourceDirectories = [
"/var/lib/hass/backups" "/var/lib/hass/backups"
@ -322,22 +323,5 @@ in
"f ${config.services.home-assistant.configDir}/scenes.yaml 0755 hass hass" "f ${config.services.home-assistant.configDir}/scenes.yaml 0755 hass hass"
"f ${config.services.home-assistant.configDir}/scripts.yaml 0755 hass hass" "f ${config.services.home-assistant.configDir}/scripts.yaml 0755 hass hass"
]; ];
# Adds the "backup" user to the "hass" group.
users.groups.hass = {
members = [ "backup" ];
};
# This allows the "backup" user, member of the "backup" group, to access what's inside the home
# folder, which is needed for accessing the "backups" folder. It allows to read (r), enter the
# directory (x) but not modify what's inside.
users.users.hass.homeMode = "0750";
systemd.services.home-assistant.serviceConfig = {
# This allows all members of the "hass" group to read files, list directories and enter
# directories created by the home-assistant service. This is needed for the "backup" user,
# member of the "hass" group, to backup what is inside the "backup/" folder.
UMask = lib.mkForce "0027";
};
}; };
} }

View file

@ -138,6 +138,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "jellyfin";
sourceDirectories = [ sourceDirectories = [
"/var/lib/jellyfin" "/var/lib/jellyfin"
]; ];
@ -153,16 +154,6 @@ in
allowedUDPPorts = [ 1900 7359 ]; allowedUDPPorts = [ 1900 7359 ];
}; };
users.groups = {
media = {
name = "media";
members = [ "jellyfin" ];
};
jellyfin = {
members = [ "backup" ];
};
};
services.nginx.enable = true; services.nginx.enable = true;
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex # Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
@ -432,13 +423,5 @@ in
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ]; redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
} }
]; ];
# For backup
systemd.services.jellyfin.serviceConfig = {
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
UMask = lib.mkForce "0027";
StateDirectoryMode = lib.mkForce "0750";
};
}; };
} }

View file

@ -515,6 +515,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "nextcloud";
sourceDirectories = [ sourceDirectories = [
cfg.dataDir cfg.dataDir
]; ];
@ -568,12 +569,6 @@ in
}; };
}; };
# users.groups = {
# nextcloud = {
# members = [ "backup" ];
# };
# };
# LDAP is manually configured through # LDAP is manually configured through
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md, see also # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md, see also
# https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html # https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html
@ -708,10 +703,6 @@ in
services.postgresql.settings = lib.mkIf (! (isNull cfg.postgresSettings)) cfg.postgresSettings; services.postgresql.settings = lib.mkIf (! (isNull cfg.postgresSettings)) cfg.postgresSettings;
systemd.services.phpfpm-nextcloud.serviceConfig = {
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
UMask = lib.mkForce "0027";
};
systemd.services.phpfpm-nextcloud.preStart = '' systemd.services.phpfpm-nextcloud.preStart = ''
mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug
''; '';

View file

@ -132,6 +132,7 @@ in
''; '';
readOnly = true; readOnly = true;
default = { default = {
user = "vaultwarden";
sourceDirectories = [ sourceDirectories = [
dataFolder dataFolder
]; ];
@ -224,17 +225,6 @@ in
passwordFile = builtins.toString cfg.databasePasswordFile; passwordFile = builtins.toString cfg.databasePasswordFile;
} }
]; ];
systemd.services.vaultwarden.serviceConfig.UMask = lib.mkForce "0027";
# systemd.services.vaultwarden.serviceConfig.Group = lib.mkForce "media";
users.users.vaultwarden = {
extraGroups = [ "media" ];
};
users.groups.vaultwarden = {
members = [ "backup" ];
};
# TODO: make this work. # TODO: make this work.
# It does not work because it leads to infinite recursion. # It does not work because it leads to infinite recursion.
# ${cfg.mount}.path = dataFolder; # ${cfg.mount}.path = dataFolder;

View file

@ -9,7 +9,72 @@ let
../../modules/blocks/restic.nix ../../modules/blocks/restic.nix
]; ];
commonTestScript = '' commonTest = user: pkgs.testers.runNixOSTest {
name = "restic_backupAndRestore_${user}";
nodes.machine = {
imports = ( testLib.baseImports pkgs' ) ++ [
../../modules/blocks/restic.nix
];
shb.restic.instances."testinstance" = {
enable = true;
passphraseFile = pkgs.writeText "passphrase" "PassPhrase";
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
user = user;
repositories = [
{
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = "/run/secrets/A";
B.source = "/run/secrets/B";
};
}
{
path = "/opt/repos/B";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
}
];
hooks.before_backup = [''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets_restic/restic-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "B:$B"
exit 12
fi
fi
''];
};
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = ''
from dictdiffer import diff from dictdiffer import diff
def list_files(dir): def list_files(dir):
@ -32,6 +97,19 @@ let
if len(result) > 0: if len(result) > 0:
raise Exception("Unexpected files:", result) raise Exception("Unexpected files:", result)
with subtest("Create secrets"):
print(machine.succeed("""
mkdir -p /run/secrets/
echo secretA > /run/secrets/A
echo secretB > /run/secrets/B
chown root:keys -R /run/secrets
find /run/secrets -type d -exec chmod u=rwx,g=rx,o=x '{}' ';'
find /run/secrets -type f -exec chmod u=r,g=r,o= '{}' ';'
ls -l /run/secrets
"""))
with subtest("Create initial content"): with subtest("Create initial content"):
machine.succeed(""" machine.succeed("""
mkdir -p /opt/files/A mkdir -p /opt/files/A
@ -42,7 +120,8 @@ let
echo repoB_fileA_1 > /opt/files/B/fileA echo repoB_fileA_1 > /opt/files/B/fileA
echo repoB_fileB_1 > /opt/files/B/fileB echo repoB_fileB_1 > /opt/files/B/fileB
# chown :backup -R /opt/files chown ${user}: -R /opt/files
chmod go-rwx -R /opt/files
""") """)
assert_files("/opt/files", { assert_files("/opt/files", {
@ -104,145 +183,10 @@ let
'/opt/files/A/fileB': 'repoA_fileB_2', '/opt/files/A/fileB': 'repoA_fileB_2',
}) })
''; '';
};
in in
{ {
backupAndRestoreRoot = pkgs.testers.runNixOSTest { backupAndRestoreRoot = commonTest "root";
name = "restic_backupAndRestore"; backupAndRestoreUser = commonTest "nobody";
nodes.machine = {
imports = ( testLib.baseImports pkgs' ) ++ [
../../modules/blocks/restic.nix
];
shb.restic = {
user = "root";
group = "root";
};
shb.restic.instances."testinstance" = {
enable = true;
passphraseFile = pkgs.writeText "passphrase" "PassPhrase";
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
repositories = [
{
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = pkgs.writeText "A" "secretA";
B.source = pkgs.writeText "B" "secretB";
};
}
{
path = "/opt/repos/B";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
}
];
hooks.before_backup = [''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets/restic/restic-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "A:$A"
exit 12
fi
fi
''];
};
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = commonTestScript;
};
backupAndRestoreUser = pkgs.testers.runNixOSTest {
name = "restic_backupAndRestore";
nodes.machine = {
imports = ( testLib.baseImports pkgs' ) ++ [
../../modules/blocks/restic.nix
];
shb.restic = {
user = "backup";
group = "backup";
};
shb.restic.instances."testinstance" = {
enable = true;
passphraseFile = pkgs.writeText "passphrase" "PassPhrase";
sourceDirectories = [
"/opt/files/A"
"/opt/files/B"
];
repositories = [
{
path = "/opt/repos/A";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
# Those are not needed by the repository but are still included
# so we can test them in the hooks section.
secrets = {
A.source = pkgs.writeText "A" "secretA";
B.source = pkgs.writeText "B" "secretB";
};
}
{
path = "/opt/repos/B";
timerConfig = {
OnCalendar = "00:00:00";
RandomizedDelaySec = "5h";
};
}
];
hooks.before_backup = [''
echo $RUNTIME_DIRECTORY
if [ "$RUNTIME_DIRECTORY" = /run/restic-backups-testinstance_opt_repos_A ]; then
if ! [ -f /run/secrets/restic/restic-backups-testinstance_opt_repos_A ]; then
exit 10
fi
if [ -z "$A" ] || ! [ "$A" = "secretA" ]; then
echo "A:$A"
exit 11
fi
if [ -z "$B" ] || ! [ "$B" = "secretB" ]; then
echo "A:$A"
exit 12
fi
fi
''];
};
};
extraPythonPackages = p: [ p.dictdiffer ];
skipTypeCheck = true;
testScript = commonTestScript;
};
} }

View file

@ -90,7 +90,6 @@ in
ssl = null; ssl = null;
} }
]; ];
users.groups.radarr.members = [ "backup" ];
services.nginx.enable = true; services.nginx.enable = true;
services.bazarr = {}; services.bazarr = {};
services.jackett = {}; services.jackett = {};
@ -156,7 +155,6 @@ in
ssl = null; ssl = null;
} }
]; ];
users.groups.radarr.members = [ "backup" ];
services.nginx.enable = true; services.nginx.enable = true;
services.bazarr = {}; services.bazarr = {};
services.jackett = {}; services.jackett = {};