WIP: Add atomic backups with restic
This commit is contained in:
parent
ab5e088016
commit
5ddb9b4707
|
@ -14,6 +14,7 @@
|
||||||
"${modulesPath}/profiles/minimal.nix"
|
"${modulesPath}/profiles/minimal.nix"
|
||||||
(import ../modules)
|
(import ../modules)
|
||||||
|
|
||||||
|
./services/backups.nix
|
||||||
./services/conduit.nix
|
./services/conduit.nix
|
||||||
./services/foundryvtt.nix
|
./services/foundryvtt.nix
|
||||||
./services/gitea.nix
|
./services/gitea.nix
|
||||||
|
|
228
configuration/services/backups.nix
Normal file
228
configuration/services/backups.nix
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
inherit (lib) types optional singleton;
|
||||||
|
mkShutdownScript = service:
|
||||||
|
pkgs.writeShellScript "backup-${service}-shutdown" ''
|
||||||
|
if systemctl is-active --quiet '${service}'; then
|
||||||
|
touch '/tmp/${service}-was-active'
|
||||||
|
systemctl stop '${service}'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
mkRestartScript = service:
|
||||||
|
pkgs.writeShellScript "backup-${service}-restart" ''
|
||||||
|
if [ -f '/tmp/${service}-was-active' ]; then
|
||||||
|
rm '/tmp/${service}-was-active'
|
||||||
|
systemctl start '${service}'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
writeScript = name: packages: text:
|
||||||
|
lib.getExe (pkgs.writeShellApplication {
|
||||||
|
inherit name text;
|
||||||
|
runtimeInputs = packages;
|
||||||
|
});
|
||||||
|
in {
|
||||||
|
options = {
|
||||||
|
services.backups = lib.mkOption {
|
||||||
|
description = lib.mdDoc ''
|
||||||
|
Configure restic backups with a specific tag.
|
||||||
|
'';
|
||||||
|
type = types.attrsOf (types.submodule ({
|
||||||
|
config,
|
||||||
|
name,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
options = {
|
||||||
|
user = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
The user as which to run the backup.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
paths = lib.mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
description = ''
|
||||||
|
The paths to back up.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
tag = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = ''
|
||||||
|
The restic tag to mark the backup with.
|
||||||
|
'';
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
preparation = {
|
||||||
|
packages = lib.mkOption {
|
||||||
|
type = types.listOf types.package;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
The list of packages to make available in the
|
||||||
|
preparation script.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
text = lib.mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The preparation script to run before the backup.
|
||||||
|
|
||||||
|
This should include things like database dumps and
|
||||||
|
enabling maintenance modes. If a service needs to be
|
||||||
|
shut down for backups, use `pauseServices` instead.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
cleanup = {
|
||||||
|
packages = lib.mkOption {
|
||||||
|
type = types.listOf types.package;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
The list of packages to make available in the
|
||||||
|
cleanup script.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
text = lib.mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The cleanup script to run after the backup.
|
||||||
|
|
||||||
|
This should do things like cleaning up database dumps
|
||||||
|
and disabling maintenance modes.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
pauseServices = lib.mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
The systemd services that need to be shut down before
|
||||||
|
the backup can run. Services will be restarted after the
|
||||||
|
backup is complete.
|
||||||
|
|
||||||
|
This is intended to be used for services that do not
|
||||||
|
support hot backups.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (config.services.backups != {}) {
|
||||||
|
systemd.services =
|
||||||
|
{
|
||||||
|
restic-prune = {
|
||||||
|
# Doesn't hurt to finish the ongoing prune
|
||||||
|
restartIfChanged = false;
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RESTIC_PASSWORD_FILE = config.sops.secrets."restic/local-backups".path;
|
||||||
|
RESTIC_REPOSITORY = "/var/lib/backups/";
|
||||||
|
};
|
||||||
|
|
||||||
|
path = with pkgs; [
|
||||||
|
restic
|
||||||
|
];
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
# TODO(tlater): In an append-only setup, we should be
|
||||||
|
# careful with this; an attacker could delete backups by
|
||||||
|
# simply appending ad infinitum:
|
||||||
|
# https://restic.readthedocs.io/en/stable/060_forget.html#security-considerations-in-append-only-mode
|
||||||
|
restic forget --keep-last 3 --prune
|
||||||
|
restic check
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
DynamicUser = true;
|
||||||
|
Group = "backup";
|
||||||
|
|
||||||
|
CacheDirectory = "restic-prune";
|
||||||
|
CacheDirectoryMode = "0700";
|
||||||
|
ReadWritePaths = "/var/lib/backups/";
|
||||||
|
|
||||||
|
# Ensure we don't leave behind any files with the
|
||||||
|
# temporary UID of this service.
|
||||||
|
ExecStopPost = "+${pkgs.coreutils}/bin/chown -R root:backup /var/lib/backups/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.mapAttrs' (name: backup:
|
||||||
|
lib.nameValuePair "backup-${name}" {
|
||||||
|
# Don't want to restart mid-backup
|
||||||
|
restartIfChanged = false;
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RESTIC_CACHE_DIR = "%C/backup-${name}";
|
||||||
|
RESTIC_PASSWORD_FILE = config.sops.secrets."restic/local-backups".path;
|
||||||
|
# TODO(tlater): If I ever add more than one repo, service
|
||||||
|
# shutdown/restarting will potentially break if multiple
|
||||||
|
# backups for the same service overlap. A more clever
|
||||||
|
# sentinel file with reference counts would probably solve
|
||||||
|
# this.
|
||||||
|
RESTIC_REPOSITORY = "/var/lib/backups/";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
User = backup.user;
|
||||||
|
Group = "backup";
|
||||||
|
RuntimeDirectory = "backup-${name}";
|
||||||
|
CacheDirectory = "backup-${name}";
|
||||||
|
CacheDirectoryMode = "0700";
|
||||||
|
PrivateTmp = true;
|
||||||
|
|
||||||
|
ExecStart = [
|
||||||
|
(lib.concatStringsSep " " (["${pkgs.restic}/bin/restic" "backup" "--tag" name] ++ backup.paths))
|
||||||
|
];
|
||||||
|
|
||||||
|
ExecStartPre =
|
||||||
|
map (service: "+${mkShutdownScript service}") backup.pauseServices
|
||||||
|
++ singleton (writeScript "backup-${name}-repo-init" [pkgs.restic pkgs.coreutils] ''
|
||||||
|
restic snapshots || (restic init && chmod -R g+rwx "$RESTIC_REPOSITORY"/*)
|
||||||
|
'')
|
||||||
|
++ optional (backup.preparation.text != null)
|
||||||
|
(writeScript "backup-${name}-prepare" backup.preparation.packages backup.preparation.text);
|
||||||
|
|
||||||
|
# TODO(tlater): Add repo pruning/checking
|
||||||
|
ExecStopPost =
|
||||||
|
map (service: "+${mkRestartScript service}") backup.pauseServices
|
||||||
|
++ optional (backup.cleanup.text != null)
|
||||||
|
(writeScript "backup-${name}-cleanup" backup.cleanup.packages backup.cleanup.text);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
config.services.backups;
|
||||||
|
|
||||||
|
systemd.timers =
|
||||||
|
{
|
||||||
|
restic-prune = {
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
timerConfig.OnCalendar = "Thursday 03:00:00 UTC";
|
||||||
|
# Don't make this persistent, in case the server was offline
|
||||||
|
# for a while. This job cannot run at the same time as any
|
||||||
|
# of the backup jobs.
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// lib.mapAttrs' (name: backup:
|
||||||
|
lib.nameValuePair "backup-${name}" {
|
||||||
|
wantedBy = ["timers.target"];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "Wednesday 02:30:00 UTC";
|
||||||
|
RandomizedDelaySec = "1h";
|
||||||
|
FixedRandomDelay = true;
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
config.services.backups;
|
||||||
|
|
||||||
|
users.groups.backup = {};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/backups/ 0770 root backup"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
|
@ -231,4 +231,14 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.backups.conduit = {
|
||||||
|
user = "root";
|
||||||
|
paths = [
|
||||||
|
"/var/lib/matrix-conduit/"
|
||||||
|
];
|
||||||
|
# Other services store their data in conduit, so no other services
|
||||||
|
# need to be shut down currently.
|
||||||
|
pauseServices = ["conduit.service"];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
{config, ...}: let
|
{
|
||||||
|
pkgs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
domain = "gitea.${config.services.nginx.domain}";
|
domain = "gitea.${config.services.nginx.domain}";
|
||||||
in {
|
in {
|
||||||
services.gitea = {
|
services.gitea = {
|
||||||
|
@ -52,4 +56,24 @@ in {
|
||||||
enabled = true
|
enabled = true
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.backups.gitea = {
|
||||||
|
user = "gitea";
|
||||||
|
paths = [
|
||||||
|
"/var/lib/gitea/gitea-db.sql"
|
||||||
|
"/var/lib/gitea/repositories/"
|
||||||
|
"/var/lib/gitea/data/"
|
||||||
|
"/var/lib/gitea/custom/"
|
||||||
|
# Conf is backed up via nix
|
||||||
|
];
|
||||||
|
preparation = {
|
||||||
|
packages = [config.services.postgresql.package];
|
||||||
|
text = "pg_dump ${config.services.gitea.database.name} --file=/var/lib/gitea/gitea-db.sql";
|
||||||
|
};
|
||||||
|
cleanup = {
|
||||||
|
packages = [pkgs.coreutils];
|
||||||
|
text = "rm /var/lib/gitea/gitea-db.sql";
|
||||||
|
};
|
||||||
|
pauseServices = ["gitea.service"];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,4 +74,33 @@ in {
|
||||||
bantime = 86400
|
bantime = 86400
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.backups.nextcloud = {
|
||||||
|
user = "nextcloud";
|
||||||
|
paths = [
|
||||||
|
"/var/lib/nextcloud/nextcloud-db.sql"
|
||||||
|
"/var/lib/nextcloud/data/"
|
||||||
|
"/var/lib/nextcloud/config/config.php"
|
||||||
|
];
|
||||||
|
preparation = {
|
||||||
|
packages = [
|
||||||
|
config.services.postgresql.package
|
||||||
|
config.services.nextcloud.occ
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
nextcloud-occ maintenance:mode --on
|
||||||
|
pg_dump ${config.services.nextcloud.config.dbname} --file=/var/lib/nextcloud/nextcloud-db.sql
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
cleanup = {
|
||||||
|
packages = [
|
||||||
|
pkgs.coreutils
|
||||||
|
config.services.nextcloud.occ
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
rm /var/lib/nextcloud/nextcloud-db.sql
|
||||||
|
nextcloud-occ maintenance:mode --off
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,4 +110,12 @@ in {
|
||||||
# ProtectHome = "read-only"; # See further up
|
# ProtectHome = "read-only"; # See further up
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.backups.starbound = {
|
||||||
|
user = "root";
|
||||||
|
paths = [
|
||||||
|
"/var/lib/starbound/storage/universe/"
|
||||||
|
];
|
||||||
|
pauseServices = ["starbound.service"];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,12 @@
|
||||||
mode = "0440";
|
mode = "0440";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"restic/local-backups" = {
|
||||||
|
owner = "root";
|
||||||
|
group = "backup";
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
|
||||||
"turn/env" = {};
|
"turn/env" = {};
|
||||||
"turn/secret" = {
|
"turn/secret" = {
|
||||||
owner = "turnserver";
|
owner = "turnserver";
|
||||||
|
|
|
@ -7,6 +7,8 @@ heisenbridge:
|
||||||
hs-token: ENC[AES256_GCM,data:VBwvwomv0Xg=,iv:q6INtJ+rg+QiXj8uBdBzQYQZUBBXp+9odxDHwvu8Jxc=,tag:XKhm8nxygAkKaiVPJ2Fcdg==,type:str]
|
hs-token: ENC[AES256_GCM,data:VBwvwomv0Xg=,iv:q6INtJ+rg+QiXj8uBdBzQYQZUBBXp+9odxDHwvu8Jxc=,tag:XKhm8nxygAkKaiVPJ2Fcdg==,type:str]
|
||||||
wireguard:
|
wireguard:
|
||||||
server-key: ENC[AES256_GCM,data:FvY897XdKoa/mckE8JQLCkklsnYD6Wz1wpsu5t3uhEnW3iarnDQxF9msuYU=,iv:jqGXfekM+Vs+J9b5nlZ5Skd1ZKHajoUo2Dc4tMYPm1w=,tag:EehikjI/FCU8wqtpvJRamQ==,type:str]
|
server-key: ENC[AES256_GCM,data:FvY897XdKoa/mckE8JQLCkklsnYD6Wz1wpsu5t3uhEnW3iarnDQxF9msuYU=,iv:jqGXfekM+Vs+J9b5nlZ5Skd1ZKHajoUo2Dc4tMYPm1w=,tag:EehikjI/FCU8wqtpvJRamQ==,type:str]
|
||||||
|
restic:
|
||||||
|
local-backups: ENC[AES256_GCM,data:3QjEv03t7wE=,iv:y/6Lv4eUbZZfGPwUONykz8VNL62cAJuWaJy9yk3aAmk=,tag:wMlGsepuG9JjwtUKGWSibw==,type:str]
|
||||||
turn:
|
turn:
|
||||||
env: ENC[AES256_GCM,data:xjIz/AY109lyiL5N01p5T3HcYco/rM5CJSRTtg==,iv:16bW6OpyOK/QL0QPGQp/Baa9xyT8E3ZsYkwqmjuofk0=,tag:J5re3uKxIykw3YunvQWBgg==,type:str]
|
env: ENC[AES256_GCM,data:xjIz/AY109lyiL5N01p5T3HcYco/rM5CJSRTtg==,iv:16bW6OpyOK/QL0QPGQp/Baa9xyT8E3ZsYkwqmjuofk0=,tag:J5re3uKxIykw3YunvQWBgg==,type:str]
|
||||||
secret: ENC[AES256_GCM,data:eQ7dAocoZtg=,iv:fgzjTPv30WqTKlLy+yMn5MsKQgjhPnwlGFFwYEg3gWs=,tag:1ze33U1NBkgMX/9SiaBNQg==,type:str]
|
secret: ENC[AES256_GCM,data:eQ7dAocoZtg=,iv:fgzjTPv30WqTKlLy+yMn5MsKQgjhPnwlGFFwYEg3gWs=,tag:1ze33U1NBkgMX/9SiaBNQg==,type:str]
|
||||||
|
@ -19,8 +21,8 @@ sops:
|
||||||
azure_kv: []
|
azure_kv: []
|
||||||
hc_vault: []
|
hc_vault: []
|
||||||
age: []
|
age: []
|
||||||
lastmodified: "2023-04-23T17:35:16Z"
|
lastmodified: "2023-09-22T21:07:02Z"
|
||||||
mac: ENC[AES256_GCM,data:4cW8k6o3jET8k+yJGyApjOyuSUQb+d+4wX/RTNnpbt+867sExQrZUrOMif/u8S4WmcKVSJgvrzuxK9hpDPYhJ1d/5YuHH1Dyj7QDRdhbZYHhkpPus0ZVTEpSknZzx2eWH1ch/fyJJknlrBlfb/tz50Dv+w9mhkL7qteaIq+Vmsc=,iv:YMfAuGwu1kAM0wGkq3kzVMnC72yo7ZT04BuEwoLRPIA=,tag:6I1VRzteRaLuxN+sfLA5Mw==,type:str]
|
mac: ENC[AES256_GCM,data:gItC41S8MInLmikdH1okhPs+FVf8sCF/iQeJ5reigBunHkOngoc6nOFANyAcNZETszzhgTLXXtmVNEjW46v6K7D6nmoi/zwpedUxwzMwDC5I28VTMDHVMAThYSGtdo6kig8i2pi8rzEQd1DStxMv3TWML5y6DDTlFsd3lfudaHA=,iv:zXebvIVPR76GwUhpactwRgF/eEmx2OBkT18E8lkwzRA=,tag:6HyISACbFCGlpIIgkFeA/A==,type:str]
|
||||||
pgp:
|
pgp:
|
||||||
- created_at: "2022-10-12T16:48:23Z"
|
- created_at: "2022-10-12T16:48:23Z"
|
||||||
enc: |
|
enc: |
|
||||||
|
|
Loading…
Reference in a new issue