always pass secrets as files
This commit is contained in:
parent
fd6abfc50d
commit
a302112120
1 changed files with 81 additions and 121 deletions
202
flake.nix
202
flake.nix
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue