From a3021121201f9eea1f39bc253ce992bf4f178416 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 4 Apr 2023 16:34:42 -0500 Subject: [PATCH] always pass secrets as files --- flake.nix | 202 ++++++++++++++++++++++-------------------------------- 1 file changed, 81 insertions(+), 121 deletions(-) diff --git a/flake.nix b/flake.nix index f370375..33ea0f3 100644 --- a/flake.nix +++ b/flake.nix @@ -88,12 +88,7 @@ options = { enable = lib.options.mkEnableOption "Restic"; passwordFile = lib.options.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - }; - password = lib.options.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.path; }; storage = lib.options.mkOption { type = lib.types.enum [ @@ -105,10 +100,10 @@ type = lib.types.submodule { options = { enable = lib.options.mkEnableOption "use healthcheck"; - api_url = lib.options.mkOption { + apiUrl = lib.options.mkOption { type = lib.types.str; }; - api_key = lib.options.mkOption { + apiKeyFile = lib.options.mkOption { type = lib.types.str; }; timeout = lib.options.mkOption { @@ -128,10 +123,10 @@ bucket = lib.options.mkOption { type = lib.types.str; }; - account_id = lib.options.mkOption { + accountId = lib.options.mkOption { type = lib.types.str; }; - account_key = lib.options.mkOption { + accountKeyFile = lib.options.mkOption { type = lib.types.str; }; }; @@ -140,11 +135,12 @@ azure = lib.options.mkOption { type = lib.types.submodule { options = { - account_name = lib.options.mkOption { + accountName = lib.options.mkOption { type = lib.types.str; }; - account_key = lib.options.mkOption { - type = lib.types.str; + accountKeyFile = lib.options.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; }; }; }; @@ -153,7 +149,7 @@ type = lib.types.listOf ( lib.types.submodule { options = { - one-file-system = lib.options.mkOption { + oneFileSystem = lib.options.mkOption { type = lib.types.bool; default = false; }; @@ -180,131 +176,61 @@ }; }; }; + config = let + bucket = { "b2" = cfg.b2.bucket; "azure" = builtins.replaceStrings [ "." ] [ "-" ] "${config.networking.hostName}.${config.networking.domain}"; }.${cfg.storage}; + directory = { "b2" = "${config.networking.hostName}.${config.networking.domain}"; "azure" = "/"; }.${cfg.storage}; + in 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 = [ 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 = let curlOptions = "--fail --silent --show-error --max-time 10 --retry 5"; hcPayload = if cfg.healthcheck.enable then - builtins.toJSON - { - name = "${config.networking.hostName}.${config.networking.domain}"; - timeout = cfg.healthcheck.timeout; - grace = cfg.healthcheck.grace; - unique = [ "name" ]; - channels = "*"; - } + ( + pkgs.writeText "healthcheck-payload" + ( + builtins.toJSON + { + name = "${config.networking.hostName}.${config.networking.domain}"; + timeout = cfg.healthcheck.timeout; + grace = cfg.healthcheck.grace; + unique = [ "name" ]; + channels = "*"; + } + ) + ) else - ""; + null; hcSetup = if cfg.healthcheck.enable then '' - HC_PAYLOAD='${hcPayload}' - 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) + 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) '' else ""; hcStart = if cfg.healthcheck.enable then '' - if [ ! -z "$HC_URL" ] + if [ ! -z "''${HC_URL}" ] 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 '' else @@ -312,36 +238,56 @@ hcStop = if cfg.healthcheck.enable then '' - if [ ! -z "$HC_URL" ] + if [ ! -z "''${HC_URL}" ] then - ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null $HC_URL || true + ${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null "''${HC_URL}" || true fi '' 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" ( map ( backup: let - one-file-system = - if backup.one-file-system + oneFileSystem = + if backup.oneFileSystem then " --one-file-system" else ""; excludes = lib.strings.concatMapStrings ( - arg: " --exclude=${arg}" + arg: '' --exclude="${arg}"'' ) backup.excludes; paths = lib.strings.concatStringsSep " " backup.paths; in '' ${backup.preCommand} - - ${package}/bin/restic backup${one-file-system}${excludes} ${paths} - + ${package}/bin/restic backup${oneFileSystem}${excludes} ${paths} ${backup.postCommand} '' ) @@ -349,22 +295,35 @@ ); initCheck = { "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 ${package}/bin/restic init fi ''; "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') - if [ "$container" != "true" ] + export RCLONE_CONFIG=/dev/null + 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 - ${pkgs.rclone}/bin/rclone --config /etc/restic/rclone mkdir "azure:${bucket}" + ${pkgs.rclone}/bin/rclone mkdir 'azure:${bucket}' 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 ${package}/bin/restic init fi @@ -375,12 +334,13 @@ serviceConfig = { Type = "oneshot"; CacheDirectory = "restic"; - EnvironmentFile = "/etc/restic/environment"; }; script = '' ${hcSetup} ${hcStart} + ${resticConfig} + ${initCheck} ${backupCommands}