From df8580dc6f2078eeb5c46027b0102dac2d5634fc Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 5 May 2025 16:37:46 +0200 Subject: [PATCH 1/4] pgbackrest: modernize --- pkgs/by-name/pg/pgbackrest/package.nix | 46 +++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pkgs/by-name/pg/pgbackrest/package.nix b/pkgs/by-name/pg/pgbackrest/package.nix index 9a9be35d2982..da6634889d89 100644 --- a/pkgs/by-name/pg/pgbackrest/package.nix +++ b/pkgs/by-name/pg/pgbackrest/package.nix @@ -1,31 +1,31 @@ { - lib, - stdenv, - fetchFromGitHub, - meson, - ninja, - python3, - pkg-config, - libbacktrace, bzip2, - lz4, + fetchFromGitHub, + lib, + libbacktrace, libpq, + libssh2, libxml2, libyaml, + lz4, + meson, + ninja, + pkg-config, + python3, + stdenv, zlib, - libssh2, zstd, }: -stdenv.mkDerivation rec { +stdenv.mkDerivation (finalAttrs: { pname = "pgbackrest"; version = "2.55.1"; src = fetchFromGitHub { owner = "pgbackrest"; repo = "pgbackrest"; - rev = "release/${version}"; - sha256 = "sha256-A1dTywcCHBu7Ml0Q9k//VVPFN1C3kmmMkq4ok9T4g94="; + tag = "release/${finalAttrs.version}"; + hash = "sha256-A1dTywcCHBu7Ml0Q9k//VVPFN1C3kmmMkq4ok9T4g94="; }; strictDeps = true; @@ -33,28 +33,28 @@ stdenv.mkDerivation rec { nativeBuildInputs = [ meson ninja - python3 pkg-config + python3 ]; buildInputs = [ - libbacktrace bzip2 - lz4 + libbacktrace libpq + libssh2 libxml2 libyaml + lz4 zlib - libssh2 zstd ]; - meta = with lib; { + meta = { description = "Reliable PostgreSQL backup & restore"; - homepage = "https://pgbackrest.org/"; - changelog = "https://github.com/pgbackrest/pgbackrest/releases"; - license = licenses.mit; + homepage = "https://pgbackrest.org"; + changelog = "https://github.com/pgbackrest/pgbackrest/releases/tag/release%2F${finalAttrs.version}"; + license = lib.licenses.mit; mainProgram = "pgbackrest"; - maintainers = with maintainers; [ zaninime ]; + maintainers = with lib.maintainers; [ zaninime ]; }; -} +}) From d5ddb6116f492553e1bebbc614dd62034c4b1ca6 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 25 Apr 2025 14:37:36 +0200 Subject: [PATCH 2/4] nixos/pgbackrest: init module --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/backup/pgbackrest.nix | 353 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pgbackrest/default.nix | 4 + nixos/tests/pgbackrest/posix.nix | 147 ++++++++ pkgs/by-name/pg/pgbackrest/package.nix | 3 + 7 files changed, 511 insertions(+) create mode 100644 nixos/modules/services/backup/pgbackrest.nix create mode 100644 nixos/tests/pgbackrest/default.nix create mode 100644 nixos/tests/pgbackrest/posix.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 71cf50a5f015..ebbd62adee7c 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -208,6 +208,8 @@ - [GLPI-Agent](https://github.com/glpi-project/glpi-agent), GLPI Agent. Available as [services.glpiAgent](options.html#opt-services.glpiAgent.enable). +- [pgBackRest](https://pgbackrest.org), a reliable backup and restore solution for PostgreSQL. Available as [services.pgbackrest](options.html#opt-services.pgbackrest.enable). + - [Recyclarr](https://github.com/recyclarr/recyclarr) a TRaSH Guides synchronizer for Sonarr and Radarr. Available as [services.recyclarr](#opt-services.recyclarr.enable). - [Rebuilderd](https://github.com/kpcyrd/rebuilderd) an independent verification of binary packages - Reproducible Builds. Available as [services.rebuilderd](#opt-services.rebuilderd.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 7284ca5e39ac..b0078c57335c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -440,6 +440,7 @@ ./services/backup/duplicati.nix ./services/backup/duplicity.nix ./services/backup/mysql-backup.nix + ./services/backup/pgbackrest.nix ./services/backup/postgresql-backup.nix ./services/backup/postgresql-wal-receiver.nix ./services/backup/restic-rest-server.nix diff --git a/nixos/modules/services/backup/pgbackrest.nix b/nixos/modules/services/backup/pgbackrest.nix new file mode 100644 index 000000000000..da3c719c217a --- /dev/null +++ b/nixos/modules/services/backup/pgbackrest.nix @@ -0,0 +1,353 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.pgbackrest; + + settingsFormat = pkgs.formats.ini { + listsAsDuplicateKeys = true; + }; + + # pgBackRest "options" + settingsType = + with lib.types; + attrsOf (oneOf [ + bool + ints.unsigned + str + (attrsOf str) + (listOf str) + ]); + + # Applied to both repoNNN-* and pgNNN-* options in global and stanza sections. + flattenWithIndex = + attrs: prefix: + lib.concatMapAttrs ( + name: + let + index = lib.lists.findFirstIndex (n: n == name) null (lib.attrNames attrs); + index1 = index + 1; + in + lib.mapAttrs' (option: lib.nameValuePair "${prefix}${toString index1}-${option}") + ) attrs; + + # Remove nulls, turn attrsets into lists and bools into y/n + normalize = + x: + lib.pipe x [ + (lib.filterAttrs (_: v: v != null)) + (lib.mapAttrs (_: v: if lib.isAttrs v then lib.mapAttrsToList (n': v': "${n'}=${v'}") v else v)) + (lib.mapAttrs ( + _: v: + if v == true then + "y" + else if v == false then + "n" + else + v + )) + ]; + + fullConfig = + { + global = normalize (cfg.settings // flattenWithIndex cfg.repos "repo"); + } + // lib.mapAttrs ( + _: cfg': normalize (cfg'.settings // flattenWithIndex cfg'.instances "pg") + ) cfg.stanzas; + + namedJobs = lib.listToAttrs ( + lib.flatten ( + lib.mapAttrsToList ( + stanza: + { jobs, ... }: + lib.mapAttrsToList ( + job: attrs: lib.nameValuePair "pgbackrest-${stanza}-${job}" (attrs // { inherit stanza job; }) + ) jobs + ) cfg.stanzas + ) + ); +in + +{ + meta = { + maintainers = with lib.maintainers; [ wolfgangwalther ]; + }; + + # TODO: Add enableServer option and corresponding pgBackRest TLS server service. + # TODO: Allow command-specific options + # TODO: Write wrapper around pgbackrest to turn --repo= into --repo= + # The following two are dependent on improvements upstream: + # https://github.com/pgbackrest/pgbackrest/issues/2621 + # TODO: Add support for more repository types + # TODO: Support passing encryption key safely + options.services.pgbackrest = { + enable = lib.mkEnableOption "pgBackRest"; + + repos = lib.mkOption { + type = + with lib.types; + attrsOf ( + submodule ( + { config, name, ... }: + let + setHostForType = + type: + if name == "localhost" then + null + # "posix" is the default repo type, which uses the -host option. + else if config.type or "posix" != type then + null + else + name; + in + { + freeformType = settingsType; + + options.host = lib.mkOption { + type = nullOr str; + default = setHostForType "posix"; + defaultText = lib.literalExpression "name"; + description = "Repository host when operating remotely"; + }; + } + ) + ); + default = { }; + description = '' + An attribute set of repositories as described in: + + + Each repository defaults to set `repo-host` to the attribute's name. + The special value "localhost" will unset `repo-host`. + + ::: {.note} + The prefix `repoNNN-` is added automatically. + Example: Use `path` instead of `repo1-path`. + ::: + ''; + example = lib.literalExpression '' + { + localhost.path = "/var/lib/backup"; + "backup.example.com".host-type = "tls"; + } + ''; + }; + + stanzas = lib.mkOption { + type = + with lib.types; + attrsOf (submodule { + options = { + jobs = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options.schedule = lib.mkOption { + type = lib.types.str; + description = '' + When or how often the backup should run. + Must be in the format described in {manpage}`systemd.time(7)`. + ''; + }; + + options.type = lib.mkOption { + type = lib.types.str; + description = '' + Backup type as described in: + + ''; + }; + } + ); + default = { }; + description = '' + Backups jobs to schedule for this stanza as described in: + + ''; + example = lib.literalExpression '' + { + weekly = { schedule = "Sun, 6:30"; type = "full"; }; + daily = { schedule = "Mon..Sat, 6:30"; type = "diff"; }; + } + ''; + }; + + instances = lib.mkOption { + type = + with lib.types; + attrsOf ( + submodule ( + { name, ... }: + { + freeformType = settingsType; + options.host = lib.mkOption { + type = nullOr str; + default = if name == "localhost" then null else name; + defaultText = lib.literalExpression ''if name == "localhost" then null else name''; + description = "PostgreSQL host for operating remotely."; + }; + } + ) + ); + default = { }; + description = '' + An attribute set of database instances as described in: + + + Each instance defaults to set `pg-host` to the attribute's name. + The special value "localhost" will unset `pg-host`. + + ::: {.note} + The prefix `pgNNN-` is added automatically. + Example: Use `user` instead of `pg1-user`. + ::: + ''; + example = lib.literalExpression '' + { + localhost.database = "app"; + "postgres.example.com".port = "5433"; + } + ''; + }; + + settings = lib.mkOption { + type = settingsType; + default = { }; + description = '' + An attribute set of options as described in: + + + All options can be used. + Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. + Stanza options should be set via [`instances`](#opt-services.pgbackrest.stanzas._name_.instances) instead. + ''; + example = lib.literalExpression '' + { + process-max = 2; + } + ''; + }; + }; + }); + default = { }; + description = '' + An attribute set of stanzas as described in: + + ''; + }; + + settings = lib.mkOption { + type = settingsType; + default = { }; + description = '' + An attribute set of options as described in: + + + All globally available options, i.e. all except stanza options, can be used. + Repository options should be set via [`repos`](#opt-services.pgbackrest.repos) instead. + ''; + example = lib.literalExpression '' + { + process-max = 2; + } + ''; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + services.pgbackrest.settings = { + log-level-console = lib.mkDefault "info"; + log-level-file = lib.mkDefault "off"; + cmd-ssh = lib.getExe pkgs.openssh; + }; + + environment.systemPackages = [ pkgs.pgbackrest ]; + environment.etc."pgbackrest/pgbackrest.conf".source = + settingsFormat.generate "pgbackrest.conf" fullConfig; + + users.users.pgbackrest = { + name = "pgbackrest"; + group = "pgbackrest"; + description = "pgBackRest service user"; + isSystemUser = true; + useDefaultShell = true; + createHome = true; + home = cfg.repos.localhost.path or "/var/lib/pgbackrest"; + }; + users.groups.pgbackrest = { }; + + systemd.services = lib.mapAttrs ( + _: + { + stanza, + job, + type, + ... + }: + { + description = "pgBackRest job ${job} for stanza ${stanza}"; + + serviceConfig = { + User = "pgbackrest"; + Group = "pgbackrest"; + Type = "oneshot"; + # stanza-create is idempotent, so safe to always run + ExecStartPre = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' stanza-create"; + ExecStart = "${lib.getExe pkgs.pgbackrest} --stanza='${stanza}' backup --type='${type}'"; + }; + } + ) namedJobs; + + systemd.timers = lib.mapAttrs ( + name: + { + stanza, + job, + schedule, + ... + }: + { + description = "pgBackRest job ${job} for stanza ${stanza}"; + wantedBy = [ "timers.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + timerConfig = { + OnCalendar = schedule; + Persistent = true; + Unit = "${name}.service"; + }; + } + ) namedJobs; + } + + # The default stanza is set up for the local postgresql instance. + # It does not backup automatically, the systemd timer still needs to be set. + (lib.mkIf config.services.postgresql.enable { + services.pgbackrest.stanzas.default = { + settings.cmd = lib.getExe pkgs.pgbackrest; + instances.localhost = { + path = config.services.postgresql.dataDir; + user = "postgres"; + }; + }; + services.postgresql.identMap = '' + postgres pgbackrest postgres + ''; + services.postgresql.initdbArgs = [ "--allow-group-access" ]; + users.users.pgbackrest.extraGroups = [ "postgres" ]; + + services.postgresql.settings = { + archive_command = ''${lib.getExe pkgs.pgbackrest} --stanza=default archive-push "%p"''; + archive_mode = lib.mkDefault "on"; + }; + users.groups.pgbackrest.members = [ "postgres" ]; + }) + ] + ); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index f0e816446511..50fb8a3119be 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1025,6 +1025,7 @@ in peertube = handleTestOn [ "x86_64-linux" ] ./web-apps/peertube.nix { }; peroxide = handleTest ./peroxide.nix { }; pgadmin4 = runTest ./pgadmin4.nix; + pgbackrest = import ./pgbackrest { inherit runTest; }; pgbouncer = handleTest ./pgbouncer.nix { }; pghero = runTest ./pghero.nix; pgweb = runTest ./pgweb.nix; diff --git a/nixos/tests/pgbackrest/default.nix b/nixos/tests/pgbackrest/default.nix new file mode 100644 index 000000000000..72cfd6eb4b51 --- /dev/null +++ b/nixos/tests/pgbackrest/default.nix @@ -0,0 +1,4 @@ +{ runTest }: +{ + posix = runTest ./posix.nix; +} diff --git a/nixos/tests/pgbackrest/posix.nix b/nixos/tests/pgbackrest/posix.nix new file mode 100644 index 000000000000..d5bfaa6d310c --- /dev/null +++ b/nixos/tests/pgbackrest/posix.nix @@ -0,0 +1,147 @@ +{ lib, pkgs, ... }: +let + inherit (import ../ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + backupPath = "/var/lib/pgbackrest"; +in +{ + name = "pgbackrest-posix"; + + meta = { + maintainers = with lib.maintainers; [ wolfgangwalther ]; + }; + + nodes.primary = + { + pkgs, + ... + }: + { + services.openssh.enable = true; + users.users.postgres.openssh.authorizedKeys.keys = [ + snakeOilPublicKey + ]; + + services.postgresql = { + enable = true; + initialScript = pkgs.writeText "init.sql" '' + CREATE TABLE t(c text); + INSERT INTO t VALUES ('hello world'); + ''; + }; + + services.pgbackrest = { + enable = true; + repos.backup = { + type = "posix"; + path = backupPath; + host-user = "pgbackrest"; + }; + }; + }; + + nodes.backup = + { + nodes, + ... + }: + { + services.openssh.enable = true; + users.users.pgbackrest.openssh.authorizedKeys.keys = [ + snakeOilPublicKey + ]; + + services.pgbackrest = { + enable = true; + repos.localhost.path = backupPath; + + stanzas.default = { + jobs.future = { + schedule = "3000-01-01"; + type = "full"; + }; + instances.primary = { + path = nodes.primary.services.postgresql.dataDir; + user = "postgres"; + }; + }; + + # Examples from https://pgbackrest.org/configuration.html#introduction + # Not used for the test, except for dumping the config. + stanzas.config-format.settings = { + start-fast = true; + compress-level = 3; + buffer-size = "2MiB"; + db-timeout = 600; + db-exclude = [ + "db1" + "db2" + "db5" + ]; + tablespace-map = { + ts_01 = "/db/ts_01"; + ts_02 = "/db/ts_02"; + }; + }; + }; + }; + + testScript = + { nodes, ... }: + '' + start_all() + + primary.wait_for_unit("multi-user.target") + backup.wait_for_unit("multi-user.target") + + with subtest("config file is written correctly"): + from textwrap import dedent + have = backup.succeed("cat /etc/pgbackrest/pgbackrest.conf") + want = dedent("""\ + [config-format] + buffer-size=2MiB + compress-level=3 + db-exclude=db1 + db-exclude=db2 + db-exclude=db5 + db-timeout=600 + start-fast=y + tablespace-map=ts_01=/db/ts_01 + tablespace-map=ts_02=/db/ts_02 + """) + assert want in have, repr((want, have)) + + primary.log(primary.succeed(""" + HOME="${nodes.primary.services.postgresql.dataDir}" + mkdir -m 700 -p ~/.ssh + cat ${snakeOilPrivateKey} > ~/.ssh/id_ecdsa + chmod 400 ~/.ssh/id_ecdsa + ssh-keyscan backup >> ~/.ssh/known_hosts + chown -R postgres:postgres ~/.ssh + """)) + + backup.log(backup.succeed(""" + HOME="${backupPath}" + mkdir -m 700 -p ~/.ssh + cat ${snakeOilPrivateKey} > ~/.ssh/id_ecdsa + chmod 400 ~/.ssh/id_ecdsa + ssh-keyscan primary >> ~/.ssh/known_hosts + chown -R pgbackrest:pgbackrest ~ + """)) + + with subtest("backup/restore works with remote instance/local repo (SSH)"): + backup.succeed("sudo -u pgbackrest pgbackrest --stanza=default stanza-create") + backup.succeed("sudo -u pgbackrest pgbackrest --stanza=default check") + + backup.systemctl("start pgbackrest-default-future") + + # corrupt cluster + primary.systemctl("stop postgresql") + primary.execute("rm ${nodes.primary.services.postgresql.dataDir}/global/pg_control") + + primary.succeed("sudo -u postgres pgbackrest --stanza=default restore --delta") + + primary.systemctl("start postgresql") + primary.wait_for_unit("postgresql.service") + assert "hello world" in primary.succeed("sudo -u postgres psql -c 'TABLE t;'") + ''; +} diff --git a/pkgs/by-name/pg/pgbackrest/package.nix b/pkgs/by-name/pg/pgbackrest/package.nix index da6634889d89..7f0d3be5d5e0 100644 --- a/pkgs/by-name/pg/pgbackrest/package.nix +++ b/pkgs/by-name/pg/pgbackrest/package.nix @@ -15,6 +15,7 @@ stdenv, zlib, zstd, + nixosTests, }: stdenv.mkDerivation (finalAttrs: { @@ -49,6 +50,8 @@ stdenv.mkDerivation (finalAttrs: { zstd ]; + passthru.tests = nixosTests.pgbackrest; + meta = { description = "Reliable PostgreSQL backup & restore"; homepage = "https://pgbackrest.org"; From 82e519947fe2df5123093884f794e5608133b8cd Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 5 May 2025 16:27:39 +0200 Subject: [PATCH 3/4] nixos/pgbackrest: add SFTP support --- nixos/modules/services/backup/pgbackrest.nix | 21 +++++ nixos/tests/pgbackrest/default.nix | 1 + nixos/tests/pgbackrest/sftp.nix | 95 ++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 nixos/tests/pgbackrest/sftp.nix diff --git a/nixos/modules/services/backup/pgbackrest.nix b/nixos/modules/services/backup/pgbackrest.nix index da3c719c217a..f361b68cf71a 100644 --- a/nixos/modules/services/backup/pgbackrest.nix +++ b/nixos/modules/services/backup/pgbackrest.nix @@ -100,6 +100,7 @@ in if name == "localhost" then null # "posix" is the default repo type, which uses the -host option. + # Other types use prefixed options, for example -sftp-host. else if config.type or "posix" != type then null else @@ -114,6 +115,26 @@ in defaultText = lib.literalExpression "name"; description = "Repository host when operating remotely"; }; + + options.sftp-host = lib.mkOption { + type = nullOr str; + default = setHostForType "sftp"; + defaultText = lib.literalExpression "name"; + description = "SFTP repository host"; + }; + + options.sftp-private-key-file = lib.mkOption { + type = nullOr (pathWith { + inStore = false; + absolute = true; + }); + default = null; + description = '' + SFTP private key file. + + The file must be accessible by both the pgbackrest and the postgres users. + ''; + }; } ) ); diff --git a/nixos/tests/pgbackrest/default.nix b/nixos/tests/pgbackrest/default.nix index 72cfd6eb4b51..5f837e5c351c 100644 --- a/nixos/tests/pgbackrest/default.nix +++ b/nixos/tests/pgbackrest/default.nix @@ -1,4 +1,5 @@ { runTest }: { posix = runTest ./posix.nix; + sftp = runTest ./sftp.nix; } diff --git a/nixos/tests/pgbackrest/sftp.nix b/nixos/tests/pgbackrest/sftp.nix new file mode 100644 index 000000000000..8e97fb679980 --- /dev/null +++ b/nixos/tests/pgbackrest/sftp.nix @@ -0,0 +1,95 @@ +{ lib, pkgs, ... }: +let + inherit (import ../ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey; + backupPath = "/home/backup"; +in +{ + name = "pgbackrest-sftp"; + + meta = { + maintainers = with lib.maintainers; [ wolfgangwalther ]; + }; + + nodes.primary = + { + pkgs, + ... + }: + { + services.postgresql = { + enable = true; + initialScript = pkgs.writeText "init.sql" '' + CREATE TABLE t(c text); + INSERT INTO t VALUES ('hello world'); + ''; + }; + + services.pgbackrest = { + enable = true; + repos.backup = { + type = "sftp"; + path = "/home/backup"; + sftp-host-key-check-type = "none"; + sftp-host-key-hash-type = "sha256"; + sftp-host-user = "backup"; + sftp-private-key-file = "/var/lib/pgbackrest/sftp_key"; + }; + + stanzas.default.jobs.future = { + schedule = "3000-01-01"; + type = "diff"; + }; + }; + }; + + nodes.backup = + { + nodes, + ... + }: + { + services.openssh.enable = true; + users.users.backup = { + name = "backup"; + group = "backup"; + isNormalUser = true; + createHome = true; + openssh.authorizedKeys.keys = [ + snakeOilPublicKey + ]; + }; + users.groups.backup = { }; + }; + + testScript = + { nodes, ... }: + '' + start_all() + + primary.wait_for_unit("multi-user.target") + backup.wait_for_unit("multi-user.target") + + primary.log(primary.succeed(""" + HOME="/var/lib/pgbackrest" + cat ${snakeOilPrivateKey} > ~/sftp_key + chown -R pgbackrest:pgbackrest ~/sftp_key + chmod 770 ~ + """)) + + with subtest("backup/restore works with local instance/remote repo (SFTP)"): + primary.succeed("sudo -u pgbackrest pgbackrest --stanza=default stanza-create", timeout=10) + primary.succeed("sudo -u pgbackrest pgbackrest --stanza=default check") + + primary.systemctl("start pgbackrest-default-future") + + # corrupt cluster + primary.systemctl("stop postgresql") + primary.execute("rm ${nodes.primary.services.postgresql.dataDir}/global/pg_control") + + primary.succeed("sudo -u postgres pgbackrest --stanza=default restore --delta") + + primary.systemctl("start postgresql") + primary.wait_for_unit("postgresql.service") + assert "hello world" in primary.succeed("sudo -u postgres psql -c 'TABLE t;'") + ''; +} From 9d78475ae9a9162d6ae03ad9082e9f4e435f2689 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 12 May 2025 09:03:49 +0200 Subject: [PATCH 4/4] nixos/pgbackrest: prevent storing secrets in store While we're not (fully) supporting the related features, yet, we already prevent accidental storage of secrets in the store. This will also avoid breaking changes in the future, when those features are properly supported. --- nixos/modules/services/backup/pgbackrest.nix | 56 +++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/nixos/modules/services/backup/pgbackrest.nix b/nixos/modules/services/backup/pgbackrest.nix index f361b68cf71a..1e1377818097 100644 --- a/nixos/modules/services/backup/pgbackrest.nix +++ b/nixos/modules/services/backup/pgbackrest.nix @@ -71,6 +71,23 @@ let ) cfg.stanzas ) ); + + disabledOption = lib.mkOption { + default = null; + readOnly = true; + internal = true; + }; + + secretPathOption = + with lib.types; + lib.mkOption { + type = nullOr (pathWith { + inStore = false; + absolute = true; + }); + default = null; + internal = true; + }; in { @@ -135,6 +152,22 @@ in The file must be accessible by both the pgbackrest and the postgres users. ''; }; + + # The following options should not be used; they would store secrets in the store. + options.azure-key = disabledOption; + options.cipher-pass = disabledOption; + options.s3-key = disabledOption; + options.s3-key-secret = disabledOption; + options.s3-kms-key-id = disabledOption; # unsure whether that's a secret or not + options.s3-sse-customer-key = disabledOption; # unsure whether that's a secret or not + options.s3-token = disabledOption; + options.sftp-private-key-passphrase = disabledOption; + + # The following options are not fully supported / tested, yet, but point to files with secrets. + # Users can already set those options, but we'll force non-store paths. + options.gcs-key = secretPathOption; + options.host-cert-file = secretPathOption; + options.host-key-file = secretPathOption; } ) ); @@ -211,6 +244,11 @@ in defaultText = lib.literalExpression ''if name == "localhost" then null else name''; description = "PostgreSQL host for operating remotely."; }; + + # The following options are not fully supported / tested, yet, but point to files with secrets. + # Users can already set those options, but we'll force non-store paths. + options.host-cert-file = secretPathOption; + options.host-key-file = secretPathOption; } ) ); @@ -236,7 +274,14 @@ in }; settings = lib.mkOption { - type = settingsType; + type = lib.types.submodule { + freeformType = settingsType; + + # The following options are not fully supported / tested, yet, but point to files with secrets. + # Users can already set those options, but we'll force non-store paths. + options.tls-server-cert-file = secretPathOption; + options.tls-server-key-file = secretPathOption; + }; default = { }; description = '' An attribute set of options as described in: @@ -262,7 +307,14 @@ in }; settings = lib.mkOption { - type = settingsType; + type = lib.types.submodule { + freeformType = settingsType; + + # The following options are not fully supported / tested, yet, but point to files with secrets. + # Users can already set those options, but we'll force non-store paths. + options.tls-server-cert-file = secretPathOption; + options.tls-server-key-file = secretPathOption; + }; default = { }; description = '' An attribute set of options as described in: