1
1
Fork 1
mirror of https://github.com/oddlama/nixos-extra-modules.git synced 2025-10-10 13:50:39 +02:00

feat: add hetzner storage box intergation for restic

This commit is contained in:
oddlama 2024-01-15 01:40:09 +01:00
parent 4744a2844c
commit a776d7c476
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
4 changed files with 158 additions and 0 deletions

View file

@ -18,6 +18,7 @@ EFI/BIOS boot config | Module | [Link](./modules/boot.nix) | - | - | Allows you
Nginx recommended options | Module | [Link](./modules/nginx.nix) | - | agenix | Sets many recommended settings for nginx with a single switch plus some opinionated defaults. Also adds a switch for setting recommended security headers on each location.
Node options | Module | [Link](./modules/node.nix) | - | - | A module that stores meta information about your nodes (hosts). Required for some other modules that operate across nodes.
Guests (MicroVMs & Containers) | Module | [Link](./modules/guests) | zfs, node options | - | This module implements a common interface to use guest systems with microvms or nixos-containers.
Restic hetzner storage box setup | Module | [Link](./modules/restic.nix) | - | - | This module exposes new options for restic backups that allow a simple setup of hetzner storage boxes. There's [an app](./apps/setup-hetzner-storage-boxes.nix) that you should expose on your flake to automate remote setup.
#### Home Manager Modules

View file

@ -0,0 +1,100 @@
{
pkgs,
nixosConfigurations,
decryptIdentity,
}: let
inherit
(pkgs.lib)
attrValues
concatLines
concatStringsSep
escapeShellArg
filterAttrs
flatten
flip
getExe
groupBy
head
length
mapAttrs
mapAttrsToList
optional
throwIf
unique
;
allBoxDefinitions = flatten (
flip map (attrValues nixosConfigurations) (
hostCfg:
flip map (attrValues hostCfg.config.services.restic.backups) (
backupCfg:
optional backupCfg.hetznerStorageBox.enable backupCfg.hetznerStorageBox
)
)
);
subUserFor = box: "${box.mainUser}-sub${toString box.subUid}";
boxesBySubuser = groupBy subUserFor allBoxDefinitions;
# We need to know the main storage box user to create subusers
boxSubuserToMainUser =
flip mapAttrs boxesBySubuser (_: boxes:
head (unique (flip map boxes (box: box.mainUser))));
boxSubuserToPrivateKeys =
flip mapAttrs boxesBySubuser (_: boxes:
unique (flip map boxes (box: box.sshPrivateKeyFile)));
# Any subuid that has more than one path in use
boxSubuserToPaths =
flip mapAttrs boxesBySubuser (_: boxes:
unique (flip map boxes (box: box.path)));
duplicates = filterAttrs (_: boxes: length boxes > 1) boxSubuserToPaths;
# Only one path must remain per subuser.
boxSubuserToPath = throwIf (duplicates != {}) ''
At least one storage box subuser has multiple paths assigned to it:
${concatStringsSep "\n" (mapAttrsToList (n: v: "${n}: ${toString v}") duplicates)}
'' (mapAttrs (_: head) boxSubuserToPaths);
appendPubkey = privateKey: ''
PATH="$PATH:${pkgs.age-plugin-yubikey}/bin" ${pkgs.rage}/bin/rage -d -i ${decryptIdentity} ${escapeShellArg privateKey} \
| (exec 3<&0; ssh-keygen -f /proc/self/fd/3 -y) \
>> "$TMPFILE"
'';
setupSubuser = subuser: privateKeys: let
mainUser = boxSubuserToMainUser.${subuser};
path = boxSubuserToPath.${subuser};
in ''
echo "${mainUser} (for ${subuser}): Removing old ${path}/.ssh if it exists"
# Remove any .ssh folder if it exists
${pkgs.openssh}/bin/ssh -p 23 "${mainUser}@${mainUser}.your-storagebox.de" -- rm -r ./${path}/.ssh &>/dev/null || true
echo "${mainUser} (for ${subuser}): Creating ${path}/.ssh"
# Create subuser directory and .ssh
${pkgs.openssh}/bin/ssh -p 23 "${mainUser}@${mainUser}.your-storagebox.de" -- mkdir -p ./${path}/.ssh
# Derive and upload all authorized keys
TMPFILE=$(mktemp)
${concatLines (map appendPubkey privateKeys)}
echo "${mainUser} (for ${subuser}): Uploading $(wc -l < "$TMPFILE") authorized_keys"
${pkgs.openssh}/bin/scp -P 23 "$TMPFILE" "${mainUser}@${mainUser}.your-storagebox.de":./${path}/.ssh/authorized_keys
rm "$TMPFILE"
'';
in {
type = "app";
program = getExe (pkgs.writeShellApplication {
name = "setup-hetzner-storage-boxes";
text = ''
set -euo pipefail
${concatLines (mapAttrsToList setupSubuser boxSubuserToPrivateKeys)}
echo
echo "Please visit https://robot.hetzner.com/storage and make sure"
echo "that the following subusers are setup correctly:"
${concatLines (mapAttrsToList (u: p: "echo ' ${u}: ${p}'") boxSubuserToPath)}
'';
});
}

View file

@ -7,6 +7,7 @@
./interface-naming.nix
./nginx.nix
./node.nix
./restic.nix
];
nixpkgs.overlays = [

56
modules/restic.nix Normal file
View file

@ -0,0 +1,56 @@
{lib, ...}: let
inherit
(lib)
mkEnableOption
mkIf
mkOption
types
;
in {
options.services.restic.backups = mkOption {
type = types.attrsOf (types.submodule ({config, ...}: {
options.hetznerStorageBox = {
enable = mkEnableOption "Automatically configure this backup to use the given hetzner storage box. Will use SFTP via SSH.";
mainUser = mkOption {
type = types.str;
description = ''
The main user. While not technically required for restic, we still use it to
derive the subuser name and it is required for the automatic setup script
that creates the users.
'';
};
subUid = mkOption {
type = types.int;
description = "The id of the subuser that was allocated on the hetzner server for this backup.";
};
path = mkOption {
type = types.str;
description = ''
The remote path to backup into. While not technically required for restic
(since the subuser is chrooted on the remote), it is required for the
automatic setup script that creates the users.
'';
};
sshPrivateKeyFile = mkOption {
type = types.path;
description = "The path to the ssh private key to use for uploading backups. Don't use a path from the nix store!";
};
};
config = let
subuser = "${config.hetznerStorageBox.mainUser}-sub${toString config.hetznerStorageBox.subUid}";
url = "${subuser}@${subuser}.your-storagebox.de";
in
mkIf config.hetznerStorageBox.enable {
repository = "sftp://${url}:23/";
extraOptions = [
"sftp.command='ssh -s sftp -p 23 -i ${config.hetznerStorageBox.sshPrivateKeyFile} ${url}'"
];
};
}));
};
}