always pass secrets as files

This commit is contained in:
Jeffrey C. Ollie 2023-04-04 16:34:42 -05:00
parent fd6abfc50d
commit a302112120
No known key found for this signature in database
GPG key ID: F936E4DCB7E25F15

202
flake.nix
View file

@ -88,12 +88,7 @@
options = { options = {
enable = lib.options.mkEnableOption "Restic"; enable = lib.options.mkEnableOption "Restic";
passwordFile = lib.options.mkOption { passwordFile = lib.options.mkOption {
type = lib.types.nullOr lib.types.str; type = lib.types.path;
default = null;
};
password = lib.options.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
}; };
storage = lib.options.mkOption { storage = lib.options.mkOption {
type = lib.types.enum [ type = lib.types.enum [
@ -105,10 +100,10 @@
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
enable = lib.options.mkEnableOption "use healthcheck"; enable = lib.options.mkEnableOption "use healthcheck";
api_url = lib.options.mkOption { apiUrl = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
api_key = lib.options.mkOption { apiKeyFile = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
timeout = lib.options.mkOption { timeout = lib.options.mkOption {
@ -128,10 +123,10 @@
bucket = lib.options.mkOption { bucket = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
account_id = lib.options.mkOption { accountId = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
account_key = lib.options.mkOption { accountKeyFile = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
}; };
@ -140,11 +135,12 @@
azure = lib.options.mkOption { azure = lib.options.mkOption {
type = lib.types.submodule { type = lib.types.submodule {
options = { options = {
account_name = lib.options.mkOption { accountName = lib.options.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
account_key = lib.options.mkOption { accountKeyFile = lib.options.mkOption {
type = lib.types.str; type = lib.types.nullOr lib.types.str;
default = null;
}; };
}; };
}; };
@ -153,7 +149,7 @@
type = lib.types.listOf ( type = lib.types.listOf (
lib.types.submodule { lib.types.submodule {
options = { options = {
one-file-system = lib.options.mkOption { oneFileSystem = lib.options.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
@ -180,131 +176,61 @@
}; };
}; };
}; };
config = config =
let let
bucket = { bucket = {
"b2" = cfg.b2.bucket; "b2" = cfg.b2.bucket;
"azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}";
}.${cfg.storage}; }.${cfg.storage};
directory = { directory = {
"b2" = "${config.networking.hostName}.${config.networking.domain}"; "b2" = "${config.networking.hostName}.${config.networking.domain}";
"azure" = "/"; "azure" = "/";
}.${cfg.storage}; }.${cfg.storage};
in in
lib.mkIf cfg.enable { lib.mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.passwordFile == null && cfg.password == null);
message = "Must specifiy either passwordFile or password";
}
];
warnings =
if cfg.password != null then [
''Restic encryption password will be stored world readable in the Nix store.''
] else [ ];
environment.systemPackages = [ environment.systemPackages = [
package package
]; ];
environment.etc."restic/password" = lib.mkIf (cfg.password != null) {
text = cfg.password;
user = "root";
group = "root";
mode = "0400";
};
environment.etc."restic/environment" =
let
hcConfig =
if cfg.healthcheck.enable
then ''
HC_API_KEY=${cfg.healthcheck.api_key}
HC_API_URL=${cfg.healthcheck.api_url}
''
else
"";
storageConfig = {
"b2" = ''
B2_ACCOUNT_ID=${cfg.b2.account_id}
B2_ACCOUNT_KEY=${cfg.b2.account_key}
'';
"azure" = ''
AZURE_ACCOUNT_NAME=${cfg.azure.account_name}
AZURE_ACCOUNT_KEY=${cfg.azure.account_key}
'';
}.${cfg.storage};
repositoryConfig = {
"b2" = "b2:${bucket}:${directory}";
"azure" = "azure:${bucket}:${directory}";
}.${cfg.storage};
in
{
text = ''
${hcConfig}
RESTIC_CACHE_DIR=/var/cache/restic
RESTIC_PASSWORD_FILE=${if cfg.passwordFile != null then cfg.passwordFile else "/etc/restic/password"}
RESTIC_REPOSITORY=${repositoryConfig}
${storageConfig}
'';
user = "root";
group = "root";
mode = "0400";
};
environment.etc."restic/rclone" = {
text = {
"b2" = ''
[b2]
type = b2
account = ${cfg.b2.account_id}
key = ${cfg.b2.account_key}
'';
"azure" = ''
[azure]
type = azureblob
account = ${cfg.azure.account_name}
key = ${cfg.azure.account_key}
'';
}.${cfg.storage};
user = "root";
group = "root";
mode = "0400";
};
systemd.services.restic = systemd.services.restic =
let let
curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; curlOptions = "--fail --silent --show-error --max-time 10 --retry 5";
hcPayload = hcPayload =
if cfg.healthcheck.enable if cfg.healthcheck.enable
then then
builtins.toJSON (
{ pkgs.writeText "healthcheck-payload"
name = "${config.networking.hostName}.${config.networking.domain}"; (
timeout = cfg.healthcheck.timeout; builtins.toJSON
grace = cfg.healthcheck.grace; {
unique = [ "name" ]; name = "${config.networking.hostName}.${config.networking.domain}";
channels = "*"; timeout = cfg.healthcheck.timeout;
} grace = cfg.healthcheck.grace;
unique = [ "name" ];
channels = "*";
}
)
)
else else
""; null;
hcSetup = hcSetup =
if cfg.healthcheck.enable if cfg.healthcheck.enable
then '' then ''
HC_PAYLOAD='${hcPayload}' HC_URL=''$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header 'Content-Type: application/json' --header "X-Api-Key: ''$(<${cfg.healthcheck.apiKeyFile})" --data @${hcPayload} "${cfg.healthcheck.apiUrl}" | ${pkgs.jq}/bin/jq -r .ping_url)
HC_URL=$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header "X-Api-Key: $HC_API_KEY" --data "$HC_PAYLOAD" $HC_API_URL | ${pkgs.jq}/bin/jq -r .ping_url)
'' ''
else else
""; "";
hcStart = hcStart =
if cfg.healthcheck.enable if cfg.healthcheck.enable
then '' then ''
if [ ! -z "$HC_URL" ] if [ ! -z "''${HC_URL}" ]
then then
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL/start || true ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null "''${HC_URL}/start" || true
fi fi
'' ''
else else
@ -312,36 +238,56 @@
hcStop = hcStop =
if cfg.healthcheck.enable if cfg.healthcheck.enable
then '' then ''
if [ ! -z "$HC_URL" ] if [ ! -z "''${HC_URL}" ]
then then
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null "''${HC_URL}" || true
fi fi
'' ''
else else
""; "";
repositoryConfig = {
"b2" = "b2:${bucket}:${directory}";
"azure" = "azure:${bucket}:${directory}";
}.${cfg.storage};
resticConfig = ''
export RESTIC_CACHE_DIR=/var/cache/restic
export RESTIC_PASSWORD_FILE=${cfg.passwordFile}
export RESTIC_REPOSITORY=${repositoryConfig}
''
+ {
"b2" = ''
export B2_ACCOUNT_ID='${cfg.b2.accountId}'
export B2_ACCOUNT_KEY="''$(<${cfg.b2.accountKeyFile})"
'';
"azure" = ''
export AZURE_ACCOUNT_NAME='${cfg.azure.accountName}'
export AZURE_ACCOUNT_KEY="''$(<${cfg.azure.accountKeyFile})"
'';
}.${cfg.storage};
backupCommands = lib.strings.concatStringsSep "\n" ( backupCommands = lib.strings.concatStringsSep "\n" (
map map
( (
backup: backup:
let let
one-file-system = oneFileSystem =
if backup.one-file-system if backup.oneFileSystem
then then
" --one-file-system" " --one-file-system"
else else
""; "";
excludes = lib.strings.concatMapStrings excludes = lib.strings.concatMapStrings
( (
arg: " --exclude=${arg}" arg: '' --exclude="${arg}"''
) )
backup.excludes; backup.excludes;
paths = lib.strings.concatStringsSep " " backup.paths; paths = lib.strings.concatStringsSep " " backup.paths;
in in
'' ''
${backup.preCommand} ${backup.preCommand}
${package}/bin/restic backup${oneFileSystem}${excludes} ${paths}
${package}/bin/restic backup${one-file-system}${excludes} ${paths}
${backup.postCommand} ${backup.postCommand}
'' ''
) )
@ -349,22 +295,35 @@
); );
initCheck = { initCheck = {
"b2" = '' "b2" = ''
config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "b2:${bucket}/${directory}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0') export RCLONE_CONFIG=/dev/null
export RCLONE_CONFIG_B2_TYPE=b2
export RCLONE_CONFIG_B2_ACCOUNT='${cfg.b2.accountId}'
export RCLONE_CONFIG_B2_KEY="''$(<${cfg.b2.accountKeyFile})"
if [ "$config" != "true" ] config=''$(${pkgs.rclone}/bin/rclone lsjson 'b2:${bucket}/${directory}/config' | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
if [ "''${config}" != 'true' ]
then then
${package}/bin/restic init ${package}/bin/restic init
fi fi
''; '';
"azure" = '' "azure" = ''
container=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:" | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0') export RCLONE_CONFIG=/dev/null
if [ "$container" != "true" ] export RCLONE_CONFIG_AZURE_TYPE=azureblob
export RCLONE_CONFIG_AZURE_ACCOUNT='${cfg.azure.accountName}'
export RCLONE_CONFIG_AZURE_KEY="''$(<${cfg.azure.accountKeyFile})"
container=''$(${pkgs.rclone}/bin/rclone lsjson 'azure:' | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0')
if [ "''${container}" != 'true' ]
then then
${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" ${pkgs.rclone}/bin/rclone mkdir 'azure:${bucket}'
fi fi
config=$(${pkgs.rclone}/bin/rclone --config /etc/restic/rclone lsjson "azure:${bucket}/config" | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
if [ "$config" != "true" ] config=$(${pkgs.rclone}/bin/rclone lsjson 'azure:${bucket}/config' | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
if [ "''${config}" != 'true' ]
then then
${package}/bin/restic init ${package}/bin/restic init
fi fi
@ -375,12 +334,13 @@
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
CacheDirectory = "restic"; CacheDirectory = "restic";
EnvironmentFile = "/etc/restic/environment";
}; };
script = '' script = ''
${hcSetup} ${hcSetup}
${hcStart} ${hcStart}
${resticConfig}
${initCheck} ${initCheck}
${backupCommands} ${backupCommands}