From f30b1bac192e2dc252107ac8a59a03ad25e1b96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 13 Sep 2024 11:13:15 +0200 Subject: [PATCH 01/53] ci(Mergify): configuration update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörg Thalheim --- .mergify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mergify.yml b/.mergify.yml index dbe75ec..0144107 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,7 +1,7 @@ queue_rules: - name: default merge_conditions: - - check-success=buildbot/nix-eval + - check-success=buildbot/nix-build defaults: actions: queue: From e2d404a7ea599a013189aa42947f66cede0645c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:10:47 +0000 Subject: [PATCH 02/53] build(deps): bump cachix/install-nix-action from V27 to 28 (#623) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from V27 to 28. This release includes the previously tagged commit. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/V27...V28) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .github/workflows/update-vendor-hash.yml | 2 +- .github/workflows/upgrade-flakes.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17b3668..3da2016 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@V27 + - uses: cachix/install-nix-action@V28 - name: Add keys group (needed for go tests) run: sudo groupadd keys - name: Run unit tests diff --git a/.github/workflows/update-vendor-hash.yml b/.github/workflows/update-vendor-hash.yml index fa351c2..6d13a8b 100644 --- a/.github/workflows/update-vendor-hash.yml +++ b/.github/workflows/update-vendor-hash.yml @@ -14,7 +14,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Install Nix - uses: cachix/install-nix-action@V27 + uses: cachix/install-nix-action@V28 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/upgrade-flakes.yml b/.github/workflows/upgrade-flakes.yml index 3aa7dd5..52c0df8 100644 --- a/.github/workflows/upgrade-flakes.yml +++ b/.github/workflows/upgrade-flakes.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@V27 + uses: cachix/install-nix-action@V28 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} From 5876a12ff688a72a62b553749146612a1d6c12d1 Mon Sep 17 00:00:00 2001 From: "A. Manzer" <33374297+oberon227@users.noreply.github.com> Date: Thu, 26 Sep 2024 22:46:07 -0400 Subject: [PATCH 03/53] Allow sops-nix to be restarted when systemd is degraded If Systemd is running, but with even a single failed unit, it'll enter Degraded state. Restart sops-nix anyway. --- modules/home-manager/sops.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index d62ff58..3520bb9 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -278,7 +278,7 @@ in { linux = let systemctl = config.systemd.user.systemctlPath; in '' systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) - if [[ $systemdStatus == 'running' ]]; then + if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then ${systemctl} restart --user sops-nix else echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed." From 3176c111120f05b78ff3bb3375ca9f04baa07c4d Mon Sep 17 00:00:00 2001 From: Aadniz <8147434+Aadniz@users.noreply.github.com> Date: Sat, 21 Sep 2024 22:02:39 +0200 Subject: [PATCH 04/53] Minor fix for binary example in README.md `sops -e krb5.keytab` does not encrypt the file, rather it outputs the encrypted data in the console. `>` should be used here to send the data to the file. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 44242ef..291447c 100644 --- a/README.md +++ b/README.md @@ -674,8 +674,7 @@ JSON/YAML files. Unlike the other two formats, for binary files, one file corres To encrypt an binary file use the following command: ``` console -$ cp /etc/krb5/krb5.keytab krb5.keytab -$ sops -e krb5.keytab +$ sops -e /etc/krb5/krb5.keytab > krb5.keytab # an example of what this might result in: $ head krb5.keytab { From 127a96f49ddc377be6ba76964411bab11ae27803 Mon Sep 17 00:00:00 2001 From: Lin Yinfeng Date: Wed, 11 Sep 2024 16:19:07 +0800 Subject: [PATCH 05/53] modules/sops/templates: support systemd activation --- modules/sops/templates/default.nix | 60 +++++++++++++++++++----------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 5b6e692..892ad9b 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -6,6 +6,27 @@ let cfg = config.sops; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; users = config.users.users; + useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || + (options.services ? userborn && config.services.userborn.enable); + renderScript = '' + echo Setting up sops templates... + ${concatMapStringsSep "\n" (name: + let + tpl = config.sops.templates.${name}; + substitute = pkgs.writers.writePython3 "substitute" { } + (readFile ./subs.py); + subst-pairs = pkgs.writeText "pairs" (concatMapStringsSep "\n" + (name: + "${toString config.sops.placeholder.${name}} ${ + config.sops.secrets.${name}.path + }") (attrNames config.sops.secrets)); + in '' + mkdir -p "${dirOf tpl.path}" + (umask 077; ${substitute} ${tpl.file} ${subst-pairs} > ${tpl.path}) + chmod "${tpl.mode}" "${tpl.path}" + chown "${tpl.owner}:${tpl.group}" "${tpl.path}" + '') (attrNames config.sops.templates)} + ''; in { options.sops = { templates = mkOption { @@ -84,26 +105,23 @@ in { (name: _: mkDefault "") config.sops.secrets; - system.activationScripts.renderSecrets = mkIf (cfg.templates != { }) - (stringAfter ([ "setupSecrets" ] - ++ optional (secretsForUsers != { }) "setupSecretsForUsers") '' - echo Setting up sops templates... - ${concatMapStringsSep "\n" (name: - let - tpl = config.sops.templates.${name}; - substitute = pkgs.writers.writePython3 "substitute" { } - (readFile ./subs.py); - subst-pairs = pkgs.writeText "pairs" (concatMapStringsSep "\n" - (name: - "${toString config.sops.placeholder.${name}} ${ - config.sops.secrets.${name}.path - }") (attrNames config.sops.secrets)); - in '' - mkdir -p "${dirOf tpl.path}" - (umask 077; ${substitute} ${tpl.file} ${subst-pairs} > ${tpl.path}) - chmod "${tpl.mode}" "${tpl.path}" - chown "${tpl.owner}:${tpl.group}" "${tpl.path}" - '') (attrNames config.sops.templates)} - ''); + systemd.services.sops-render-secrets = let + installServices = [ "sops-install-secrets.service" ] ++ optional (secretsForUsers != { }) "sops-install-secrets-for-users.service"; + in lib.mkIf (cfg.templates != { } && useSystemdActivation) { + wantedBy = [ "sysinit.target" ]; + requires = installServices; + after = installServices; + unitConfig.DefaultDependencies = "no"; + + script = renderScript; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + }; + + system.activationScripts.renderSecrets = mkIf (cfg.templates != { } && !useSystemdActivation) + (stringAfter ([ "setupSecrets" ] ++ optional (secretsForUsers != { }) "setupSecretsForUsers") + renderScript); }); } From 3198a242e547939c5e659353551b0668ec150268 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:15:13 +0000 Subject: [PATCH 06/53] build(deps): bump cachix/install-nix-action from V28 to 29 (#628) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from V28 to 29. This release includes the previously tagged commit. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/V28...v29) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .github/workflows/update-vendor-hash.yml | 2 +- .github/workflows/upgrade-flakes.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3da2016..0810436 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@V28 + - uses: cachix/install-nix-action@v29 - name: Add keys group (needed for go tests) run: sudo groupadd keys - name: Run unit tests diff --git a/.github/workflows/update-vendor-hash.yml b/.github/workflows/update-vendor-hash.yml index 6d13a8b..fdd54ee 100644 --- a/.github/workflows/update-vendor-hash.yml +++ b/.github/workflows/update-vendor-hash.yml @@ -14,7 +14,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Install Nix - uses: cachix/install-nix-action@V28 + uses: cachix/install-nix-action@v29 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/upgrade-flakes.yml b/.github/workflows/upgrade-flakes.yml index 52c0df8..41f460f 100644 --- a/.github/workflows/upgrade-flakes.yml +++ b/.github/workflows/upgrade-flakes.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@V28 + uses: cachix/install-nix-action@v29 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} From 135e6a2ba16a43c7386acfd38719cf5df83f8af4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 6 Oct 2024 03:16:18 +0000 Subject: [PATCH 07/53] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/9bb1e7571aadf31ddb4af77fc64b2d59580f9a39?narHash=sha256-Yd0FK9SkWy%2BZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84%3D' (2024-09-05) → 'github:NixOS/nixpkgs/e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da?narHash=sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M%3D' (2024-10-05) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/dc454045f5b5d814e5862a6d057e7bb5c29edc05?narHash=sha256-vNv%2BaJUW5/YurRy1ocfvs4q/48yVESwlC/yHzjkZSP8%3D' (2024-09-08) → 'github:NixOS/nixpkgs/17ae88b569bb15590549ff478bab6494dde4a907?narHash=sha256-uogSvuAp%2B1BYtdu6UWuObjHqSbBohpyARXDWqgI12Ss%3D' (2024-10-05) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 7200b9f..00a0a9c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1725534445, - "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=", + "lastModified": 1728093190, + "narHash": "sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39", + "rev": "e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1725762081, - "narHash": "sha256-vNv+aJUW5/YurRy1ocfvs4q/48yVESwlC/yHzjkZSP8=", + "lastModified": 1728156290, + "narHash": "sha256-uogSvuAp+1BYtdu6UWuObjHqSbBohpyARXDWqgI12Ss=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dc454045f5b5d814e5862a6d057e7bb5c29edc05", + "rev": "17ae88b569bb15590549ff478bab6494dde4a907", "type": "github" }, "original": { From 2750ed784e93e745a33fb55be7c2657adfb57c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 6 Oct 2024 17:29:14 +0200 Subject: [PATCH 08/53] nixos-tests: enable system switch again --- pkgs/sops-install-secrets/nixos-test.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 0d9b0bd..488b8a3 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -13,6 +13,7 @@ let secrets.test_key.neededForUsers = true; secrets."nested/test/file".owner = "example-user"; }; + system.switch.enable = true; users.users.example-user = lib.mkMerge [ (lib.mkIf (! config.systemd.sysusers.enable) { @@ -285,6 +286,7 @@ in { reloadUnits = [ "reload-trigger.service" ]; }; }; + system.switch.enable = true; # must run before sops sets up keys boot.initrd.postDeviceCommands = '' @@ -318,6 +320,7 @@ in { ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; }; }; + }; testScript = '' machine.wait_for_unit("multi-user.target") From 84d006846f98b2bfed3796f1ccc8e62faf0c2ae9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:14:23 +0000 Subject: [PATCH 09/53] build(deps): bump cachix/install-nix-action from 29 to 30 (#630) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 29 to 30. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v29...v30) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- .github/workflows/update-vendor-hash.yml | 2 +- .github/workflows/upgrade-flakes.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0810436..b3e026d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v29 + - uses: cachix/install-nix-action@v30 - name: Add keys group (needed for go tests) run: sudo groupadd keys - name: Run unit tests diff --git a/.github/workflows/update-vendor-hash.yml b/.github/workflows/update-vendor-hash.yml index fdd54ee..bf6b787 100644 --- a/.github/workflows/update-vendor-hash.yml +++ b/.github/workflows/update-vendor-hash.yml @@ -14,7 +14,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Install Nix - uses: cachix/install-nix-action@v29 + uses: cachix/install-nix-action@v30 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/upgrade-flakes.yml b/.github/workflows/upgrade-flakes.yml index 41f460f..6fac218 100644 --- a/.github/workflows/upgrade-flakes.yml +++ b/.github/workflows/upgrade-flakes.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@v29 + uses: cachix/install-nix-action@v30 with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} From 715dd6cbd0a59da6e8c2b84eee70652ae2e975a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:20:07 +0000 Subject: [PATCH 10/53] build(deps): bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#631) * build(deps): bump golang.org/x/crypto from 0.27.0 to 0.28.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.28.0. - [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update vendorHash --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- default.nix | 2 +- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/default.nix b/default.nix index cbff37d..886f8a4 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} -, vendorHash ? "sha256-kFDRjAqUOcTma5qLQz9YKRfP85A1Z9AXm/jThssP5wU=" +, vendorHash ? "sha256-CRqLHfj77lXJFZdYtnxE9YnCa8HxCD4Bji8uhDJY6eY=" }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; diff --git a/go.mod b/go.mod index 08c753a..e86bfbe 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/getsops/sops/v3 v3.8.1 github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 - golang.org/x/crypto v0.27.0 - golang.org/x/sys v0.25.0 + golang.org/x/crypto v0.28.0 + golang.org/x/sys v0.26.0 gopkg.in/ini.v1 v1.67.0 ) @@ -91,8 +91,8 @@ require ( golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.167.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 8d3aaba..b4e4c73 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -288,15 +288,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -304,8 +304,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 06535d0e3d0201e6a8080dd32dbfde339b94f01b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:01:50 +0000 Subject: [PATCH 11/53] build(deps): bump github.com/ProtonMail/go-crypto from 1.1.0-alpha.5-proton to 1.1.0-beta.0-proton (#633) * build(deps): bump github.com/ProtonMail/go-crypto Bumps [github.com/ProtonMail/go-crypto](https://github.com/ProtonMail/go-crypto) from 1.1.0-alpha.5-proton to 1.1.0-beta.0-proton. - [Release notes](https://github.com/ProtonMail/go-crypto/releases) - [Commits](https://github.com/ProtonMail/go-crypto/compare/v1.1.0-alpha.5-proton...v1.1.0-beta.0-proton) --- updated-dependencies: - dependency-name: github.com/ProtonMail/go-crypto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * update vendorHash --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- default.nix | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 886f8a4..9b5f2db 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} -, vendorHash ? "sha256-CRqLHfj77lXJFZdYtnxE9YnCa8HxCD4Bji8uhDJY6eY=" +, vendorHash ? "sha256-wd25uVUm3ISDjafy+4vImmLyObagEEeE+Ci8PbvaYD8=" }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; diff --git a/go.mod b/go.mod index e86bfbe..db3d967 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 - github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton + github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton github.com/getsops/sops/v3 v3.8.1 github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 diff --git a/go.sum b/go.sum index b4e4c73..32eb02f 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 h1:zF3WQbETL3cLvt github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0/go.mod h1:OUOla4dJLQ5FfdB07jnjawnMEqI0M3Q4WuD2W/DjhLo= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton h1:KVBEgU3CJpmzLChnLiSuEyCuhGhcMt3eOST+7A+ckto= -github.com/ProtonMail/go-crypto v1.1.0-alpha.5-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo= +github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= From c504fd7ac946d7a1b17944d73b261ca0a0b226a5 Mon Sep 17 00:00:00 2001 From: sops-nix-bot <106470913+sops-nix-bot@users.noreply.github.com> Date: Sun, 20 Oct 2024 05:29:32 +0200 Subject: [PATCH 12/53] flake.lock: Update (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da?narHash=sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M%3D' (2024-10-05) → 'github:NixOS/nixpkgs/ccc0c2126893dd20963580b6478d1a10a4512185?narHash=sha256-4HQI%2B6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo%3D' (2024-10-18) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/17ae88b569bb15590549ff478bab6494dde4a907?narHash=sha256-uogSvuAp%2B1BYtdu6UWuObjHqSbBohpyARXDWqgI12Ss%3D' (2024-10-05) → 'github:NixOS/nixpkgs/bb8c2cf7ea0dd2e18a52746b2c3a5b0c73b93c22?narHash=sha256-66RHecx%2BzohbZwJVEPF7uuwHeqf8rykZTMCTqIrOew4%3D' (2024-10-19) Co-authored-by: github-actions[bot] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 00a0a9c..9d6b8e4 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1728093190, - "narHash": "sha256-CAZF2NRuHmqTtRTNAruWpHA43Gg2UvuCNEIzabP0l6M=", + "lastModified": 1729265718, + "narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e2f08f4d8b3ecb5cf5c9fd9cb2d53bb3c71807da", + "rev": "ccc0c2126893dd20963580b6478d1a10a4512185", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1728156290, - "narHash": "sha256-uogSvuAp+1BYtdu6UWuObjHqSbBohpyARXDWqgI12Ss=", + "lastModified": 1729357638, + "narHash": "sha256-66RHecx+zohbZwJVEPF7uuwHeqf8rykZTMCTqIrOew4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "17ae88b569bb15590549ff478bab6494dde4a907", + "rev": "bb8c2cf7ea0dd2e18a52746b2c3a5b0c73b93c22", "type": "github" }, "original": { From 26642e8f193f547e72d38cd4c0c4e45b49236d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Mon, 21 Oct 2024 23:06:03 +0200 Subject: [PATCH 13/53] Add some missing literalExpression --- modules/home-manager/sops.nix | 6 +++--- modules/sops/default.nix | 2 +- modules/sops/templates/default.nix | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 3520bb9..a111af1 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -54,7 +54,7 @@ let sopsFile = lib.mkOption { type = lib.types.path; default = cfg.defaultSopsFile; - defaultText = "\${config.sops.defaultSopsFile}"; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; description = '' Sops file the secret is loaded from. ''; @@ -267,7 +267,7 @@ in { }; # [re]load secrets on home-manager activation - home.activation = let + home.activation = let darwin = let domain-target = "gui/$(id -u ${config.home.username})"; in '' @@ -286,7 +286,7 @@ in { unset systemdStatus ''; - + in { sops-nix = if pkgs.stdenv.isLinux then linux else darwin; }; diff --git a/modules/sops/default.nix b/modules/sops/default.nix index e2d3824..1a2d793 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -89,7 +89,7 @@ let }; sopsFile = lib.mkOption { type = lib.types.path; - defaultText = "\${config.sops.defaultSopsFile}"; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; description = '' Sops file the secret is loaded from. ''; diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 892ad9b..6f2bca7 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -69,7 +69,7 @@ in { group = mkOption { type = singleLineStr; default = users.${config.owner}.group; - defaultText = ''config.users.users.''${cfg.owner}.group''; + defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group''; description = '' Group of the file. ''; @@ -77,7 +77,7 @@ in { file = mkOption { type = types.path; default = pkgs.writeText config.name config.content; - defaultText = ''pkgs.writeText config.name config.content''; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; example = "./configuration-template.conf"; description = '' File used as the template. When this value is specified, `sops.templates..content` is ignored. From a4c33bfecb93458d90f9eb26f1cf695b47285243 Mon Sep 17 00:00:00 2001 From: Martijn de Munnik Date: Wed, 16 Oct 2024 01:30:11 +0200 Subject: [PATCH 14/53] Allow to set uid and gid instead of owner and group. No checks will be performed when uid and gid are set. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` sops.secrets = { sslCertificate = { sopsFile = ./secrets.yaml; owner = ""; group = ""; uid = config.containers."nginx".config.users.users."nginx".uid; gid = config.containers."nginx".config.users.groups."nginx".gid; }; sslCertificateKey = { sopsFile = ./secrets.yaml; owner = ""; group = ""; uid = config.containers."nginx".config.users.users."nginx".uid; gid = config.containers."nginx".config.users.groups."nginx".gid; }; }; ``` Co-authored-by: Jörg Thalheim --- modules/sops/default.nix | 32 ++++++++++--- modules/sops/secrets-for-users/default.nix | 2 +- pkgs/sops-install-secrets/main.go | 47 +++++++++++-------- pkgs/sops-install-secrets/main_test.go | 43 +++++++++++------- pkgs/sops-install-secrets/nixos-test.nix | 52 ++++++++++++++++++++-- 5 files changed, 132 insertions(+), 44 deletions(-) diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 1a2d793..b348070 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -73,18 +73,32 @@ let ''; }; owner = lib.mkOption { - type = lib.types.str; - default = "root"; + type = with lib.types; nullOr str; + default = null; description = '' - User of the file. + User of the file. Can only be set if uid is 0. + ''; + }; + uid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. ''; }; group = lib.mkOption { - type = lib.types.str; - default = users.${config.owner}.group; + type = with lib.types; nullOr str; + default = if config.owner != null then users.${config.owner}.group else null; defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`"; description = '' - Group of the file. + Group of the file. Can only be set if gid is 0. + ''; + }; + gid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. ''; }; sopsFile = lib.mkOption { @@ -318,6 +332,12 @@ in { builtins.isPath secret.sopsFile || (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + } { + assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; + message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; + } { + assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; + message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; }]) cfg.secrets) ); diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix index bb65532..7c316c4 100644 --- a/modules/sops/secrets-for-users/default.nix +++ b/modules/sops/secrets-for-users/default.nix @@ -43,7 +43,7 @@ in }; assertions = [{ - assertion = (lib.filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == { }; + assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; message = "neededForUsers cannot be used for secrets that are not root-owned"; } { assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 2597fc5..6f51665 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -29,8 +29,10 @@ type secret struct { Name string `json:"name"` Key string `json:"key"` Path string `json:"path"` - Owner string `json:"owner"` - Group string `json:"group"` + Owner *string `json:"owner,omitempty"` + UID int `json:"uid"` + Group *string `json:"group,omitempty"` + GID int `json:"gid"` SopsFile string `json:"sopsFile"` Format FormatType `json:"format"` Mode string `json:"mode"` @@ -475,25 +477,34 @@ func (app *appContext) validateSecret(secret *secret) error { secret.group = 0 } else if app.checkMode == Off || app.ignorePasswd { // we only access to the user/group during deployment - owner, err := user.Lookup(secret.Owner) - if err != nil { - return fmt.Errorf("failed to lookup user '%s': %w", secret.Owner, err) - } - ownerNr, err := strconv.ParseUint(owner.Uid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err) - } - secret.owner = int(ownerNr) - group, err := user.LookupGroup(secret.Group) - if err != nil { - return fmt.Errorf("failed to lookup group '%s': %w", secret.Group, err) + if secret.Owner == nil { + secret.owner = secret.UID + } else { + owner, err := user.Lookup(*secret.Owner) + if err != nil { + return fmt.Errorf("failed to lookup user '%s': %w", *secret.Owner, err) + } + uid, err := strconv.ParseUint(owner.Uid, 10, 64) + if err != nil { + return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err) + } + secret.owner = int(uid) } - groupNr, err := strconv.ParseUint(group.Gid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) + + if secret.Group == nil { + secret.group = secret.GID + } else { + group, err := user.LookupGroup(*secret.Group) + if err != nil { + return fmt.Errorf("failed to lookup group '%s': %w", *secret.Group, err) + } + gid, err := strconv.ParseUint(group.Gid, 10, 64) + if err != nil { + return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) + } + secret.group = int(gid) } - secret.group = int(groupNr) } if secret.Format == "" { diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index e001c65..8c09e68 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -98,12 +98,14 @@ func testGPG(t *testing.T) { } }() + nobody := "nobody" + nogroup := "nogroup" // should create a symlink yamlSecret := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: path.Join(testdir.path, "test-target"), Mode: "0400", @@ -112,12 +114,13 @@ func testGPG(t *testing.T) { } var jsonSecret, binarySecret, dotenvSecret, iniSecret secret + root := "root" // should not create a symlink jsonSecret = yamlSecret jsonSecret.Name = "test2" - jsonSecret.Owner = "root" + jsonSecret.Owner = &root jsonSecret.Format = "json" - jsonSecret.Group = "root" + jsonSecret.Group = &root jsonSecret.SopsFile = path.Join(assets, "secrets.json") jsonSecret.Path = path.Join(testdir.secretsPath, "test2") jsonSecret.Mode = "0700" @@ -130,16 +133,16 @@ func testGPG(t *testing.T) { dotenvSecret = yamlSecret dotenvSecret.Name = "test4" - dotenvSecret.Owner = "root" - dotenvSecret.Group = "root" + dotenvSecret.Owner = &root + dotenvSecret.Group = &root dotenvSecret.Format = "dotenv" dotenvSecret.SopsFile = path.Join(assets, "secrets.env") dotenvSecret.Path = path.Join(testdir.secretsPath, "test4") iniSecret = yamlSecret iniSecret.Name = "test5" - iniSecret.Owner = "root" - iniSecret.Group = "root" + iniSecret.Owner = &root + iniSecret.Group = &root iniSecret.Format = "ini" iniSecret.SopsFile = path.Join(assets, "secrets.ini") iniSecret.Path = path.Join(testdir.secretsPath, "test5") @@ -214,11 +217,13 @@ func testSSHKey(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -247,11 +252,13 @@ func TestAge(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -280,11 +287,13 @@ func TestAgeWithSSH(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -314,11 +323,13 @@ func TestValidateManifest(t *testing.T) { testdir := newTestDir(t) defer testdir.Remove() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: path.Join(testdir.path, "test-target"), Mode: "0400", diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 488b8a3..4f3a760 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -112,12 +112,41 @@ in { age-keys = testers.runNixOSTest { name = "sops-age-keys"; - nodes.machine = { lib, ... }: { + nodes.machine = { config, ... }: { imports = [ ../../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key = { }; + secrets = { + test_key = { }; + + test_key_someuser_somegroup = { + uid = config.users.users."someuser".uid; + gid = config.users.groups."somegroup".gid; + key = "test_key"; + }; + test_key_someuser_root = { + uid = config.users.users."someuser".uid; + key = "test_key"; + }; + test_key_root_root = { + key = "test_key"; + }; + test_key_1001_1001 = { + uid = 1001; + gid = 1001; + key = "test_key"; + }; + }; + }; + + users.users."someuser" = { + uid = 1000; + group = "somegroup"; + isNormalUser = true; + }; + users.groups."somegroup" = { + gid = 1000; }; # must run before sops sets up keys @@ -130,6 +159,22 @@ in { testScript = '' start_all() machine.succeed("cat /run/secrets/test_key | grep -q test_value") + + with subtest("test ownership"): + machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_somegroup) = '1000' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_somegroup) = '1000' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_somegroup) = 'someuser' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_somegroup) = 'somegroup' ]") + + machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_root) = '1000' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_root) = '0' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_root) = 'someuser' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_root) = 'root' ]") + + machine.succeed("[ $(stat -c%u /run/secrets/test_key_1001_1001) = '1001' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_1001_1001) = '1001' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]") ''; }; @@ -142,6 +187,7 @@ in { type = "ed25519"; path = ./test-assets/ssh-ed25519-key; }]; + sops = { defaultSopsFile = ./test-assets/secrets.yaml; secrets.test_key = { }; @@ -161,7 +207,7 @@ in { pgp-keys = testers.runNixOSTest { name = "sops-pgp-keys"; - nodes.server = { pkgs, lib, config, ... }: { + nodes.server = { lib, config, ... }: { imports = [ ../../modules/sops ]; users.users.someuser = { From d089e742fb79259b9c4dd9f18e9de1dd4fa3c1ec Mon Sep 17 00:00:00 2001 From: Mark Sisson <5761292+marksisson@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:46:27 -0500 Subject: [PATCH 15/53] feat(home-manager/sops): add environment variable configuration Added support for configuring environment variables before calling `sops-install-secrets`. Introduced a new `environment` option which allows specifying environment variables. Modified systemd service and launchd agent to use the specified environment variables. --- modules/home-manager/sops.nix | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index a111af1..850f1c7 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -96,10 +96,7 @@ let escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile; - script = toString (pkgs.writeShellScript "sops-nix-user" ((lib.optionalString (cfg.gnupg.home != null) '' - export SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg - '') - + (lib.optionalString cfg.age.generateKey '' + script = toString (pkgs.writeShellScript "sops-nix-user" ((lib.optionalString cfg.age.generateKey '' if [[ ! -f ${escapedAgeKeyFile} ]]; then echo generating machine-specific age key... ${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile}) @@ -174,6 +171,16 @@ in { description = "What to log"; }; + environment = lib.mkOption { + type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); + default = {}; + description = '' + Environment variables to set before calling sops-install-secrets. + + To properly quote strings with quotes use lib.escapeShellArg. + ''; + }; + age = { keyFile = lib.mkOption { type = lib.types.nullOr pathNotInStore; @@ -243,6 +250,8 @@ in { }]) cfg.secrets) ); + sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); + systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { Unit = { Description = "sops-nix activation"; @@ -251,6 +260,7 @@ in { Type = "oneshot"; ExecStart = script; }; + Environment = builtins.concatStringsSep " " (lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment); Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; }; @@ -259,6 +269,7 @@ in { enable = true; config = { Program = script; + EnvironmentVariables = cfg.environment; KeepAlive = false; RunAtLoad = true; StandardOutPath = "${config.home.homeDirectory}/Library/Logs/SopsNix/stdout"; From 78a0e634fc8981d6b564f08b6715c69a755c4c7d Mon Sep 17 00:00:00 2001 From: Sizhe Zhao Date: Thu, 24 Oct 2024 15:39:25 +0800 Subject: [PATCH 16/53] fix(home-manager/sops): fix setting systemd unit environment --- modules/home-manager/sops.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 850f1c7..72c8c82 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -260,7 +260,7 @@ in { Type = "oneshot"; ExecStart = script; }; - Environment = builtins.concatStringsSep " " (lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment); + Environment = cfg.environment; Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; }; From b2211d1a537136cc1d0d5c0af391e8712016b34e Mon Sep 17 00:00:00 2001 From: Sizhe Zhao Date: Fri, 25 Oct 2024 11:50:31 +0800 Subject: [PATCH 17/53] fix(home-manager/sops): fix setting unit env The Environment option should be set in Service section. --- modules/home-manager/sops.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 72c8c82..768131a 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -258,9 +258,9 @@ in { }; Service = { Type = "oneshot"; + Environment = builtins.concatStringsSep " " (lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment); ExecStart = script; }; - Environment = cfg.environment; Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; }; From 1666d16426abe79af5c47b7c0efa82fd31bf4c56 Mon Sep 17 00:00:00 2001 From: sops-nix-bot <106470913+sops-nix-bot@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:28:01 +0100 Subject: [PATCH 18/53] flake.lock: Update (#644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/ccc0c2126893dd20963580b6478d1a10a4512185?narHash=sha256-4HQI%2B6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo%3D' (2024-10-18) → 'github:NixOS/nixpkgs/4e0eec54db79d4d0909f45a88037210ff8eaffee?narHash=sha256-bpb6r3GjzhNW8l%2BmWtRtLNg5PhJIae041sPyqcFNGb4%3D' (2024-10-26) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/bb8c2cf7ea0dd2e18a52746b2c3a5b0c73b93c22?narHash=sha256-66RHecx%2BzohbZwJVEPF7uuwHeqf8rykZTMCTqIrOew4%3D' (2024-10-19) → 'github:NixOS/nixpkgs/cd3e8833d70618c4eea8df06f95b364b016d4950?narHash=sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk%3D' (2024-10-26) Co-authored-by: github-actions[bot] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 9d6b8e4..7ebd0b2 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1729265718, - "narHash": "sha256-4HQI+6LsO3kpWTYuVGIzhJs1cetFcwT7quWCk/6rqeo=", + "lastModified": 1729951556, + "narHash": "sha256-bpb6r3GjzhNW8l+mWtRtLNg5PhJIae041sPyqcFNGb4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ccc0c2126893dd20963580b6478d1a10a4512185", + "rev": "4e0eec54db79d4d0909f45a88037210ff8eaffee", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1729357638, - "narHash": "sha256-66RHecx+zohbZwJVEPF7uuwHeqf8rykZTMCTqIrOew4=", + "lastModified": 1729973466, + "narHash": "sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bb8c2cf7ea0dd2e18a52746b2c3a5b0c73b93c22", + "rev": "cd3e8833d70618c4eea8df06f95b364b016d4950", "type": "github" }, "original": { From e9b5eef9b51cdf966c76143e13a9476725b2f760 Mon Sep 17 00:00:00 2001 From: sops-nix-bot <106470913+sops-nix-bot@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:49:44 +0100 Subject: [PATCH 19/53] flake.lock: Update (#646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/4e0eec54db79d4d0909f45a88037210ff8eaffee?narHash=sha256-bpb6r3GjzhNW8l%2BmWtRtLNg5PhJIae041sPyqcFNGb4%3D' (2024-10-26) → 'github:NixOS/nixpkgs/2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53?narHash=sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z%2Bw%3D' (2024-10-30) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/cd3e8833d70618c4eea8df06f95b364b016d4950?narHash=sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk%3D' (2024-10-26) → 'github:NixOS/nixpkgs/3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c?narHash=sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY%3D' (2024-11-03) Co-authored-by: github-actions[bot] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 7ebd0b2..54e91e8 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1729951556, - "narHash": "sha256-bpb6r3GjzhNW8l+mWtRtLNg5PhJIae041sPyqcFNGb4=", + "lastModified": 1730272153, + "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4e0eec54db79d4d0909f45a88037210ff8eaffee", + "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1729973466, - "narHash": "sha256-knnVBGfTCZlQgxY1SgH0vn2OyehH9ykfF8geZgS95bk=", + "lastModified": 1730602179, + "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cd3e8833d70618c4eea8df06f95b364b016d4950", + "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", "type": "github" }, "original": { From 59d6988329626132eaf107761643f55eb979eef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Thu, 31 Oct 2024 02:02:43 +0100 Subject: [PATCH 20/53] Fix module declarations --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 287ac86..f3fa146 100644 --- a/flake.nix +++ b/flake.nix @@ -27,10 +27,10 @@ inherit (prev) ssh-to-pgp; }; nixosModules = { - sops = import ./modules/sops; + sops = ./modules/sops; default = self.nixosModules.sops; }; - homeManagerModules.sops = import ./modules/home-manager/sops.nix; + homeManagerModules.sops = ./modules/home-manager/sops.nix; homeManagerModule = self.homeManagerModules.sops; packages = forAllSystems (system: import ./default.nix { From bb7d6362119b792aeebe1926da1b5ebe37fed0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 18 Mar 2024 13:42:31 +0100 Subject: [PATCH 21/53] template refactoring --- modules/sops/manifest-for.nix | 1 + modules/sops/templates/default.nix | 4 +- pkgs/sops-install-secrets/main.go | 197 ++++++++++++++++++++++++++--- 3 files changed, 180 insertions(+), 22 deletions(-) diff --git a/modules/sops/manifest-for.nix b/modules/sops/manifest-for.nix index 0752909..508ce8b 100644 --- a/modules/sops/manifest-for.nix +++ b/modules/sops/manifest-for.nix @@ -15,6 +15,7 @@ writeTextFile { ageKeyFile = cfg.age.keyFile; ageSshKeyPaths = cfg.age.sshKeyPaths; useTmpfs = cfg.useTmpfs; + templates = cfg.templates; userMode = false; logging = { keyImport = builtins.elem "keyImport" cfg.log; diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 6f2bca7..a8e938f 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -37,13 +37,13 @@ in { type = singleLineStr; default = config._module.args.name; description = '' - Name of the file used in /run/secrets-rendered + Name of the file used in /run/secrets/rendered ''; }; path = mkOption { description = "Path where the rendered file will be placed"; type = singleLineStr; - default = "/run/secrets-rendered/${config.name}"; + default = "/run/secrets/rendered/${config.name}"; }; content = mkOption { type = lines; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 6f51665..44252be 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -11,6 +11,7 @@ import ( "os/user" "path" "path/filepath" + "regexp" "strconv" "strings" "syscall" @@ -49,8 +50,23 @@ type loggingConfig struct { SecretChanges bool `json:"secretChanges"` } +type template struct { + Name string `json:"name"` + Content string `json:"content"` + Path string `json:"path"` + Mode string `json:"mode"` + Owner string `json:"owner"` + Group string `json:"group"` + File string `json:"file"` + value []byte + mode os.FileMode + owner int + group int +} + type manifest struct { Secrets []secret `json:"secrets"` + Templates []template `json:"templates"` SecretsMountPoint string `json:"secretsMountPoint"` SymlinkPath string `json:"symlinkPath"` KeepGenerations int `json:"keepGenerations"` @@ -130,6 +146,7 @@ type options struct { type appContext struct { manifest manifest secretFiles map[string]secretFile + placeholder map[string]secret checkMode CheckMode ignorePasswd bool } @@ -346,24 +363,30 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us return &dir, nil } -func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error { +func createParentDirs(parent string, target string, keysGid int, userMode bool) error { + dirs := strings.Split(filepath.Dir(target), "/") + pathSoFar := parent + for _, dir := range dirs { + pathSoFar = filepath.Join(pathSoFar, dir) + if err := os.MkdirAll(pathSoFar, 0o751); err != nil { + return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) + } + if !userMode { + if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil { + return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) + } + } + } + return nil +} + +func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool) error { for _, secret := range secrets { fp := filepath.Join(secretDir, secret.Name) - dirs := strings.Split(filepath.Dir(secret.Name), "/") - pathSoFar := secretDir - for _, dir := range dirs { - pathSoFar = filepath.Join(pathSoFar, dir) - if err := os.MkdirAll(pathSoFar, 0o751); err != nil { - return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, fp, err) - } - if !userMode { - if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil { - return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, fp, err) - } - } + if err := createParentDirs(secretDir, secret.Name, keysGid, userMode); err != nil { + return err } - if err := os.WriteFile(fp, []byte(secret.value), secret.mode); err != nil { return fmt.Errorf("cannot write %s: %w", fp, err) } @@ -465,18 +488,55 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { return nil } -func (app *appContext) validateSecret(secret *secret) error { - mode, err := strconv.ParseUint(secret.Mode, 8, 16) +func validateMode(mode string) (os.FileMode, error) { + parsed, err := strconv.ParseUint(mode, 8, 16) if err != nil { - return fmt.Errorf("invalid number in mode: %d: %w", mode, err) + return 0, fmt.Errorf("Invalid number in mode: %d: %w", mode, err) } - secret.mode = os.FileMode(mode) + return os.FileMode(parsed), nil +} + +func validateOwner(owner string) (int, error) { + lookedUp, err := user.Lookup(owner) + if err != nil { + return 0, fmt.Errorf("Failed to lookup user '%s': %w", owner, err) + } + ownerNr, err := strconv.ParseUint(lookedUp.Uid, 10, 64) + if err != nil { + return 0, fmt.Errorf("Cannot parse uid %s: %w", lookedUp.Uid, err) + } + return int(ownerNr), nil +} + +func validateGroup(group string) (int, error) { + lookedUp, err := user.LookupGroup(group) + if err != nil { + return 0, fmt.Errorf("Failed to lookup group '%s': %w", group, err) + } + groupNr, err := strconv.ParseUint(lookedUp.Gid, 10, 64) + if err != nil { + return 0, fmt.Errorf("Cannot parse gid %s: %w", lookedUp.Gid, err) + } + return int(groupNr), nil +} + +func (app *appContext) validateSecret(secret *secret) error { + mode, err := validateMode(secret.Mode) + if err != nil { + return err + } + secret.mode = mode if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { secret.owner = 0 secret.group = 0 } else if app.checkMode == Off || app.ignorePasswd { // we only access to the user/group during deployment + owner, err := validateOwner(*secret.Owner) + if err != nil { + return err + } + secret.owner = owner if secret.Owner == nil { secret.owner = secret.UID @@ -528,6 +588,73 @@ func (app *appContext) validateSecret(secret *secret) error { return app.validateSopsFile(secret, &file) } +var PLACEHOLDER = regexp.MustCompile(``) + +func renderTemplate(content *string, secrets []secret) ([]byte, error) { + secretMap := make(map[string][]byte) + var err error = nil + replaced := PLACEHOLDER.ReplaceAllStringFunc(*content, func(match string) string { + secretName := PLACEHOLDER.FindStringSubmatch(match)[1] + for _, secret := range secrets { + if secret.Name == secretName { + secretMap[secretName] = secret.value + return string(secret.value) + } + } + return match + }) + if err != nil { + return nil, err + } + return []byte{}, nil +} + +func (app *appContext) validateTemplate(template *template) error { + mode, err := validateMode(template.Mode) + if err != nil { + return err + } + template.mode = mode + + if app.ignorePasswd || os.Getenv("NIXOS_ACTION") == "dry-activate" { + template.owner = 0 + template.group = 0 + } else if app.checkMode == Off || app.ignorePasswd { + // we only access to the user/group during deployment + owner, err := validateOwner(template.Owner) + if err != nil { + return err + } + template.owner = owner + + group, err := validateGroup(template.Group) + if err != nil { + return err + } + template.group = group + } + + var templateText string + if template.Content != "" { + templateText = template.Content + } else if template.File != "" { + templateBytes, err := os.ReadFile(template.File) + if err != nil { + return fmt.Errorf("Cannot read %s: %w", template.File, err) + } + templateText = string(templateBytes) + } else { + return fmt.Errorf("Neither content nor file was specified for template %s", template.Name) + } + rendered, err := renderTemplate(&templateText, app.manifest.Secrets) + if err != nil { + return fmt.Errorf("Failed to render template %s: %w", template.Name, err) + } + template.value = rendered + + return nil +} + func (app *appContext) validateManifest() error { m := &app.manifest if m.GnupgHome != "" { @@ -541,8 +668,15 @@ func (app *appContext) validateManifest() error { } } - for i := range m.Secrets { - if err := app.validateSecret(&m.Secrets[i]); err != nil { + for _, secret := range m.Secrets { + if err := app.validateSecret(&secret); err != nil { + return err + } + } + if len(m.Templates) > 0 { + } + for _, template := range m.Templates { + if err := app.validateTemplate(&template); err != nil { return err } } @@ -926,6 +1060,24 @@ func replaceRuntimeDir(path, rundir string) (ret string) { return } +func writeTemplates(targetDir string, templates []template, keysGid int, userMode bool) error { + for _, template := range templates { + fp := filepath.Join(targetDir, template.Name) + + createParentDirs(targetDir, template.Name, keysGid, userMode) + + if err := os.WriteFile(fp, []byte(template.value), template.mode); err != nil { + return fmt.Errorf("Cannot write %s: %w", fp, err) + } + if !userMode { + if err := os.Chown(fp, template.owner, template.group); err != nil { + return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err) + } + } + } + return nil +} + func installSecrets(args []string) error { opts, err := parseFlags(args) if err != nil { @@ -1042,6 +1194,11 @@ func installSecrets(args []string) error { if err := writeSecrets(*secretDir, manifest.Secrets, keysGID, manifest.UserMode); err != nil { return fmt.Errorf("cannot write secrets: %w", err) } + + if err := writeTemplates(path.Join(*secretDir, "rendered"), manifest.Templates, keysGid, manifest.UserMode); err != nil { + return fmt.Errorf("Cannot render templates: %w", err) + } + if !manifest.UserMode { if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil { return fmt.Errorf("cannot request units to restart: %w", err) From aa5caa129bd96b7883fa5164dae7d91279ecb9d2 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Tue, 5 Nov 2024 11:46:26 -0600 Subject: [PATCH 22/53] rebase, complete implementation --- modules/sops/manifest-for.nix | 1 + modules/sops/templates/default.nix | 78 +++----- modules/sops/templates/subs.py | 26 --- pkgs/sops-install-secrets/main.go | 219 +++++++++++------------ pkgs/sops-install-secrets/nixos-test.nix | 33 ++-- 5 files changed, 149 insertions(+), 208 deletions(-) delete mode 100644 modules/sops/templates/subs.py diff --git a/modules/sops/manifest-for.nix b/modules/sops/manifest-for.nix index 508ce8b..bdefaee 100644 --- a/modules/sops/manifest-for.nix +++ b/modules/sops/manifest-for.nix @@ -16,6 +16,7 @@ writeTextFile { ageSshKeyPaths = cfg.age.sshKeyPaths; useTmpfs = cfg.useTmpfs; templates = cfg.templates; + placeholderBySecretName = cfg.placeholder; userMode = false; logging = { keyImport = builtins.elem "keyImport" cfg.log; diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index a8e938f..53c8848 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -1,40 +1,21 @@ { config, pkgs, lib, options, ... }: -with lib; -with lib.types; -with builtins; let - cfg = config.sops; - secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + inherit (lib) + mkOption + mkDefault + mapAttrs + types + ; + users = config.users.users; - useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || - (options.services ? userborn && config.services.userborn.enable); - renderScript = '' - echo Setting up sops templates... - ${concatMapStringsSep "\n" (name: - let - tpl = config.sops.templates.${name}; - substitute = pkgs.writers.writePython3 "substitute" { } - (readFile ./subs.py); - subst-pairs = pkgs.writeText "pairs" (concatMapStringsSep "\n" - (name: - "${toString config.sops.placeholder.${name}} ${ - config.sops.secrets.${name}.path - }") (attrNames config.sops.secrets)); - in '' - mkdir -p "${dirOf tpl.path}" - (umask 077; ${substitute} ${tpl.file} ${subst-pairs} > ${tpl.path}) - chmod "${tpl.mode}" "${tpl.path}" - chown "${tpl.owner}:${tpl.group}" "${tpl.path}" - '') (attrNames config.sops.templates)} - ''; in { options.sops = { templates = mkOption { description = "Templates for secret files"; - type = attrsOf (submodule ({ config, ... }: { + type = types.attrsOf (types.submodule ({ config, ... }: { options = { name = mkOption { - type = singleLineStr; + type = types.singleLineStr; default = config._module.args.name; description = '' Name of the file used in /run/secrets/rendered @@ -42,32 +23,32 @@ in { }; path = mkOption { description = "Path where the rendered file will be placed"; - type = singleLineStr; + type = types.singleLineStr; default = "/run/secrets/rendered/${config.name}"; }; content = mkOption { - type = lines; + type = types.lines; default = ""; description = '' Content of the file ''; }; mode = mkOption { - type = singleLineStr; + type = types.singleLineStr; default = "0400"; description = '' Permissions mode of the rendered secret file in octal. ''; }; owner = mkOption { - type = singleLineStr; + type = types.singleLineStr; default = "root"; description = '' User of the file. ''; }; group = mkOption { - type = singleLineStr; + type = types.singleLineStr; default = users.${config.owner}.group; defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group''; description = '' @@ -88,40 +69,21 @@ in { default = { }; }; placeholder = mkOption { - type = attrsOf (mkOptionType { + type = types.attrsOf (types.mkOptionType { name = "coercibleToString"; description = "value that can be coerced to string"; - check = strings.isConvertibleWithToString; - merge = mergeEqualOption; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; }); default = { }; visible = false; }; }; - config = optionalAttrs (options ? sops.secrets) - (mkIf (config.sops.templates != { }) { + config = lib.optionalAttrs (options ? sops.secrets) + (lib.mkIf (config.sops.templates != { }) { sops.placeholder = mapAttrs - (name: _: mkDefault "") + (name: _: mkDefault "") config.sops.secrets; - - systemd.services.sops-render-secrets = let - installServices = [ "sops-install-secrets.service" ] ++ optional (secretsForUsers != { }) "sops-install-secrets-for-users.service"; - in lib.mkIf (cfg.templates != { } && useSystemdActivation) { - wantedBy = [ "sysinit.target" ]; - requires = installServices; - after = installServices; - unitConfig.DefaultDependencies = "no"; - - script = renderScript; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - }; - }; - - system.activationScripts.renderSecrets = mkIf (cfg.templates != { } && !useSystemdActivation) - (stringAfter ([ "setupSecrets" ] ++ optional (secretsForUsers != { }) "setupSecretsForUsers") - renderScript); }); } diff --git a/modules/sops/templates/subs.py b/modules/sops/templates/subs.py deleted file mode 100644 index 778b690..0000000 --- a/modules/sops/templates/subs.py +++ /dev/null @@ -1,26 +0,0 @@ -from sys import argv - - -def substitute(target: str, subst: str) -> str: - with open(target) as f: - content = f.read() - - with open(subst) as f: - subst_pairs = f.read().splitlines() - - for pair in subst_pairs: - placeholder, path = pair.split() - if placeholder in content: - with open(path) as f: - content = content.replace(placeholder, f.read()) - - return content - - -def main() -> None: - target = argv[1] - subst = argv[2] - print(substitute(target, subst)) - - -main() diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 44252be..918a219 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -11,7 +11,6 @@ import ( "os/user" "path" "path/filepath" - "regexp" "strconv" "strings" "syscall" @@ -51,32 +50,36 @@ type loggingConfig struct { } type template struct { - Name string `json:"name"` - Content string `json:"content"` - Path string `json:"path"` - Mode string `json:"mode"` - Owner string `json:"owner"` - Group string `json:"group"` - File string `json:"file"` + Name string `json:"name"` + Content string `json:"content"` + Path string `json:"path"` + Mode string `json:"mode"` + Owner *string `json:"owner,omitempty"` + UID int `json:"uid"` + Group *string `json:"group,omitempty"` + GID int `json:"gid"` + File string `json:"file"` value []byte mode os.FileMode + content string owner int group int } type manifest struct { - Secrets []secret `json:"secrets"` - Templates []template `json:"templates"` - SecretsMountPoint string `json:"secretsMountPoint"` - SymlinkPath string `json:"symlinkPath"` - KeepGenerations int `json:"keepGenerations"` - SSHKeyPaths []string `json:"sshKeyPaths"` - GnupgHome string `json:"gnupgHome"` - AgeKeyFile string `json:"ageKeyFile"` - AgeSSHKeyPaths []string `json:"ageSshKeyPaths"` - UseTmpfs bool `json:"useTmpfs"` - UserMode bool `json:"userMode"` - Logging loggingConfig `json:"logging"` + Secrets []secret `json:"secrets"` + Templates map[string]*template `json:"templates"` + PlaceholderBySecretName map[string]string `json:"placeholderBySecretName"` + SecretsMountPoint string `json:"secretsMountPoint"` + SymlinkPath string `json:"symlinkPath"` + KeepGenerations int `json:"keepGenerations"` + SSHKeyPaths []string `json:"sshKeyPaths"` + GnupgHome string `json:"gnupgHome"` + AgeKeyFile string `json:"ageKeyFile"` + AgeSSHKeyPaths []string `json:"ageSshKeyPaths"` + UseTmpfs bool `json:"useTmpfs"` + UserMode bool `json:"userMode"` + Logging loggingConfig `json:"logging"` } type secretFile struct { @@ -144,11 +147,11 @@ type options struct { } type appContext struct { - manifest manifest - secretFiles map[string]secretFile - placeholder map[string]secret - checkMode CheckMode - ignorePasswd bool + manifest manifest + secretFiles map[string]secretFile + secretByPlaceholder map[string]*secret + checkMode CheckMode + ignorePasswd bool } func readManifest(path string) (*manifest, error) { @@ -363,28 +366,28 @@ func prepareSecretsDir(secretMountpoint string, linkName string, keysGID int, us return &dir, nil } -func createParentDirs(parent string, target string, keysGid int, userMode bool) error { +func createParentDirs(parent string, target string, keysGID int, userMode bool) error { dirs := strings.Split(filepath.Dir(target), "/") pathSoFar := parent for _, dir := range dirs { pathSoFar = filepath.Join(pathSoFar, dir) if err := os.MkdirAll(pathSoFar, 0o751); err != nil { - return fmt.Errorf("Cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) + return fmt.Errorf("cannot create directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) } if !userMode { - if err := os.Chown(pathSoFar, 0, int(keysGid)); err != nil { - return fmt.Errorf("Cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) + if err := os.Chown(pathSoFar, 0, int(keysGID)); err != nil { + return fmt.Errorf("cannot own directory '%s' for %s: %w", pathSoFar, filepath.Join(parent, target), err) } } } return nil } -func writeSecrets(secretDir string, secrets []secret, keysGid int, userMode bool) error { +func writeSecrets(secretDir string, secrets []secret, keysGID int, userMode bool) error { for _, secret := range secrets { fp := filepath.Join(secretDir, secret.Name) - if err := createParentDirs(secretDir, secret.Name, keysGid, userMode); err != nil { + if err := createParentDirs(secretDir, secret.Name, keysGID, userMode); err != nil { return err } if err := os.WriteFile(fp, []byte(secret.value), secret.mode); err != nil { @@ -463,7 +466,7 @@ func (app *appContext) loadSopsFile(s *secret) (*secretFile, error) { if err != nil { return nil, fmt.Errorf("cannot parse ini of '%s': %w", s.SopsFile, err) } - // TODO: we do not acctually check the contents of the ini here... + // TODO: we do not actually check the contents of the ini here... } return &secretFile{ @@ -491,7 +494,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { func validateMode(mode string) (os.FileMode, error) { parsed, err := strconv.ParseUint(mode, 8, 16) if err != nil { - return 0, fmt.Errorf("Invalid number in mode: %d: %w", mode, err) + return 0, fmt.Errorf("invalid number in mode: %s: %w", mode, err) } return os.FileMode(parsed), nil } @@ -499,11 +502,11 @@ func validateMode(mode string) (os.FileMode, error) { func validateOwner(owner string) (int, error) { lookedUp, err := user.Lookup(owner) if err != nil { - return 0, fmt.Errorf("Failed to lookup user '%s': %w", owner, err) + return 0, fmt.Errorf("failed to lookup user '%s': %w", owner, err) } ownerNr, err := strconv.ParseUint(lookedUp.Uid, 10, 64) if err != nil { - return 0, fmt.Errorf("Cannot parse uid %s: %w", lookedUp.Uid, err) + return 0, fmt.Errorf("cannot parse uid %s: %w", lookedUp.Uid, err) } return int(ownerNr), nil } @@ -511,11 +514,11 @@ func validateOwner(owner string) (int, error) { func validateGroup(group string) (int, error) { lookedUp, err := user.LookupGroup(group) if err != nil { - return 0, fmt.Errorf("Failed to lookup group '%s': %w", group, err) + return 0, fmt.Errorf("failed to lookup group '%s': %w", group, err) } groupNr, err := strconv.ParseUint(lookedUp.Gid, 10, 64) if err != nil { - return 0, fmt.Errorf("Cannot parse gid %s: %w", lookedUp.Gid, err) + return 0, fmt.Errorf("cannot parse gid %s: %w", lookedUp.Gid, err) } return int(groupNr), nil } @@ -531,39 +534,24 @@ func (app *appContext) validateSecret(secret *secret) error { secret.owner = 0 secret.group = 0 } else if app.checkMode == Off || app.ignorePasswd { - // we only access to the user/group during deployment - owner, err := validateOwner(*secret.Owner) - if err != nil { - return err - } - secret.owner = owner - if secret.Owner == nil { secret.owner = secret.UID } else { - owner, err := user.Lookup(*secret.Owner) + owner, err := validateOwner(*secret.Owner) if err != nil { - return fmt.Errorf("failed to lookup user '%s': %w", *secret.Owner, err) + return err } - uid, err := strconv.ParseUint(owner.Uid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err) - } - secret.owner = int(uid) + secret.owner = owner } if secret.Group == nil { secret.group = secret.GID } else { - group, err := user.LookupGroup(*secret.Group) + group, err := validateGroup(*secret.Group) if err != nil { - return fmt.Errorf("failed to lookup group '%s': %w", *secret.Group, err) + return err } - gid, err := strconv.ParseUint(group.Gid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) - } - secret.group = int(gid) + secret.group = group } } @@ -582,31 +570,26 @@ func (app *appContext) validateSecret(secret *secret) error { return err } app.secretFiles[secret.SopsFile] = *maybeFile + file = *maybeFile } return app.validateSopsFile(secret, &file) } -var PLACEHOLDER = regexp.MustCompile(``) - -func renderTemplate(content *string, secrets []secret) ([]byte, error) { - secretMap := make(map[string][]byte) - var err error = nil - replaced := PLACEHOLDER.ReplaceAllStringFunc(*content, func(match string) string { - secretName := PLACEHOLDER.FindStringSubmatch(match)[1] - for _, secret := range secrets { - if secret.Name == secretName { - secretMap[secretName] = secret.value - return string(secret.value) - } - } - return match - }) - if err != nil { - return nil, err +func renderTemplates(templates map[string]*template, secretByPlaceholder map[string]*secret) { + for _, template := range templates { + rendered := renderTemplate(&template.content, secretByPlaceholder) + template.value = []byte(rendered) } - return []byte{}, nil +} + +func renderTemplate(content *string, secretByPlaceholder map[string]*secret) string { + rendered := *content + for placeholder, secret := range secretByPlaceholder { + rendered = strings.ReplaceAll(rendered, placeholder, string(secret.value)) + } + return rendered } func (app *appContext) validateTemplate(template *template) error { @@ -620,18 +603,25 @@ func (app *appContext) validateTemplate(template *template) error { template.owner = 0 template.group = 0 } else if app.checkMode == Off || app.ignorePasswd { - // we only access to the user/group during deployment - owner, err := validateOwner(template.Owner) - if err != nil { - return err + if template.Owner == nil { + template.owner = template.UID + } else { + owner, err := validateOwner(*template.Owner) + if err != nil { + return err + } + template.owner = owner } - template.owner = owner - group, err := validateGroup(template.Group) - if err != nil { - return err + if template.Group == nil { + template.group = template.GID + } else { + group, err := validateGroup(*template.Group) + if err != nil { + return err + } + template.group = group } - template.group = group } var templateText string @@ -640,17 +630,14 @@ func (app *appContext) validateTemplate(template *template) error { } else if template.File != "" { templateBytes, err := os.ReadFile(template.File) if err != nil { - return fmt.Errorf("Cannot read %s: %w", template.File, err) + return fmt.Errorf("cannot read %s: %w", template.File, err) } templateText = string(templateBytes) } else { - return fmt.Errorf("Neither content nor file was specified for template %s", template.Name) + return fmt.Errorf("neither content nor file was specified for template %s", template.Name) } - rendered, err := renderTemplate(&templateText, app.manifest.Secrets) - if err != nil { - return fmt.Errorf("Failed to render template %s: %w", template.Name, err) - } - template.value = rendered + + template.content = templateText return nil } @@ -668,15 +655,22 @@ func (app *appContext) validateManifest() error { } } - for _, secret := range m.Secrets { - if err := app.validateSecret(&secret); err != nil { + for i := range m.Secrets { + secret := &m.Secrets[i] + if err := app.validateSecret(secret); err != nil { return err } + + // The Nix module only defines placeholders for secrets if there are + // templates. + if len(m.Templates) > 0 { + placeholder := m.PlaceholderBySecretName[secret.Name] + app.secretByPlaceholder[placeholder] = secret + } } - if len(m.Templates) > 0 { - } + for _, template := range m.Templates { - if err := app.validateTemplate(&template); err != nil { + if err := app.validateTemplate(template); err != nil { return err } } @@ -1060,20 +1054,21 @@ func replaceRuntimeDir(path, rundir string) (ret string) { return } -func writeTemplates(targetDir string, templates []template, keysGid int, userMode bool) error { +func writeTemplates(targetDir string, templates map[string]*template, keysGID int, userMode bool) error { for _, template := range templates { fp := filepath.Join(targetDir, template.Name) - createParentDirs(targetDir, template.Name, keysGid, userMode) + if err := createParentDirs(targetDir, template.Name, keysGID, userMode); err != nil { + return err + } if err := os.WriteFile(fp, []byte(template.value), template.mode); err != nil { - return fmt.Errorf("Cannot write %s: %w", fp, err) + return fmt.Errorf("cannot write %s: %w", fp, err) } if !userMode { if err := os.Chown(fp, template.owner, template.group); err != nil { - return fmt.Errorf("Cannot change owner/group of '%s' to %d/%d: %w", fp, secret.owner, secret.group, err) - } - } + return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, template.owner, template.group, err) + } } } return nil } @@ -1106,10 +1101,11 @@ func installSecrets(args []string) error { } app := appContext{ - manifest: *manifest, - checkMode: opts.checkMode, - ignorePasswd: opts.ignorePasswd, - secretFiles: make(map[string]secretFile), + manifest: *manifest, + checkMode: opts.checkMode, + ignorePasswd: opts.ignorePasswd, + secretFiles: make(map[string]secretFile), + secretByPlaceholder: make(map[string]*secret), } if err = app.validateManifest(); err != nil { @@ -1183,10 +1179,13 @@ func installSecrets(args []string) error { } } - if err = decryptSecrets(manifest.Secrets); err != nil { + if err := decryptSecrets(manifest.Secrets); err != nil { return err } + // Now that the secrets are decrypted, we can render the templates. + renderTemplates(manifest.Templates, app.secretByPlaceholder) + secretDir, err := prepareSecretsDir(manifest.SecretsMountPoint, manifest.SymlinkPath, keysGID, manifest.UserMode) if err != nil { return fmt.Errorf("failed to prepare new secrets directory: %w", err) @@ -1195,8 +1194,8 @@ func installSecrets(args []string) error { return fmt.Errorf("cannot write secrets: %w", err) } - if err := writeTemplates(path.Join(*secretDir, "rendered"), manifest.Templates, keysGid, manifest.UserMode); err != nil { - return fmt.Errorf("Cannot render templates: %w", err) + if err := writeTemplates(path.Join(*secretDir, "rendered"), manifest.Templates, keysGID, manifest.UserMode); err != nil { + return fmt.Errorf("cannot render templates: %w", err) } if !manifest.UserMode { diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 4f3a760..f5b9ed6 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -241,7 +241,7 @@ in { testScript = '' def assertEqual(exp: str, act: str) -> None: if exp != act: - raise Exception(f"'{exp}' != '{act}'") + raise Exception(f"{exp!r} != {act!r}") start_all() @@ -260,7 +260,7 @@ in { templates = testers.runNixOSTest { name = "sops-templates"; - nodes.machine = { config, lib, ... }: { + nodes.machine = { config, ... }: { imports = [ ../../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; @@ -296,32 +296,37 @@ in { }; testScript = '' - start_all() - machine.succeed("[ $(stat -c%U /run/secrets-rendered/test_template) = 'someuser' ]") - machine.succeed("[ $(stat -c%G /run/secrets-rendered/test_template) = 'somegroup' ]") - machine.succeed("[ $(stat -c%U /run/secrets-rendered/test_default) = 'root' ]") - machine.succeed("[ $(stat -c%G /run/secrets-rendered/test_default) = 'root' ]") + def assertEqual(exp: str, act: str) -> None: + if exp != act: + raise Exception(f"{exp!r} != {act!r}") - expected = """ + + start_all() + machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_template) = 'someuser' ]") + machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_template) = 'somegroup' ]") + machine.succeed("[ $(stat -c%U /run/secrets/rendered/test_default) = 'root' ]") + machine.succeed("[ $(stat -c%G /run/secrets/rendered/test_default) = 'root' ]") + + expected = """\ This line is not modified. The next value will be replaced by test_value This line is also not modified. """ - rendered = machine.succeed("cat /run/secrets-rendered/test_template") + rendered = machine.succeed("cat /run/secrets/rendered/test_template") - expected_default = """ + expected_default = """\ Test value: test_value """ - rendered_default = machine.succeed("cat /run/secrets-rendered/test_default") + rendered_default = machine.succeed("cat /run/secrets/rendered/test_default") - if rendered.strip() != expected.strip() or rendered_default.strip() != expected_default.strip(): - raise Exception("Template is not rendered correctly") + assertEqual(expected, rendered) + assertEqual(expected_default, rendered_default) ''; }; restart-and-reload = testers.runNixOSTest { name = "sops-restart-and-reload"; - nodes.machine = { pkgs, lib, config, ... }: { + nodes.machine = { imports = [ ../../modules/sops ]; sops = { From f21c31dadf0a486ee5a501779e505036fb1b1bcf Mon Sep 17 00:00:00 2001 From: thomaslepoix Date: Mon, 25 Mar 2024 11:12:22 +0100 Subject: [PATCH 23/53] Emit plain file when key is empty Co-Authored-By: Slaier --- README.md | 38 +++++++++++++++++++++++++++++++ modules/home-manager/sops.nix | 13 ++++++++++- modules/sops/default.nix | 13 ++++++++++- pkgs/sops-install-secrets/main.go | 30 ++++++++++++++++-------- 4 files changed, 83 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 291447c..42d497f 100644 --- a/README.md +++ b/README.md @@ -707,6 +707,44 @@ This is how it can be included in your `configuration.nix`: } ``` +## Emit plain file for yaml and json formats + +By default, sops-nix extracts a single key from yaml and json files. If you +need the plain file instead of extracting a specific key from the input document, +you can set `key` to an empty string. + +For example, the input document `my-config.yaml` likes this: + +```yaml +my-secret1: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str] +my-secret2: ENC[AES256_GCM,data:tkyQPQODC3g=,iv:yHliT2FJ74EtnLIeeQtGbOoqVZnF0q5HiXYMJxYx6HE=,tag:EW5LV4kG4lcENaN2HIFiow==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] +... +``` + +This is how it can be included in your NixOS module: + +```nix +{ + sops.secrets.my-config = { + format = "yaml"; + sopsFile = ./my-config.yaml; + key = ""; + }; +} +``` + +Then, it will be mounted as `/run/secrets/my-config`: + +```yaml +my-secret1: hello +my-secret2: hello +``` + ## Use with home manager sops-nix also provides a home-manager module. diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 768131a..51d4738 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -15,11 +15,12 @@ let key = lib.mkOption { type = lib.types.str; - default = name; + default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name; description = '' Key used to lookup in the sops file. No tested data structures are supported right now. This option is ignored if format is binary. + "" means whole file. ''; }; @@ -131,6 +132,16 @@ in { ''; }; + defaultSopsKey = mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Default key used to lookup in all secrets. + This option is ignored if format is binary. + "" means whole file. + ''; + }; + validateSopsFiles = lib.mkOption { type = lib.types.bool; default = true; diff --git a/modules/sops/default.nix b/modules/sops/default.nix index b348070..bdd2f80 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -41,11 +41,12 @@ let }; key = lib.mkOption { type = lib.types.str; - default = config._module.args.name; + default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.name; description = '' Key used to lookup in the sops file. No tested data structures are supported right now. This option is ignored if format is binary. + "" means whole file. ''; }; path = lib.mkOption { @@ -176,6 +177,16 @@ in { ''; }; + defaultSopsKey = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Default key used to lookup in all secrets. + This option is ignored if format is binary. + "" means whole file. + ''; + }; + validateSopsFiles = lib.mkOption { type = lib.types.bool; default = true; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 918a219..fcdb264 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -294,12 +294,20 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { case Binary, Dotenv, Ini: sourceFile.binary = plain case Yaml: - if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("cannot parse yaml of '%s': %w", s.SopsFile, err) + if s.Key == "" { + sourceFile.binary = plain + } else { + if err := yaml.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse yaml of '%s': %w", s.SopsFile, err) + } } case JSON: - if err := json.Unmarshal(plain, &sourceFile.data); err != nil { - return fmt.Errorf("cannot parse json of '%s': %w", s.SopsFile, err) + if s.Key == "" { + sourceFile.binary = plain + } else { + if err := json.Unmarshal(plain, &sourceFile.data); err != nil { + return fmt.Errorf("Cannot parse json of '%s': %w", s.SopsFile, err) + } } default: return fmt.Errorf("secret of type %s in %s is not supported", s.Format, s.SopsFile) @@ -309,11 +317,15 @@ func decryptSecret(s *secret, sourceFiles map[string]plainData) error { case Binary, Dotenv, Ini: s.value = sourceFile.binary case Yaml, JSON: - strVal, err := recurseSecretKey(sourceFile.data, s.Key) - if err != nil { - return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) + if s.Key == "" { + s.value = sourceFile.binary + } else { + strVal, err := recurseSecretKey(sourceFile.data, s.Key) + if err != nil { + return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) + } + s.value = []byte(strVal) } - s.value = []byte(strVal) } sourceFiles[s.SopsFile] = sourceFile return nil @@ -482,7 +494,7 @@ func (app *appContext) validateSopsFile(s *secret, file *secretFile) error { s.Name, s.SopsFile, s.Format, file.firstSecret.Format, file.firstSecret.Name) } - if app.checkMode != Manifest && (s.Format != Binary && s.Format != Dotenv && s.Format != Ini) { + if app.checkMode != Manifest && !(s.Format == Binary || s.Format == Dotenv || s.Format == Ini) && s.Key != "" { _, err := recurseSecretKey(file.keys, s.Key) if err != nil { return fmt.Errorf("secret %s in %s is not valid: %w", s.Name, s.SopsFile, err) From c5ae1e214ff935f2d3593187a131becb289ea639 Mon Sep 17 00:00:00 2001 From: liyangau Date: Wed, 6 Nov 2024 19:31:35 +1100 Subject: [PATCH 24/53] fix missing lib in mkOption --- modules/home-manager/sops.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 51d4738..7a91abc 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -132,7 +132,7 @@ in { ''; }; - defaultSopsKey = mkOption { + defaultSopsKey = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = '' From 33f18b404eb8ebf1485256fa832cb59c63d72dfa Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 7 Nov 2024 12:06:10 -0600 Subject: [PATCH 25/53] Rework `restart-and-reload` to assert more strictly on the activation output I've reworked the test to assert on the entire output. This allows us to detect unexpected output without having to write weird "i expect this random string to *not* show up assertions", which aren't great at preventing regressions. I did have to change the code under test a little bit to make it behavior deterministically (by sorting the files it outputs). tl;dr: this demonstrates but does not fix it. I will fix it in a subsequent commit. --- pkgs/sops-install-secrets/main.go | 14 ++- pkgs/sops-install-secrets/nixos-test.nix | 113 ++++++++++++++++------- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index fcdb264..307d4d9 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -11,6 +11,7 @@ import ( "os/user" "path" "path/filepath" + "sort" "strconv" "strings" "syscall" @@ -984,12 +985,15 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } else { fmt.Printf("%s secret%s: ", regularPrefix, s) } - comma := "" - for name := range changed { - fmt.Printf("%s%s", comma, name) - comma = ", " + + // Sort the output for deterministic behavior. + keys := make([]string, 0, len(changed)) + for key := range changed { + keys = append(keys, key) } - fmt.Println() + sort.Strings(keys) + + fmt.Println(strings.Join(keys, ", ")) } } outputChanged(newSecrets, "adding", "would add") diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index f5b9ed6..2a36ffa 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -326,7 +326,7 @@ in { restart-and-reload = testers.runNixOSTest { name = "sops-restart-and-reload"; - nodes.machine = { + nodes.machine = {config, ...}: { imports = [ ../../modules/sops ]; sops = { @@ -336,6 +336,11 @@ in { restartUnits = [ "restart-unit.service" "reload-unit.service" ]; reloadUnits = [ "reload-trigger.service" ]; }; + + templates.test_template.content = '' + this is a template with + a secret: ${config.sops.placeholder.test_key} + ''; }; system.switch.enable = true; @@ -374,6 +379,19 @@ in { }; testScript = '' + def assertOutput(output, *expected_lines): + expected_lines = list(expected_lines) + + # Remove unrelated fluff that shows up in the output of `switch-to-configuration`. + prefix = "setting up /etc...\n" + if output.startswith(prefix): + output = output.removeprefix(prefix) + + actual_lines = output.splitlines(keepends=False) + + if actual_lines != expected_lines: + raise Exception(f"{actual_lines} != {expected_lines}") + machine.wait_for_unit("multi-user.target") machine.fail("test -f /restarted") machine.fail("test -f /reloaded") @@ -397,46 +415,75 @@ in { machine.succeed("test -f /reloaded") with subtest("change detection"): - machine.succeed("rm /run/secrets/test_key") - out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - if "adding secret" not in out: - raise Exception("Addition detection does not work") + machine.succeed("rm /run/secrets/test_key") + machine.succeed("rm /run/secrets/rendered/test_template") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + assertOutput( + out, + "adding secret: test_key", + ) - machine.succeed(": > /run/secrets/test_key") - out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - if "modifying secret" not in out: - raise Exception("Modification detection does not work") + machine.succeed(": > /run/secrets/test_key") + machine.succeed(": > /run/secrets/rendered/test_template") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + assertOutput( + out, + "modifying secret: test_key", + # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 + "removing secret: rendered/test_template", + ) - machine.succeed(": > /run/secrets/another_key") - out = machine.succeed("/run/current-system/bin/switch-to-configuration test") - if "removing secret" not in out: - raise Exception("Removal detection does not work") + machine.succeed(": > /run/secrets/another_key") + machine.succeed(": > /run/secrets/rendered/another_template") + out = machine.succeed("/run/current-system/bin/switch-to-configuration test") + assertOutput( + out, + # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 + "removing secrets: another_key, rendered/another_template, rendered/test_template", + ) with subtest("dry activation"): - machine.succeed("rm /run/secrets/test_key") - machine.succeed(": > /run/secrets/another_key") - out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") - if "would add secret" not in out: - raise Exception("Dry addition detection does not work") - if "would remove secret" not in out: - raise Exception("Dry removal detection does not work") + machine.succeed("rm /run/secrets/test_key") + machine.succeed("rm /run/secrets/rendered/test_template") + machine.succeed(": > /run/secrets/another_key") + machine.succeed(": > /run/secrets/rendered/another_template") + out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") + assertOutput( + out, + "would add secret: test_key", + # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 + "would remove secrets: another_key, rendered/another_template", + ) - machine.fail("test -f /run/secrets/test_key") - machine.succeed("test -f /run/secrets/another_key") + # Verify that we did not actually activate the new configuration. + machine.fail("test -f /run/secrets/test_key") + machine.fail("test -f /run/secrets/rendered/test_template") + machine.succeed("test -f /run/secrets/another_key") + machine.succeed("test -f /run/secrets/rendered/another_template") - machine.succeed("/run/current-system/bin/switch-to-configuration test") - machine.succeed("test -f /run/secrets/test_key") - machine.succeed("rm /restarted /reloaded") - machine.fail("test -f /run/secrets/another_key") + # Now actually activate and sanity check the resulting secrets. + machine.succeed("/run/current-system/bin/switch-to-configuration test") + machine.succeed("test -f /run/secrets/test_key") + machine.succeed("test -f /run/secrets/rendered/test_template") + machine.fail("test -f /run/secrets/another_key") + machine.fail("test -f /run/secrets/rendered/another_template") - machine.succeed(": > /run/secrets/test_key") - out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") - if "would modify secret" not in out: - raise Exception("Dry modification detection does not work") - machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]") + # Remove the restarted/reloaded indicators so we can confirm a + # dry-activate doesn't trigger systemd units. + machine.succeed("rm /restarted /reloaded") - machine.fail("test -f /restarted") # not done in dry mode - machine.fail("test -f /reloaded") # not done in dry mode + machine.succeed(": > /run/secrets/test_key") + out = machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate") + assertOutput( + out, + "would modify secret: test_key", + # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 + "would remove secret: rendered/test_template", + ) + machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]") + + machine.fail("test -f /restarted") # not done in dry mode + machine.fail("test -f /reloaded") # not done in dry mode ''; }; From fe63071416471abdab06caa234122932a7c4b980 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 7 Nov 2024 13:39:26 -0600 Subject: [PATCH 26/53] Improve activation messages about rendered templates This fixes https://github.com/Mic92/sops-nix/issues/652 --- modules/sops/templates/default.nix | 1 + pkgs/sops-install-secrets/main.go | 91 +++++++++++++++++++----- pkgs/sops-install-secrets/nixos-test.nix | 15 ++-- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 53c8848..3a53cb8 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -24,6 +24,7 @@ in { path = mkOption { description = "Path where the rendered file will be placed"; type = types.singleLineStr; + # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` default = "/run/secrets/rendered/${config.name}"; }; content = mkOption { diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 307d4d9..77ffa76 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -155,6 +155,9 @@ type appContext struct { ignorePasswd bool } +// Keep this in sync with `modules/sops/templates/default.nix` +const RenderedSubdir string = "rendered" + func readManifest(path string) (*manifest, error) { file, err := os.Open(path) if err != nil { @@ -873,7 +876,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) return filepath.Walk(filename, symWalkFunc) } -func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret) error { +func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates map[string]*template) error { var restart []string var reload []string @@ -881,6 +884,10 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s modifiedSecrets := make(map[string]bool) removedSecrets := make(map[string]bool) + newTemplates := make(map[string]bool) + modifiedTemplates := make(map[string]bool) + removedTemplates := make(map[string]bool) + // When the symlink path does not exist yet, we are being run in stage-2-init.sh // where switch-to-configuration is not run so the services would only be restarted // the next time switch-to-configuration is run. @@ -919,6 +926,33 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } } + // Find modified/new templates + for _, template := range templates { + oldPath := filepath.Join(symlinkPath, RenderedSubdir, template.Name) + newPath := filepath.Join(secretDir, RenderedSubdir, template.Name) + + // Read the old file + oldData, err := os.ReadFile(oldPath) + if err != nil { + if os.IsNotExist(err) { + // File did not exist before + newTemplates[template.Name] = true + continue + } + return err + } + + // Read the new file + newData, err := os.ReadFile(newPath) + if err != nil { + return err + } + + if !bytes.Equal(oldData, newData) { + modifiedTemplates[template.Name] = true + } + } + writeLines := func(list []string, file string) error { if len(list) != 0 { f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) @@ -952,7 +986,8 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s return nil } - // Find removed secrets + // Find removed secrets/templates. + symlinkRenderedPath := filepath.Join(symlinkPath, RenderedSubdir) err := symlinkWalk(symlinkPath, symlinkPath, func(path string, info os.FileInfo, err error) error { if err != nil { return err @@ -960,30 +995,49 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if info.IsDir() { return nil } - path = strings.TrimPrefix(path, symlinkPath+string(os.PathSeparator)) - for _, secret := range secrets { - if secret.Name == path { - return nil - } + + // If the path we're looking at isn't in `symlinkRenderedPath`, then + // it's a secret. + rel, err := filepath.Rel(symlinkRenderedPath, path) + if err != nil { + return err + } + isSecret := strings.HasPrefix(rel, "..") + + if isSecret { + path = strings.TrimPrefix(path, symlinkPath+string(os.PathSeparator)) + for _, secret := range secrets { + if secret.Name == path { + return nil + } + } + removedSecrets[path] = true + } else { + path = strings.TrimPrefix(path, symlinkRenderedPath+string(os.PathSeparator)) + for _, template := range templates { + if template.Name == path { + return nil + } + } + removedTemplates[path] = true } - removedSecrets[path] = true return nil }) if err != nil { return err } - // Output new/modified/removed secrets - outputChanged := func(changed map[string]bool, regularPrefix, dryPrefix string) { + // Output new/modified/removed secrets/templates + outputChanged := func(noun string, changed map[string]bool, regularPrefix, dryPrefix string) { if len(changed) > 0 { s := "" if len(changed) != 1 { s = "s" } if isDry { - fmt.Printf("%s secret%s: ", dryPrefix, s) + fmt.Printf("%s %s%s: ", dryPrefix, noun, s) } else { - fmt.Printf("%s secret%s: ", regularPrefix, s) + fmt.Printf("%s %s%s: ", regularPrefix, noun, s) } // Sort the output for deterministic behavior. @@ -996,9 +1050,12 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s fmt.Println(strings.Join(keys, ", ")) } } - outputChanged(newSecrets, "adding", "would add") - outputChanged(modifiedSecrets, "modifying", "would modify") - outputChanged(removedSecrets, "removing", "would remove") + outputChanged("secret", newSecrets, "adding", "would add") + outputChanged("secret", modifiedSecrets, "modifying", "would modify") + outputChanged("secret", removedSecrets, "removing", "would remove") + outputChanged("rendered secret", newTemplates, "adding", "would add") + outputChanged("rendered secret", modifiedTemplates, "modifying", "would modify") + outputChanged("rendered secret", removedTemplates, "removing", "would remove") return nil } @@ -1210,12 +1267,12 @@ func installSecrets(args []string) error { return fmt.Errorf("cannot write secrets: %w", err) } - if err := writeTemplates(path.Join(*secretDir, "rendered"), manifest.Templates, keysGID, manifest.UserMode); err != nil { + if err := writeTemplates(path.Join(*secretDir, RenderedSubdir), manifest.Templates, keysGID, manifest.UserMode); err != nil { return fmt.Errorf("cannot render templates: %w", err) } if !manifest.UserMode { - if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets); err != nil { + if err := handleModifications(isDry, manifest.Logging, manifest.SymlinkPath, *secretDir, manifest.Secrets, manifest.Templates); err != nil { return fmt.Errorf("cannot request units to restart: %w", err) } } diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 2a36ffa..3bbaf5b 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -421,6 +421,7 @@ in { assertOutput( out, "adding secret: test_key", + "adding rendered secret: test_template", ) machine.succeed(": > /run/secrets/test_key") @@ -429,8 +430,7 @@ in { assertOutput( out, "modifying secret: test_key", - # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 - "removing secret: rendered/test_template", + "modifying rendered secret: test_template", ) machine.succeed(": > /run/secrets/another_key") @@ -438,8 +438,8 @@ in { out = machine.succeed("/run/current-system/bin/switch-to-configuration test") assertOutput( out, - # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 - "removing secrets: another_key, rendered/another_template, rendered/test_template", + "removing secret: another_key", + "removing rendered secret: another_template", ) with subtest("dry activation"): @@ -451,8 +451,9 @@ in { assertOutput( out, "would add secret: test_key", - # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 - "would remove secrets: another_key, rendered/another_template", + "would remove secret: another_key", + "would add rendered secret: test_template", + "would remove rendered secret: another_template", ) # Verify that we did not actually activate the new configuration. @@ -477,8 +478,6 @@ in { assertOutput( out, "would modify secret: test_key", - # This is wrong. TODO: fix https://github.com/Mic92/sops-nix/issues/652 - "would remove secret: rendered/test_template", ) machine.succeed("[ $(cat /run/secrets/test_key | wc -c) = 0 ]") From c9f6b151cc3578f6517a5dbd152f0e29cc6d875f Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 7 Nov 2024 15:09:38 -0600 Subject: [PATCH 27/53] fix: create `template.path` symlink This fixes https://github.com/Mic92/sops-nix/issues/653. Note: `main.go` has been slowly accumulating shared logic between vanilla "secrets" and "templates". It feels to me like we could DRY up some of the logic in here by creating some shared "interface" that they both implement. I opted not to try to tackle that here, though. --- pkgs/sops-install-secrets/main.go | 53 +++++++++++++++--------- pkgs/sops-install-secrets/nixos-test.nix | 13 ++++-- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 77ffa76..42c9d5e 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -172,51 +172,51 @@ func readManifest(path string) (*manifest, error) { return &m, nil } -func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, secret *secret) bool { +func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, group int) bool { validUG := true if stat, ok := info.Sys().(*syscall.Stat_t); ok { - validUG = validUG && int(stat.Uid) == secret.owner - validUG = validUG && int(stat.Gid) == secret.group + validUG = validUG && int(stat.Uid) == owner + validUG = validUG && int(stat.Gid) == group } else { panic("Failed to cast fileInfo Sys() to *syscall.Stat_t. This is possibly an unsupported OS.") } return linkTarget == targetFile && validUG } -func symlinkSecret(targetFile string, secret *secret, userMode bool) error { +func symlinkSecret(targetFile string, path string, owner int, group int, userMode bool) error { for { - stat, err := os.Lstat(secret.Path) + stat, err := os.Lstat(path) if os.IsNotExist(err) { - if err = os.Symlink(targetFile, secret.Path); err != nil { - return fmt.Errorf("cannot create symlink '%s': %w", secret.Path, err) + if err = os.Symlink(targetFile, path); err != nil { + return fmt.Errorf("cannot create symlink '%s': %w", path, err) } if !userMode { - if err = SecureSymlinkChown(secret.Path, targetFile, secret.owner, secret.group); err != nil { - return fmt.Errorf("cannot chown symlink '%s': %w", secret.Path, err) + if err = SecureSymlinkChown(path, targetFile, owner, group); err != nil { + return fmt.Errorf("cannot chown symlink '%s': %w", path, err) } } return nil } else if err != nil { - return fmt.Errorf("cannot stat '%s': %w", secret.Path, err) + return fmt.Errorf("cannot stat '%s': %w", path, err) } if stat.Mode()&os.ModeSymlink == os.ModeSymlink { - linkTarget, err := os.Readlink(secret.Path) + linkTarget, err := os.Readlink(path) if os.IsNotExist(err) { continue } else if err != nil { - return fmt.Errorf("cannot read symlink '%s': %w", secret.Path, err) - } else if linksAreEqual(linkTarget, targetFile, stat, secret) { + return fmt.Errorf("cannot read symlink '%s': %w", path, err) + } else if linksAreEqual(linkTarget, targetFile, stat, owner, group) { return nil } } - if err := os.Remove(secret.Path); err != nil { - return fmt.Errorf("cannot override %s: %w", secret.Path, err) + if err := os.Remove(path); err != nil { + return fmt.Errorf("cannot override %s: %w", path, err) } } } -func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error { - for i, secret := range secrets { +func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*template, userMode bool) error { + for _, secret := range secrets { targetFile := filepath.Join(targetDir, secret.Name) if targetFile == secret.Path { continue @@ -225,10 +225,25 @@ func symlinkSecrets(targetDir string, secrets []secret, userMode bool) error { if err := os.MkdirAll(parent, os.ModePerm); err != nil { return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err) } - if err := symlinkSecret(targetFile, &secrets[i], userMode); err != nil { + if err := symlinkSecret(targetFile, secret.Path, secret.owner, secret.group, userMode); err != nil { return fmt.Errorf("failed to symlink secret '%s': %w", secret.Path, err) } } + + for _, template := range templates { + targetFile := filepath.Join(targetDir, RenderedSubdir, template.Name) + if targetFile == template.Path { + continue + } + parent := filepath.Dir(template.Path) + if err := os.MkdirAll(parent, os.ModePerm); err != nil { + return fmt.Errorf("cannot create parent directory of '%s': %w", template.Path, err) + } + if err := symlinkSecret(targetFile, template.Path, template.owner, template.group, userMode); err != nil { + return fmt.Errorf("failed to symlink template '%s': %w", template.Path, err) + } + } + return nil } @@ -1280,7 +1295,7 @@ func installSecrets(args []string) error { if isDry { return nil } - if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.UserMode); err != nil { + if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.Templates, manifest.UserMode); err != nil { return fmt.Errorf("failed to prepare symlinks to secret store: %w", err) } if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil { diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 3bbaf5b..e36bfe7 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -284,9 +284,12 @@ in { owner = "someuser"; group = "somegroup"; }; - sops.templates.test_default.content = '' - Test value: ${config.sops.placeholder.test_key} - ''; + sops.templates.test_default = { + content = '' + Test value: ${config.sops.placeholder.test_key} + ''; + path = "/etc/externally/linked"; + }; users.groups.somegroup = {}; users.users.someuser = { @@ -321,6 +324,10 @@ in { assertEqual(expected, rendered) assertEqual(expected_default, rendered_default) + + # Confirm that `test_default` was symlinked to the appropriate place. + realpath = machine.succeed("realpath /etc/externally/linked").strip() + assertEqual(realpath, "/run/secrets.d/1/rendered/test_default") ''; }; From 60e1bce1999f126e3b16ef45f89f72f0c3f8d16f Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 7 Nov 2024 15:26:43 -0600 Subject: [PATCH 28/53] Add support for `restartUnits` and `reloadUnits` for templates This fixes https://github.com/Mic92/sops-nix/issues/634 --- modules/sops/templates/default.nix | 18 ++++++++++++ pkgs/sops-install-secrets/main.go | 37 ++++++++++++++---------- pkgs/sops-install-secrets/nixos-test.nix | 28 +++++++++++++++--- 3 files changed, 64 insertions(+), 19 deletions(-) diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 3a53cb8..97952d2 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -65,6 +65,24 @@ in { File used as the template. When this value is specified, `sops.templates..content` is ignored. ''; }; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be restarted when the rendered template changes. + This works the same way as . + ''; + }; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when the rendered template changes. + This works the same way as . + ''; + }; }; })); default = { }; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 42c9d5e..391eb7d 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -51,20 +51,22 @@ type loggingConfig struct { } type template struct { - Name string `json:"name"` - Content string `json:"content"` - Path string `json:"path"` - Mode string `json:"mode"` - Owner *string `json:"owner,omitempty"` - UID int `json:"uid"` - Group *string `json:"group,omitempty"` - GID int `json:"gid"` - File string `json:"file"` - value []byte - mode os.FileMode - content string - owner int - group int + Name string `json:"name"` + Content string `json:"content"` + Path string `json:"path"` + Mode string `json:"mode"` + Owner *string `json:"owner,omitempty"` + UID int `json:"uid"` + Group *string `json:"group,omitempty"` + GID int `json:"gid"` + File string `json:"file"` + RestartUnits []string `json:"restartUnits"` + ReloadUnits []string `json:"reloadUnits"` + value []byte + mode os.FileMode + content string + owner int + group int } type manifest struct { @@ -951,6 +953,8 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s if err != nil { if os.IsNotExist(err) { // File did not exist before + restart = append(restart, template.RestartUnits...) + reload = append(reload, template.ReloadUnits...) newTemplates[template.Name] = true continue } @@ -964,6 +968,8 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s } if !bytes.Equal(oldData, newData) { + restart = append(restart, template.RestartUnits...) + reload = append(reload, template.ReloadUnits...) modifiedTemplates[template.Name] = true } } @@ -1156,7 +1162,8 @@ func writeTemplates(targetDir string, templates map[string]*template, keysGID in if !userMode { if err := os.Chown(fp, template.owner, template.group); err != nil { return fmt.Errorf("cannot change owner/group of '%s' to %d/%d: %w", fp, template.owner, template.group, err) - } } + } + } } return nil } diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index e36bfe7..765d8be 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -344,10 +344,14 @@ in { reloadUnits = [ "reload-trigger.service" ]; }; - templates.test_template.content = '' - this is a template with - a secret: ${config.sops.placeholder.test_key} - ''; + templates.test_template = { + content = '' + this is a template with + a secret: ${config.sops.placeholder.test_key} + ''; + restartUnits = [ "restart-unit.service" "reload-unit.service" ]; + reloadUnits = [ "reload-trigger.service" ]; + }; }; system.switch.enable = true; @@ -421,6 +425,22 @@ in { machine.succeed("test -f /restarted") machine.succeed("test -f /reloaded") + # Cleanup the marker files. + machine.succeed("rm /restarted /reloaded") + + # Ensure the template is changed + machine.succeed(": > /run/secrets/rendered/test_template") + + # The template is changed, now something should happen + machine.succeed("/run/current-system/bin/switch-to-configuration test") + + # Ensure something happened + machine.succeed("test -f /restarted") + machine.succeed("test -f /reloaded") + + # Cleanup the marker files. + machine.succeed("rm /restarted /reloaded") + with subtest("change detection"): machine.succeed("rm /run/secrets/test_key") machine.succeed("rm /run/secrets/rendered/test_template") From f1675e3b0e1e663a4af49be67ecbc9e749f85eb7 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Sat, 9 Nov 2024 20:32:29 -0800 Subject: [PATCH 29/53] home-manager: Add support for Split GPG on Qubes OS (#657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jörg Thalheim --- README.md | 25 +++++++++++++++++ modules/home-manager/sops.nix | 51 ++++++++++++++++++++++++++++++----- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42d497f..5d5769f 100644 --- a/README.md +++ b/README.md @@ -823,6 +823,31 @@ The secrets are decrypted in a systemd user service called `sops-nix`, so other } ``` +### Qubes Split GPG support + +If you are using Qubes with the [Split GPG](https://www.qubes-os.org/doc/split-gpg), +then you can configure sops to utilize the `qubes-gpg-client-wrapper` with the `sops.gnupg.qubes-split-gpg` options. +The example above updated looks like this: +```nix +{ + sops = { + gnupg.qubes-split-gpg = { + enable = true; + domain = "vault-gpg"; + }; + defaultSopsFile = ./secrets.yaml; + secrets.test = { + # sopsFile = ./secrets.yml.enc; # optionally define per-secret files + + # %r gets replaced with a runtime directory, use %% to specify a '%' + # sign. Runtime dir is $XDG_RUNTIME_DIR on linux and $(getconf + # DARWIN_USER_TEMP_DIR) on darwin. + path = "%r/test.txt"; + }; + }; +} +``` + ## Use with GPG instead of SSH keys If you prefer having a separate GPG key, sops-nix also comes with a helper tool, `sops-init-gpg-key`: diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 7a91abc..e1bb449 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -231,6 +231,19 @@ in { ''; }; + qubes-split-gpg = { + enable = lib.mkEnableOption "Enable support for Qubes Split GPG feature: https://www.qubes-os.org/doc/split-gpg"; + + domain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "vault-gpg"; + description = '' + It tells Qubes OS which secure Qube holds your GPG keys for isolated cryptographic operations. + ''; + }; + }; + sshKeyPaths = lib.mkOption { type = lib.types.listOf lib.types.path; default = []; @@ -244,11 +257,24 @@ in { config = lib.mkIf (cfg.secrets != {}) { assertions = [{ - assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != []; - message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; + assertion = + cfg.gnupg.home != null || + cfg.gnupg.sshKeyPaths != [] || + cfg.gnupg.qubes-split-gpg.enable == true || + cfg.age.keyFile != null || + cfg.age.sshKeyPaths != []; + message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable"; } { - assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); - message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; + assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []) && + !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true) && + !(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true); + message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set"; + } { + assertion = cfg.gnupg.qubes-split-gpg.enable == false || + (cfg.gnupg.qubes-split-gpg.enable == true && + cfg.gnupg.qubes-split-gpg.domain != null && + cfg.gnupg.qubes-split-gpg.domain != ""); + message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true"; }] ++ lib.optionals cfg.validateSopsFiles ( lib.concatLists (lib.mapAttrsToList (name: secret: [{ assertion = builtins.pathExists secret.sopsFile; @@ -256,12 +282,25 @@ in { } { assertion = builtins.isPath secret.sopsFile || - (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); + (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; }]) cfg.secrets) ); - sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); + home.sessionVariables = lib.mkIf cfg.gnupg.qubes-split-gpg.enable { + # TODO: Add this package to nixpkgs and use it from the store + # https://github.com/QubesOS/qubes-app-linux-split-gpg + SOPS_GPG_EXEC = "qubes-gpg-client-wrapper"; + }; + + sops.environment = { + SOPS_GPG_EXEC = lib.mkMerge [ + (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg")) + (lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC)) + ]; + + QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault cfg.gnupg.qubes-split-gpg.domain); + }; systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { Unit = { From 58f41afcc7d6d3682df547178cc19103dfff11e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:13:55 +0000 Subject: [PATCH 30/53] build(deps): bump golang.org/x/sys from 0.26.0 to 0.27.0 (#661) * build(deps): bump golang.org/x/sys from 0.26.0 to 0.27.0 Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.26.0 to 0.27.0. - [Commits](https://github.com/golang/sys/compare/v0.26.0...v0.27.0) --- updated-dependencies: - dependency-name: golang.org/x/sys dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update vendorHash --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- default.nix | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 9b5f2db..a451bb6 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} -, vendorHash ? "sha256-wd25uVUm3ISDjafy+4vImmLyObagEEeE+Ci8PbvaYD8=" +, vendorHash ? "sha256-HYriqTAwXpKzQGCDkXMNqb6LGU/+ToewhBrVWpPXNl4=" }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; diff --git a/go.mod b/go.mod index db3d967..33ca911 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 golang.org/x/crypto v0.28.0 - golang.org/x/sys v0.26.0 + golang.org/x/sys v0.27.0 gopkg.in/ini.v1 v1.67.0 ) diff --git a/go.sum b/go.sum index 32eb02f..d9614ea 100644 --- a/go.sum +++ b/go.sum @@ -288,8 +288,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From e4f36d56ebe16f928e65e69ff321d9fa1514231b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:28:15 +0000 Subject: [PATCH 31/53] build(deps): bump github.com/ProtonMail/go-crypto from 1.1.0-beta.0-proton to 1.1.2 (#662) * build(deps): bump github.com/ProtonMail/go-crypto Bumps [github.com/ProtonMail/go-crypto](https://github.com/ProtonMail/go-crypto) from 1.1.0-beta.0-proton to 1.1.2. - [Release notes](https://github.com/ProtonMail/go-crypto/releases) - [Commits](https://github.com/ProtonMail/go-crypto/compare/v1.1.0-beta.0-proton...v1.1.2) --- updated-dependencies: - dependency-name: github.com/ProtonMail/go-crypto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * update vendorHash --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- default.nix | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index a451bb6..ec6ae2f 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} -, vendorHash ? "sha256-HYriqTAwXpKzQGCDkXMNqb6LGU/+ToewhBrVWpPXNl4=" +, vendorHash ? "sha256-vnm0JkNIzw42KfO9IF0puzXvPSzy7AlGjm4wlJ0yEqQ=" }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; diff --git a/go.mod b/go.mod index 33ca911..e45cdca 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 - github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton + github.com/ProtonMail/go-crypto v1.1.2 github.com/getsops/sops/v3 v3.8.1 github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 diff --git a/go.sum b/go.sum index d9614ea..ba95ef9 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 h1:zF3WQbETL3cLvt github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0/go.mod h1:OUOla4dJLQ5FfdB07jnjawnMEqI0M3Q4WuD2W/DjhLo= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo= -github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= +github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= From 4c91d52db103e757fc25b58998b0576ae702d659 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:38:28 +0000 Subject: [PATCH 32/53] build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0 (#663) * build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.28.0 to 0.29.0. - [Commits](https://github.com/golang/crypto/compare/v0.28.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * update vendorHash --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- default.nix | 2 +- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/default.nix b/default.nix index ec6ae2f..11bb771 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,5 @@ { pkgs ? import {} -, vendorHash ? "sha256-vnm0JkNIzw42KfO9IF0puzXvPSzy7AlGjm4wlJ0yEqQ=" +, vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=" }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; diff --git a/go.mod b/go.mod index e45cdca..060734f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/getsops/sops/v3 v3.8.1 github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.29.0 golang.org/x/sys v0.27.0 gopkg.in/ini.v1 v1.67.0 ) @@ -90,9 +90,9 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/term v0.26.0 // indirect + golang.org/x/text v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.167.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index ba95ef9..477e937 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -270,8 +270,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -295,8 +295,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -304,8 +304,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From d2bd7f433b28db6bc7ae03d5eca43564da0af054 Mon Sep 17 00:00:00 2001 From: Ian Date: Sun, 3 Nov 2024 19:51:58 +0000 Subject: [PATCH 33/53] Implement darwin module for sops-nix --- flake.nix | 4 + modules/nix-darwin/default.nix | 339 ++++++++++++++++++ modules/nix-darwin/manifest-for.nix | 29 ++ .../nix-darwin/secrets-for-users/default.nix | 42 +++ modules/nix-darwin/templates/default.nix | 87 +++++ modules/nix-darwin/with-environment.nix | 13 + pkgs/sops-install-secrets/darwin.go | 6 - 7 files changed, 514 insertions(+), 6 deletions(-) create mode 100644 modules/nix-darwin/default.nix create mode 100644 modules/nix-darwin/manifest-for.nix create mode 100644 modules/nix-darwin/secrets-for-users/default.nix create mode 100644 modules/nix-darwin/templates/default.nix create mode 100644 modules/nix-darwin/with-environment.nix diff --git a/flake.nix b/flake.nix index f3fa146..8ebb37d 100644 --- a/flake.nix +++ b/flake.nix @@ -32,6 +32,10 @@ }; homeManagerModules.sops = ./modules/home-manager/sops.nix; homeManagerModule = self.homeManagerModules.sops; + darwinModules = { + sops = ./modules/nix-darwin; + default = self.darwinModules.sops; + }; packages = forAllSystems (system: import ./default.nix { pkgs = import nixpkgs {inherit system;}; diff --git a/modules/nix-darwin/default.nix b/modules/nix-darwin/default.nix new file mode 100644 index 0000000..50dec02 --- /dev/null +++ b/modules/nix-darwin/default.nix @@ -0,0 +1,339 @@ +{ config, options, lib, pkgs, ... }: + +let + cfg = config.sops; + sops-install-secrets = cfg.package; + manifestFor = pkgs.callPackage ./manifest-for.nix { + inherit cfg; + inherit (pkgs) writeTextFile; + }; + manifest = manifestFor "" regularSecrets {}; + + pathNotInStore = lib.mkOptionType { + name = "pathNotInStore"; + description = "path not in the Nix store"; + descriptionClass = "noun"; + check = x: !lib.path.hasStorePathPrefix (/. + x); + merge = lib.mergeEqualOption; + }; + + regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets; + + withEnvironment = import ./with-environment.nix { + inherit cfg lib; + }; + secretType = lib.types.submodule ({ config, ... }: { + config = { + sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; + sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); + }; + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets + ''; + }; + key = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = '' + Key used to lookup in the sops file. + No tested data structures are supported right now. + This option is ignored if format is binary. + ''; + }; + path = lib.mkOption { + type = lib.types.str; + default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}"; + defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise."; + description = '' + Path where secrets are symlinked to. + If the default is kept no symlink is created. + ''; + }; + format = lib.mkOption { + type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"]; + default = cfg.defaultSopsFormat; + description = '' + File format used to decrypt the sops secret. + Binary files are written to the target file as is. + ''; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "0400"; + description = '' + Permissions mode of the in octal. + ''; + }; + owner = lib.mkOption { + type = with lib.types; nullOr str; + default = "root"; + description = '' + User of the file. Can only be set if uid is 0. + ''; + }; + uid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. + ''; + }; + group = lib.mkOption { + type = with lib.types; nullOr str; + default = "staff"; + defaultText = "staff"; + description = '' + Group of the file. Can only be set if gid is 0. + ''; + }; + gid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. + ''; + }; + sopsFile = lib.mkOption { + type = lib.types.path; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; + description = '' + Sops file the secret is loaded from. + ''; + }; + sopsFileHash = lib.mkOption { + type = lib.types.str; + readOnly = true; + description = '' + Hash of the sops file. + ''; + }; + neededForUsers = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + **Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS. + This can be used to retrieve user's passwords from sops-nix. + Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. + ''; + }; + }; + }); + + darwinSSHKeys = [{ + type = "rsa"; + path = "/etc/ssh/ssh_host_rsa_key"; + } { + type = "ed25519"; + path = "/etc/ssh/ssh_host_ed25519_key"; + }]; + + escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; + # Skip ssh keys deployed with sops to avoid a catch 22 + defaultImportKeys = algo: + map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys); + + installScript = '' + ${if cfg.age.generateKey then '' + if [[ ! -f ${escapedKeyFile} ]]; then + echo generating machine-specific age key... + mkdir -p $(dirname ${escapedKeyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} + fi + '' else ""} + echo "Setting up secrets..." + ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} + ''; + +in { + options.sops = { + secrets = lib.mkOption { + type = lib.types.attrsOf secretType; + default = {}; + description = '' + Path where the latest secrets are mounted to. + ''; + }; + + defaultSopsFile = lib.mkOption { + type = lib.types.path; + description = '' + Default sops file used for all secrets. + ''; + }; + + defaultSopsFormat = lib.mkOption { + type = lib.types.str; + default = "yaml"; + description = '' + Default sops format used for all secrets. + ''; + }; + + validateSopsFiles = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Check all sops files at evaluation time. + This requires sops files to be added to the nix store. + ''; + }; + + keepGenerations = lib.mkOption { + type = lib.types.ints.unsigned; + default = 1; + description = '' + Number of secrets generations to keep. Setting this to 0 disables pruning. + ''; + }; + + log = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); + default = [ "keyImport" "secretChanges" ]; + description = "What to log"; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); + default = {}; + description = '' + Environment variables to set before calling sops-install-secrets. + + The values are placed in single quotes and not escaped any further to + allow usage of command substitutions for more flexibility. To properly quote + strings with quotes use lib.escapeShellArg. + + This will be evaluated twice when using secrets that use neededForUsers but + in a subshell each time so the environment variables don't collide. + ''; + }; + + package = lib.mkOption { + type = lib.types.package; + default = (pkgs.callPackage ../.. {}).sops-install-secrets; + defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; + description = '' + sops-install-secrets package to use. + ''; + }; + + validationPackage = lib.mkOption { + type = lib.types.package; + default = + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform + then sops-install-secrets + else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets; + defaultText = lib.literalExpression "config.sops.package"; + + description = '' + sops-install-secrets package to use when validating configuration. + + Defaults to sops.package if building natively, and a native version of sops-install-secrets if cross compiling. + ''; + }; + + age = { + keyFile = lib.mkOption { + type = lib.types.nullOr pathNotInStore; + default = null; + example = "/var/lib/sops-nix/key.txt"; + description = '' + Path to age key file used for sops decryption. + ''; + }; + + generateKey = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether or not to generate the age key. If this + option is set to false, the key must already be + present at the specified location. + ''; + }; + + sshKeyPaths = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = defaultImportKeys "ed25519"; + defaultText = lib.literalMD "The ed25519 keys from {option}`config.services.openssh.hostKeys`"; + description = '' + Paths to ssh keys added as age keys during sops description. + ''; + }; + }; + + gnupg = { + home = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/root/.gnupg"; + description = '' + Path to gnupg database directory containing the key for decrypting the sops file. + ''; + }; + + sshKeyPaths = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = defaultImportKeys "rsa"; + defaultText = lib.literalMD "The rsa keys from {option}`config.services.openssh.hostKeys`"; + description = '' + Path to ssh keys added as GPG keys during sops description. + This option must be explicitly unset if config.sops.gnupg.home is set. + ''; + }; + }; + }; + imports = [ + ./templates + ./secrets-for-users + ]; + + config = lib.mkMerge [ + (lib.mkIf (cfg.secrets != {}) { + assertions = [{ + assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != []; + message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; + } { + assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); + message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; + }] ++ lib.optionals cfg.validateSopsFiles ( + lib.concatLists (lib.mapAttrsToList (name: secret: [{ + assertion = builtins.pathExists secret.sopsFile; + message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; + } { + assertion = + builtins.isPath secret.sopsFile || + (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); + message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + } { + assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; + message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; + } { + assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; + message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; + }]) cfg.secrets) + ); + + system.build.sops-nix-manifest = manifest; + system.activationScripts = { + postActivation.text = lib.mkAfter installScript; + }; + + launchd.daemons.sops-install-secrets = { + command = installScript; + serviceConfig = { + RunAtLoad = true; + KeepAlive = false; + }; + }; + }) + + { + sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); + } + ]; +} diff --git a/modules/nix-darwin/manifest-for.nix b/modules/nix-darwin/manifest-for.nix new file mode 100644 index 0000000..6ab2ba0 --- /dev/null +++ b/modules/nix-darwin/manifest-for.nix @@ -0,0 +1,29 @@ +{ writeTextFile, cfg }: + +suffix: secrets: extraJson: + +writeTextFile { + name = "manifest${suffix}.json"; + text = builtins.toJSON ({ + secrets = builtins.attrValues secrets; + # Does this need to be configurable? + secretsMountPoint = "/run/secrets.d"; + symlinkPath = "/run/secrets"; + keepGenerations = cfg.keepGenerations; + gnupgHome = cfg.gnupg.home; + sshKeyPaths = cfg.gnupg.sshKeyPaths; + ageKeyFile = cfg.age.keyFile; + ageSshKeyPaths = cfg.age.sshKeyPaths; + useTmpfs = false; + templates = cfg.templates; + placeholderBySecretName = cfg.placeholder; + userMode = false; + logging = { + keyImport = builtins.elem "keyImport" cfg.log; + secretChanges = builtins.elem "secretChanges" cfg.log; + }; + } // extraJson); + checkPhase = '' + ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" + ''; +} diff --git a/modules/nix-darwin/secrets-for-users/default.nix b/modules/nix-darwin/secrets-for-users/default.nix new file mode 100644 index 0000000..b2c830a --- /dev/null +++ b/modules/nix-darwin/secrets-for-users/default.nix @@ -0,0 +1,42 @@ +{ lib, options, config, pkgs, ... }: +let + cfg = config.sops; + secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + manifestFor = pkgs.callPackage ../manifest-for.nix { + inherit cfg; + inherit (pkgs) writeTextFile; + }; + withEnvironment = import ../with-environment.nix { + inherit cfg lib; + }; + manifestForUsers = manifestFor "-for-users" secretsForUsers { + secretsMountPoint = "/run/secrets-for-users.d"; + symlinkPath = "/run/secrets-for-users"; + }; + + installScript = '' + echo "Setting up secrets for users" + ${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"} + ''; +in +{ + + assertions = [{ + assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; + message = "neededForUsers cannot be used for secrets that are not root-owned"; + }]; + + system.activationScripts = lib.mkIf (secretsForUsers != []) { + postActivation.text = lib.mkAfter installScript; + }; + + launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != []) { + command = installScript; + serviceConfig = { + RunAtLoad = true; + KeepAlive = false; + }; + }; + + system.build.sops-nix-users-manifest = manifestForUsers; +} diff --git a/modules/nix-darwin/templates/default.nix b/modules/nix-darwin/templates/default.nix new file mode 100644 index 0000000..2bb1e43 --- /dev/null +++ b/modules/nix-darwin/templates/default.nix @@ -0,0 +1,87 @@ +{ config, pkgs, lib, options, ... }: +let + inherit (lib) + mkOption + mkDefault + mapAttrs + types + ; +in { + options.sops = { + templates = mkOption { + description = "Templates for secret files"; + type = types.attrsOf (types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.singleLineStr; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets/rendered + ''; + }; + path = mkOption { + description = "Path where the rendered file will be placed"; + type = types.singleLineStr; + default = "/run/secrets/rendered/${config.name}"; + }; + content = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = types.singleLineStr; + default = "0400"; + description = '' + Permissions mode of the rendered secret file in octal. + ''; + }; + owner = mkOption { + type = types.singleLineStr; + default = "root"; + description = '' + User of the file. + ''; + }; + group = mkOption { + type = types.singleLineStr; + default = "staff"; + defaultText = "staff"; + description = '' + Group of the file. Default on darwin in staff. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; + example = "./configuration-template.conf"; + description = '' + File used as the template. When this value is specified, `sops.templates..content` is ignored. + ''; + }; + }; + })); + default = { }; + }; + placeholder = mkOption { + type = types.attrsOf (types.mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; + }); + default = { }; + visible = false; + }; + }; + + config = lib.optionalAttrs (options ? sops.secrets) + (lib.mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs + (name: _: mkDefault "") + config.sops.secrets; + }); +} diff --git a/modules/nix-darwin/with-environment.nix b/modules/nix-darwin/with-environment.nix new file mode 100644 index 0000000..f252441 --- /dev/null +++ b/modules/nix-darwin/with-environment.nix @@ -0,0 +1,13 @@ +{ cfg, lib }: + +sopsCall: + +if cfg.environment == {} then + sopsCall +else '' + ( + # shellcheck disable=SC2030,SC2031 + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} + ${sopsCall} + ) +'' diff --git a/pkgs/sops-install-secrets/darwin.go b/pkgs/sops-install-secrets/darwin.go index b56064c..05cbf75 100644 --- a/pkgs/sops-install-secrets/darwin.go +++ b/pkgs/sops-install-secrets/darwin.go @@ -6,7 +6,6 @@ package main import ( "errors" "fmt" - "log" "os" "os/exec" "strings" @@ -71,21 +70,16 @@ func MountSecretFs(mountpoint string, keysGID int, _useTmpfs bool, userMode bool size := mb * 1024 * 1024 / 512 // size in sectors a 512 bytes cmd := exec.Command("hdiutil", "attach", "-nomount", fmt.Sprintf("ram://%d", int(size))) out, err := cmd.Output() // /dev/diskN - log.Printf("%q\n", string(out)) diskpath := strings.TrimRight(string(out[:]), " \t\n") - log.Printf("%q\n", diskpath) - log.Printf("hdiutil attach ret %v. out: %s", err, diskpath) // format as hfs out, err = exec.Command("newfs_hfs", "-s", diskpath).Output() - log.Printf("newfs_hfs ret %v. out: %s", err, out) // "posix" mount takes `struct hfs_mount_args` which we dont have bindings for at hand. // See https://stackoverflow.com/a/49048846/4108673 // err = unix.Mount("hfs", mountpoint, unix.MNT_NOEXEC|unix.MNT_NODEV, mount_args) // Instead we call: out, err = exec.Command("mount", "-t", "hfs", "-o", "nobrowse,nodev,nosuid,-m=0751", diskpath, mountpoint).Output() - log.Printf("mount ret %v. out: %s", err, out) // There is no documented way to check for memfs mountpoint. Thus we place a file. path := mountpoint + "/sops-nix-secretfs" From 47fc1d8c72dbd69b32ecb2019b5b648da3dd20ce Mon Sep 17 00:00:00 2001 From: sops-nix-bot <106470913+sops-nix-bot@users.noreply.github.com> Date: Sun, 17 Nov 2024 04:30:39 +0100 Subject: [PATCH 34/53] flake.lock: Update (#658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53?narHash=sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z%2Bw%3D' (2024-10-30) → 'github:NixOS/nixpkgs/c69a9bffbecde46b4b939465422ddc59493d3e4d?narHash=sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk%3D' (2024-11-16) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c?narHash=sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY%3D' (2024-11-03) → 'github:NixOS/nixpkgs/e8c38b73aeb218e27163376a2d617e61a2ad9b59?narHash=sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g%3D' (2024-11-16) Co-authored-by: github-actions[bot] --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 54e91e8..01c146b 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1730272153, - "narHash": "sha256-B5WRZYsRlJgwVHIV6DvidFN7VX7Fg9uuwkRW9Ha8z+w=", + "lastModified": 1731763621, + "narHash": "sha256-ddcX4lQL0X05AYkrkV2LMFgGdRvgap7Ho8kgon3iWZk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "rev": "c69a9bffbecde46b4b939465422ddc59493d3e4d", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1730602179, - "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", + "lastModified": 1731797254, + "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", + "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", "type": "github" }, "original": { From eee831aadbc25f72fa9d214d57bddfd847e026b2 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Mon, 11 Nov 2024 00:18:56 -0600 Subject: [PATCH 35/53] Do not render templates when decrypting `neededForUsers` secrets This fixes https://github.com/Mic92/sops-nix/issues/659 In https://github.com/Mic92/sops-nix/pull/649, we started rendering templates twice: 1. When rendering `neededForUsers` secrets (if there are any `neededForUsers` secrets). 2. When decrypting "regular" secrets. This alone was weird and wrong, but didn't cause issues for people until https://github.com/Mic92/sops-nix/pull/655, which triggered https://github.com/Mic92/sops-nix/issues/659. The cause is not super obvious: 1. When rendering `neededForUsers` secrets, we'd generate templates in `/run/secrets-for-users/rendered`. 2. However, the `path` for these templates is in `/run/secrets/rendered`, which is not inside of the `/run/secrets-for-users` directory we're dealing with, so we'd generate a symlink from `/run/secrets/rendered/` to `/run/secrets-for-users/rendered/`, which required making the parent directory of the symlink (`/run/secrets/rendered/`). 3. This breaks sops-nix's assumption that `/run/secrets` either doesn't exist, or is a symlink, and you get the symptoms described in . Reproducing this in a test was straightforward: just expand our existing template test to also have a `neededForUsers` secret. Fixing this was also straightforward: don't render templates during the `neededForUsers` phase (if we want to add support for `neededForUsers` templates in the future, that would be straightforward to do, but I opted not do that here). --- modules/home-manager/sops.nix | 11 +++++----- modules/sops/default.nix | 7 +++++-- modules/sops/manifest-for.nix | 4 ++-- modules/sops/secrets-for-users/default.nix | 3 ++- pkgs/sops-install-secrets/main.go | 24 ++++++++++++---------- pkgs/sops-install-secrets/nixos-test.nix | 4 ++++ 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index e1bb449..9432c41 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -3,7 +3,7 @@ let cfg = config.sops; sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; - secretType = lib.types.submodule ({ config, name, ... }: { + secretType = lib.types.submodule ({ name, ... }: { options = { name = lib.mkOption { type = lib.types.str; @@ -71,10 +71,11 @@ let merge = lib.mergeEqualOption; }; - manifestFor = suffix: secrets: pkgs.writeTextFile { + manifestFor = suffix: secrets: templates: pkgs.writeTextFile { name = "manifest${suffix}.json"; text = builtins.toJSON { secrets = builtins.attrValues secrets; + templates = builtins.attrValues templates; secretsMountPoint = cfg.defaultSecretsMountPoint; symlinkPath = cfg.defaultSymlinkPath; keepGenerations = cfg.keepGenerations; @@ -93,11 +94,11 @@ let ''; }; - manifest = manifestFor "" cfg.secrets; + manifest = manifestFor "" cfg.secrets cfg.templates; escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile; - script = toString (pkgs.writeShellScript "sops-nix-user" ((lib.optionalString cfg.age.generateKey '' + script = toString (pkgs.writeShellScript "sops-nix-user" (lib.optionalString cfg.age.generateKey '' if [[ ! -f ${escapedAgeKeyFile} ]]; then echo generating machine-specific age key... ${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile}) @@ -106,7 +107,7 @@ let fi '' + '' ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest} - ''))); + '')); in { options.sops = { secrets = lib.mkOption { diff --git a/modules/sops/default.nix b/modules/sops/default.nix index bdd2f80..72cb94e 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -8,7 +8,7 @@ let inherit cfg; inherit (pkgs) writeTextFile; }; - manifest = manifestFor "" regularSecrets {}; + manifest = manifestFor "" regularSecrets regularTemplates {}; pathNotInStore = lib.mkOptionType { name = "pathNotInStore"; @@ -20,6 +20,9 @@ let regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets; + # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) + regularTemplates = cfg.templates; + useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || (options.services ? userborn && config.services.userborn.enable); @@ -354,7 +357,7 @@ in { sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); - # When using sysusers we no longer be started as an activation script because those are started in initrd while sysusers is started later. + # When using sysusers we no longer are started as an activation script because those are started in initrd while sysusers is started later. systemd.services.sops-install-secrets = lib.mkIf (regularSecrets != { } && useSystemdActivation) { wantedBy = [ "sysinit.target" ]; after = [ "systemd-sysusers.service" ]; diff --git a/modules/sops/manifest-for.nix b/modules/sops/manifest-for.nix index bdefaee..de62f08 100644 --- a/modules/sops/manifest-for.nix +++ b/modules/sops/manifest-for.nix @@ -1,11 +1,12 @@ { writeTextFile, cfg }: -suffix: secrets: extraJson: +suffix: secrets: templates: extraJson: writeTextFile { name = "manifest${suffix}.json"; text = builtins.toJSON ({ secrets = builtins.attrValues secrets; + templates = builtins.attrValues templates; # Does this need to be configurable? secretsMountPoint = "/run/secrets.d"; symlinkPath = "/run/secrets"; @@ -15,7 +16,6 @@ writeTextFile { ageKeyFile = cfg.age.keyFile; ageSshKeyPaths = cfg.age.sshKeyPaths; useTmpfs = cfg.useTmpfs; - templates = cfg.templates; placeholderBySecretName = cfg.placeholder; userMode = false; logging = { diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix index 7c316c4..dc49aa4 100644 --- a/modules/sops/secrets-for-users/default.nix +++ b/modules/sops/secrets-for-users/default.nix @@ -2,6 +2,7 @@ let cfg = config.sops; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. manifestFor = pkgs.callPackage ../manifest-for.nix { inherit cfg; inherit (pkgs) writeTextFile; @@ -9,7 +10,7 @@ let withEnvironment = import ../with-environment.nix { inherit cfg lib; }; - manifestForUsers = manifestFor "-for-users" secretsForUsers { + manifestForUsers = manifestFor "-for-users" secretsForUsers templatesForUsers { secretsMountPoint = "/run/secrets-for-users.d"; symlinkPath = "/run/secrets-for-users"; }; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 391eb7d..f4764a0 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -71,7 +71,7 @@ type template struct { type manifest struct { Secrets []secret `json:"secrets"` - Templates map[string]*template `json:"templates"` + Templates []template `json:"templates"` PlaceholderBySecretName map[string]string `json:"placeholderBySecretName"` SecretsMountPoint string `json:"secretsMountPoint"` SymlinkPath string `json:"symlinkPath"` @@ -185,7 +185,7 @@ func linksAreEqual(linkTarget, targetFile string, info os.FileInfo, owner int, g return linkTarget == targetFile && validUG } -func symlinkSecret(targetFile string, path string, owner int, group int, userMode bool) error { +func createSymlink(targetFile string, path string, owner int, group int, userMode bool) error { for { stat, err := os.Lstat(path) if os.IsNotExist(err) { @@ -217,7 +217,7 @@ func symlinkSecret(targetFile string, path string, owner int, group int, userMod } } -func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*template, userMode bool) error { +func symlinkSecretsAndTemplates(targetDir string, secrets []secret, templates []template, userMode bool) error { for _, secret := range secrets { targetFile := filepath.Join(targetDir, secret.Name) if targetFile == secret.Path { @@ -227,7 +227,7 @@ func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*te if err := os.MkdirAll(parent, os.ModePerm); err != nil { return fmt.Errorf("cannot create parent directory of '%s': %w", secret.Path, err) } - if err := symlinkSecret(targetFile, secret.Path, secret.owner, secret.group, userMode); err != nil { + if err := createSymlink(targetFile, secret.Path, secret.owner, secret.group, userMode); err != nil { return fmt.Errorf("failed to symlink secret '%s': %w", secret.Path, err) } } @@ -241,7 +241,7 @@ func symlinkSecrets(targetDir string, secrets []secret, templates map[string]*te if err := os.MkdirAll(parent, os.ModePerm); err != nil { return fmt.Errorf("cannot create parent directory of '%s': %w", template.Path, err) } - if err := symlinkSecret(targetFile, template.Path, template.owner, template.group, userMode); err != nil { + if err := createSymlink(targetFile, template.Path, template.owner, template.group, userMode); err != nil { return fmt.Errorf("failed to symlink template '%s': %w", template.Path, err) } } @@ -610,8 +610,9 @@ func (app *appContext) validateSecret(secret *secret) error { return app.validateSopsFile(secret, &file) } -func renderTemplates(templates map[string]*template, secretByPlaceholder map[string]*secret) { - for _, template := range templates { +func renderTemplates(templates []template, secretByPlaceholder map[string]*secret) { + for i := range templates { + template := &templates[i] rendered := renderTemplate(&template.content, secretByPlaceholder) template.value = []byte(rendered) } @@ -702,7 +703,8 @@ func (app *appContext) validateManifest() error { } } - for _, template := range m.Templates { + for i := range m.Templates { + template := &m.Templates[i] if err := app.validateTemplate(template); err != nil { return err } @@ -893,7 +895,7 @@ func symlinkWalk(filename string, linkDirname string, walkFn filepath.WalkFunc) return filepath.Walk(filename, symWalkFunc) } -func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates map[string]*template) error { +func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, secretDir string, secrets []secret, templates []template) error { var restart []string var reload []string @@ -1148,7 +1150,7 @@ func replaceRuntimeDir(path, rundir string) (ret string) { return } -func writeTemplates(targetDir string, templates map[string]*template, keysGID int, userMode bool) error { +func writeTemplates(targetDir string, templates []template, keysGID int, userMode bool) error { for _, template := range templates { fp := filepath.Join(targetDir, template.Name) @@ -1302,7 +1304,7 @@ func installSecrets(args []string) error { if isDry { return nil } - if err := symlinkSecrets(manifest.SymlinkPath, manifest.Secrets, manifest.Templates, manifest.UserMode); err != nil { + if err := symlinkSecretsAndTemplates(manifest.SymlinkPath, manifest.Secrets, manifest.Templates, manifest.UserMode); err != nil { return fmt.Errorf("failed to prepare symlinks to secret store: %w", err) } if err := atomicSymlink(*secretDir, manifest.SymlinkPath); err != nil { diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 765d8be..dc246b6 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -266,6 +266,10 @@ in { age.keyFile = "/run/age-keys.txt"; defaultSopsFile = ./test-assets/secrets.yaml; secrets.test_key = { }; + + # Verify that things work even with `neededForUsers` secrets. See + # . + secrets."nested/test/file".neededForUsers = true; }; # must run before sops sets up keys From a7b8f0feb7f775c9467137180054ebd9474fc6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 11:35:16 +0100 Subject: [PATCH 36/53] define templates for home-manager --- modules/home-manager/sops.nix | 4 ++ modules/home-manager/templates.nix | 91 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 modules/home-manager/templates.nix diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 9432c41..e8ad585 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -109,6 +109,10 @@ let ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest} '')); in { + imports = [ + ./templates.nix + ]; + options.sops = { secrets = lib.mkOption { type = lib.types.attrsOf secretType; diff --git a/modules/home-manager/templates.nix b/modules/home-manager/templates.nix new file mode 100644 index 0000000..a1901a5 --- /dev/null +++ b/modules/home-manager/templates.nix @@ -0,0 +1,91 @@ +{ config, pkgs, lib, options, ... }: +let + inherit (lib) + mkOption + mkDefault + mapAttrs + types + ; +in { + options.sops = { + templates = mkOption { + description = "Templates for secret files"; + type = types.attrsOf (types.submodule ({ config, ... }: { + options = { + name = mkOption { + type = types.singleLineStr; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets/rendered + ''; + }; + path = mkOption { + description = "Path where the rendered file will be placed"; + type = types.singleLineStr; + # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` + default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}"; + }; + content = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = types.singleLineStr; + default = "0400"; + description = '' + Permissions mode of the rendered secret file in octal. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; + example = "./configuration-template.conf"; + description = '' + File used as the template. When this value is specified, `sops.templates..content` is ignored. + ''; + }; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be restarted when the rendered template changes. + This works the same way as . + ''; + }; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when the rendered template changes. + This works the same way as . + ''; + }; + }; + })); + default = { }; + }; + placeholder = mkOption { + type = types.attrsOf (types.mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; + }); + default = { }; + visible = false; + }; + }; + + config = lib.optionalAttrs (options ? sops.secrets) + (lib.mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs + (name: _: mkDefault "") + config.sops.secrets; + }); +} From b05bdb2650aee87e90e76c38713f5e9fd5d35037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 11:35:23 +0100 Subject: [PATCH 37/53] nix-darwin: fix evaluation with templates --- modules/nix-darwin/default.nix | 7 +++++-- modules/nix-darwin/secrets-for-users/default.nix | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/nix-darwin/default.nix b/modules/nix-darwin/default.nix index 50dec02..351bc64 100644 --- a/modules/nix-darwin/default.nix +++ b/modules/nix-darwin/default.nix @@ -1,4 +1,4 @@ -{ config, options, lib, pkgs, ... }: +{ config, lib, pkgs, ... }: let cfg = config.sops; @@ -7,7 +7,10 @@ let inherit cfg; inherit (pkgs) writeTextFile; }; - manifest = manifestFor "" regularSecrets {}; + manifest = manifestFor "" regularSecrets regularTemplates {}; + + # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) + regularTemplates = cfg.templates; pathNotInStore = lib.mkOptionType { name = "pathNotInStore"; diff --git a/modules/nix-darwin/secrets-for-users/default.nix b/modules/nix-darwin/secrets-for-users/default.nix index b2c830a..c026cf4 100644 --- a/modules/nix-darwin/secrets-for-users/default.nix +++ b/modules/nix-darwin/secrets-for-users/default.nix @@ -2,6 +2,7 @@ let cfg = config.sops; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. manifestFor = pkgs.callPackage ../manifest-for.nix { inherit cfg; inherit (pkgs) writeTextFile; @@ -9,7 +10,7 @@ let withEnvironment = import ../with-environment.nix { inherit cfg lib; }; - manifestForUsers = manifestFor "-for-users" secretsForUsers { + manifestForUsers = manifestFor "-for-users" secretsForUsers templatesForUsers { secretsMountPoint = "/run/secrets-for-users.d"; symlinkPath = "/run/secrets-for-users"; }; From 6b85086bccc660291db1652cbe97462cc9f3cd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 12:17:45 +0100 Subject: [PATCH 38/53] reformat code base with nixfmt --- default.nix | 18 +- flake.nix | 137 +++-- modules/home-manager/sops.nix | 358 ++++++----- modules/home-manager/templates.nix | 161 ++--- modules/nix-darwin/default.nix | 361 +++++++----- modules/nix-darwin/manifest-for.nix | 45 +- .../nix-darwin/secrets-for-users/default.nix | 27 +- modules/nix-darwin/templates/default.nix | 153 ++--- modules/nix-darwin/with-environment.nix | 17 +- modules/sops/default.nix | 447 ++++++++------ modules/sops/manifest-for.nix | 45 +- modules/sops/secrets-for-users/default.nix | 78 ++- modules/sops/templates/default.nix | 191 +++--- modules/sops/with-environment.nix | 15 +- pkgs/sops-import-keys-hook/default.nix | 18 +- .../test-assets/shell.nix | 4 +- pkgs/sops-init-gpg-key/default.nix | 21 +- pkgs/sops-install-secrets/default.nix | 40 +- pkgs/sops-install-secrets/nixos-test.nix | 555 ++++++++++-------- pkgs/sops-install-secrets/shell.nix | 11 +- pkgs/sops-pgp-hook/default.nix | 18 +- pkgs/sops-pgp-hook/test-assets/shell.nix | 4 +- pkgs/unit-tests.nix | 23 +- shell.nix | 4 +- 24 files changed, 1592 insertions(+), 1159 deletions(-) diff --git a/default.nix b/default.nix index 11bb771..0c4e2ed 100644 --- a/default.nix +++ b/default.nix @@ -1,12 +1,15 @@ -{ pkgs ? import {} -, vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=" -}: let +{ + pkgs ? import { }, + vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=", +}: +let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets { inherit vendorHash; }; -in rec { +in +rec { inherit sops-install-secrets; - sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key {}; + sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { }; default = sops-init-gpg-key; sops-pgp-hook = pkgs.lib.warn '' @@ -22,8 +25,9 @@ in rec { sops-pgp-hook-test = pkgs.callPackage ./pkgs/sops-pgp-hook-test.nix { inherit vendorHash; }; - unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {}; -} // (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { + unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; +} +// (pkgs.lib.optionalAttrs pkgs.stdenv.isLinux { lint = pkgs.callPackage ./pkgs/lint.nix { inherit sops-install-secrets; }; diff --git a/flake.nix b/flake.nix index 8ebb37d..2777d50 100644 --- a/flake.nix +++ b/flake.nix @@ -2,60 +2,89 @@ description = "Integrates sops into nixos"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05"; - nixConfig.extra-substituters = ["https://cache.thalheim.io"]; - nixConfig.extra-trusted-public-keys = ["cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="]; - outputs = { - self, - nixpkgs, - nixpkgs-stable - }: let - systems = [ - "x86_64-linux" - "x86_64-darwin" - "aarch64-darwin" - "aarch64-linux" - ]; - forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); - suffix-version = version: attrs: nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; - suffix-stable = suffix-version "-24_05"; - in { - overlays.default = final: prev: let - localPkgs = import ./default.nix {pkgs = final;}; - in { - inherit (localPkgs) sops-install-secrets sops-init-gpg-key sops-pgp-hook sops-import-keys-hook sops-ssh-to-age; - # backward compatibility - inherit (prev) ssh-to-pgp; - }; - nixosModules = { - sops = ./modules/sops; - default = self.nixosModules.sops; - }; - homeManagerModules.sops = ./modules/home-manager/sops.nix; - homeManagerModule = self.homeManagerModules.sops; - darwinModules = { - sops = ./modules/nix-darwin; - default = self.darwinModules.sops; - }; - packages = forAllSystems (system: - import ./default.nix { - pkgs = import nixpkgs {inherit system;}; - }); - checks = nixpkgs.lib.genAttrs ["x86_64-linux" "aarch64-linux"] - (system: let - tests = self.packages.${system}.sops-install-secrets.tests; - packages-stable = import ./default.nix { - pkgs = import nixpkgs-stable {inherit system;}; + nixConfig.extra-substituters = [ "https://cache.thalheim.io" ]; + nixConfig.extra-trusted-public-keys = [ + "cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc=" + ]; + outputs = + { + self, + nixpkgs, + nixpkgs-stable, + }: + let + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-darwin" + "aarch64-linux" + ]; + forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); + suffix-version = + version: attrs: + nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; + suffix-stable = suffix-version "-24_05"; + in + { + overlays.default = + final: prev: + let + localPkgs = import ./default.nix { pkgs = final; }; + in + { + inherit (localPkgs) + sops-install-secrets + sops-init-gpg-key + sops-pgp-hook + sops-import-keys-hook + sops-ssh-to-age + ; + # backward compatibility + inherit (prev) ssh-to-pgp; }; - tests-stable = packages-stable.sops-install-secrets.tests; - in tests // - (suffix-stable tests-stable) // - (suffix-stable packages-stable)); + nixosModules = { + sops = ./modules/sops; + default = self.nixosModules.sops; + }; + homeManagerModules.sops = ./modules/home-manager/sops.nix; + homeManagerModule = self.homeManagerModules.sops; + darwinModules = { + sops = ./modules/nix-darwin; + default = self.darwinModules.sops; + }; + packages = forAllSystems ( + system: + import ./default.nix { + pkgs = import nixpkgs { inherit system; }; + } + ); + checks = + nixpkgs.lib.genAttrs + [ + "x86_64-linux" + "aarch64-linux" + ] + ( + system: + let + tests = self.packages.${system}.sops-install-secrets.tests; + packages-stable = import ./default.nix { + pkgs = import nixpkgs-stable { inherit system; }; + }; + tests-stable = packages-stable.sops-install-secrets.tests; + in + tests // (suffix-stable tests-stable) // (suffix-stable packages-stable) + ); - devShells = forAllSystems (system: let - pkgs = nixpkgs.legacyPackages.${system}; - in { - unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix {}; - default = pkgs.callPackage ./shell.nix {}; - }); - }; + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; + default = pkgs.callPackage ./shell.nix { }; + } + ); + }; } diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index e8ad585..68ca842 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -1,67 +1,81 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.sops; - sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; - secretType = lib.types.submodule ({ name, ... }: { - options = { - name = lib.mkOption { - type = lib.types.str; - default = name; - description = '' - Name of the file used in /run/user/*/secrets - ''; - }; + sops-install-secrets = (pkgs.callPackage ../.. { }).sops-install-secrets; + secretType = lib.types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = '' + Name of the file used in /run/user/*/secrets + ''; + }; - key = lib.mkOption { - type = lib.types.str; - default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name; - description = '' - Key used to lookup in the sops file. - No tested data structures are supported right now. - This option is ignored if format is binary. - "" means whole file. - ''; - }; + key = lib.mkOption { + type = lib.types.str; + default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name; + description = '' + Key used to lookup in the sops file. + No tested data structures are supported right now. + This option is ignored if format is binary. + "" means whole file. + ''; + }; - path = lib.mkOption { - type = lib.types.str; - default = "${cfg.defaultSymlinkPath}/${name}"; - description = '' - Path where secrets are symlinked to. - If the default is kept no other symlink is created. - `%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf - DARWIN_USER_TEMP_DIR` on darwin. - ''; - }; + path = lib.mkOption { + type = lib.types.str; + default = "${cfg.defaultSymlinkPath}/${name}"; + description = '' + Path where secrets are symlinked to. + If the default is kept no other symlink is created. + `%r` is replaced by $XDG_RUNTIME_DIR on linux or `getconf + DARWIN_USER_TEMP_DIR` on darwin. + ''; + }; - format = lib.mkOption { - type = lib.types.enum [ "yaml" "json" "binary" "ini" "dotenv" ]; - default = cfg.defaultSopsFormat; - description = '' - File format used to decrypt the sops secret. - Binary files are written to the target file as is. - ''; - }; + format = lib.mkOption { + type = lib.types.enum [ + "yaml" + "json" + "binary" + "ini" + "dotenv" + ]; + default = cfg.defaultSopsFormat; + description = '' + File format used to decrypt the sops secret. + Binary files are written to the target file as is. + ''; + }; - mode = lib.mkOption { - type = lib.types.str; - default = "0400"; - description = '' - Permissions mode of the in octal. - ''; - }; + mode = lib.mkOption { + type = lib.types.str; + default = "0400"; + description = '' + Permissions mode of the in octal. + ''; + }; - sopsFile = lib.mkOption { - type = lib.types.path; - default = cfg.defaultSopsFile; - defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; - description = '' - Sops file the secret is loaded from. - ''; + sopsFile = lib.mkOption { + type = lib.types.path; + default = cfg.defaultSopsFile; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; + description = '' + Sops file the secret is loaded from. + ''; + }; }; - }; - }); + } + ); pathNotInStore = lib.mkOptionType { name = "pathNotInStore"; @@ -71,44 +85,54 @@ let merge = lib.mergeEqualOption; }; - manifestFor = suffix: secrets: templates: pkgs.writeTextFile { - name = "manifest${suffix}.json"; - text = builtins.toJSON { - secrets = builtins.attrValues secrets; - templates = builtins.attrValues templates; - secretsMountPoint = cfg.defaultSecretsMountPoint; - symlinkPath = cfg.defaultSymlinkPath; - keepGenerations = cfg.keepGenerations; - gnupgHome = cfg.gnupg.home; - sshKeyPaths = cfg.gnupg.sshKeyPaths; - ageKeyFile = cfg.age.keyFile; - ageSshKeyPaths = cfg.age.sshKeyPaths; - userMode = true; - logging = { - keyImport = builtins.elem "keyImport" cfg.log; - secretChanges = builtins.elem "secretChanges" cfg.log; + manifestFor = + suffix: secrets: templates: + pkgs.writeTextFile { + name = "manifest${suffix}.json"; + text = builtins.toJSON { + secrets = builtins.attrValues secrets; + templates = builtins.attrValues templates; + secretsMountPoint = cfg.defaultSecretsMountPoint; + symlinkPath = cfg.defaultSymlinkPath; + keepGenerations = cfg.keepGenerations; + gnupgHome = cfg.gnupg.home; + sshKeyPaths = cfg.gnupg.sshKeyPaths; + ageKeyFile = cfg.age.keyFile; + ageSshKeyPaths = cfg.age.sshKeyPaths; + userMode = true; + logging = { + keyImport = builtins.elem "keyImport" cfg.log; + secretChanges = builtins.elem "secretChanges" cfg.log; + }; }; + checkPhase = '' + ${sops-install-secrets}/bin/sops-install-secrets -check-mode=${ + if cfg.validateSopsFiles then "sopsfile" else "manifest" + } "$out" + ''; }; - checkPhase = '' - ${sops-install-secrets}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" - ''; - }; manifest = manifestFor "" cfg.secrets cfg.templates; escapedAgeKeyFile = lib.escapeShellArg cfg.age.keyFile; - script = toString (pkgs.writeShellScript "sops-nix-user" (lib.optionalString cfg.age.generateKey '' - if [[ ! -f ${escapedAgeKeyFile} ]]; then - echo generating machine-specific age key... - ${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile}) - # age-keygen sets 0600 by default, no need to chmod. - ${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile} - fi - '' + '' - ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest} - '')); -in { + script = toString ( + pkgs.writeShellScript "sops-nix-user" ( + lib.optionalString cfg.age.generateKey '' + if [[ ! -f ${escapedAgeKeyFile} ]]; then + echo generating machine-specific age key... + ${pkgs.coreutils}/bin/mkdir -p $(${pkgs.coreutils}/bin/dirname ${escapedAgeKeyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${escapedAgeKeyFile} + fi + '' + + '' + ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest} + '' + ) + ); +in +{ imports = [ ./templates.nix ]; @@ -116,7 +140,7 @@ in { options.sops = { secrets = lib.mkOption { type = lib.types.attrsOf secretType; - default = {}; + default = { }; description = '' Secrets to decrypt. ''; @@ -182,14 +206,22 @@ in { }; log = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); - default = [ "keyImport" "secretChanges" ]; + type = lib.types.listOf ( + lib.types.enum [ + "keyImport" + "secretChanges" + ] + ); + default = [ + "keyImport" + "secretChanges" + ]; description = "What to log"; }; environment = lib.mkOption { type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); - default = {}; + default = { }; description = '' Environment variables to set before calling sops-install-secrets. @@ -219,7 +251,7 @@ in { sshKeyPaths = lib.mkOption { type = lib.types.listOf lib.types.path; - default = []; + default = [ ]; description = '' Paths to ssh keys added as age keys during sops description. ''; @@ -251,7 +283,7 @@ in { sshKeyPaths = lib.mkOption { type = lib.types.listOf lib.types.path; - default = []; + default = [ ]; description = '' Path to ssh keys added as GPG keys during sops description. This option must be explicitly unset if config.sops.gnupg.sshKeyPaths is set. @@ -260,37 +292,52 @@ in { }; }; - config = lib.mkIf (cfg.secrets != {}) { - assertions = [{ - assertion = - cfg.gnupg.home != null || - cfg.gnupg.sshKeyPaths != [] || - cfg.gnupg.qubes-split-gpg.enable == true || - cfg.age.keyFile != null || - cfg.age.sshKeyPaths != []; - message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable"; - } { - assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []) && - !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true) && - !(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true); - message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set"; - } { - assertion = cfg.gnupg.qubes-split-gpg.enable == false || - (cfg.gnupg.qubes-split-gpg.enable == true && - cfg.gnupg.qubes-split-gpg.domain != null && - cfg.gnupg.qubes-split-gpg.domain != ""); - message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true"; - }] ++ lib.optionals cfg.validateSopsFiles ( - lib.concatLists (lib.mapAttrsToList (name: secret: [{ - assertion = builtins.pathExists secret.sopsFile; - message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; - } { - assertion = - builtins.isPath secret.sopsFile || - (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); - message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; - }]) cfg.secrets) - ); + config = lib.mkIf (cfg.secrets != { }) { + assertions = + [ + { + assertion = + cfg.gnupg.home != null + || cfg.gnupg.sshKeyPaths != [ ] + || cfg.gnupg.qubes-split-gpg.enable == true + || cfg.age.keyFile != null + || cfg.age.sshKeyPaths != [ ]; + message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home or sops.gnupg.qubes-split-gpg.enable"; + } + { + assertion = + !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]) + && !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true) + && !(cfg.gnupg.sshKeyPaths != [ ] && cfg.gnupg.qubes-split-gpg.enable == true); + message = "Exactly one of sops.gnupg.home, sops.gnupg.qubes-split-gpg.enable and sops.gnupg.sshKeyPaths must be set"; + } + { + assertion = + cfg.gnupg.qubes-split-gpg.enable == false + || ( + cfg.gnupg.qubes-split-gpg.enable == true + && cfg.gnupg.qubes-split-gpg.domain != null + && cfg.gnupg.qubes-split-gpg.domain != "" + ); + message = "sops.gnupg.qubes-split-gpg.domain is required when sops.gnupg.qubes-split-gpg.enable is set to true"; + } + ] + ++ lib.optionals cfg.validateSopsFiles ( + lib.concatLists ( + lib.mapAttrsToList (name: secret: [ + { + assertion = builtins.pathExists secret.sopsFile; + message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; + } + { + assertion = + builtins.isPath secret.sopsFile + || (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); + message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + } + ]) cfg.secrets + ) + ); home.sessionVariables = lib.mkIf cfg.gnupg.qubes-split-gpg.enable { # TODO: Add this package to nixpkgs and use it from the store @@ -300,11 +347,17 @@ in { sops.environment = { SOPS_GPG_EXEC = lib.mkMerge [ - (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg")) - (lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC)) + (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) ( + lib.mkDefault "${pkgs.gnupg}/bin/gpg" + )) + (lib.mkIf cfg.gnupg.qubes-split-gpg.enable ( + lib.mkDefault config.home.sessionVariables.SOPS_GPG_EXEC + )) ]; - QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable (lib.mkDefault cfg.gnupg.qubes-split-gpg.domain); + QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable ( + lib.mkDefault cfg.gnupg.qubes-split-gpg.domain + ); }; systemd.user.services.sops-nix = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { @@ -313,10 +366,13 @@ in { }; Service = { Type = "oneshot"; - Environment = builtins.concatStringsSep " " (lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment); + Environment = builtins.concatStringsSep " " ( + lib.mapAttrsToList (name: value: "'${name}=${value}'") cfg.environment + ); ExecStart = script; }; - Install.WantedBy = if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; + Install.WantedBy = + if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; }; # Darwin: load secrets once on login @@ -333,28 +389,36 @@ in { }; # [re]load secrets on home-manager activation - home.activation = let - darwin = let - domain-target = "gui/$(id -u ${config.home.username})"; - in '' - /bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true - /bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist - ''; + home.activation = + let + darwin = + let + domain-target = "gui/$(id -u ${config.home.username})"; + in + '' + /bin/launchctl bootout ${domain-target}/org.nix-community.home.sops-nix && true + /bin/launchctl bootstrap ${domain-target} ${config.home.homeDirectory}/Library/LaunchAgents/org.nix-community.home.sops-nix.plist + ''; - linux = let systemctl = config.systemd.user.systemctlPath; in '' - systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) + linux = + let + systemctl = config.systemd.user.systemctlPath; + in + '' + systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) - if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then - ${systemctl} restart --user sops-nix - else - echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed." - fi + if [[ $systemdStatus == 'running' || $systemdStatus == 'degraded' ]]; then + ${systemctl} restart --user sops-nix + else + echo "User systemd daemon not running. Probably executed on boot where no manual start/reload is needed." + fi - unset systemdStatus - ''; + unset systemdStatus + ''; - in { - sops-nix = if pkgs.stdenv.isLinux then linux else darwin; - }; + in + { + sops-nix = if pkgs.stdenv.isLinux then linux else darwin; + }; }; } diff --git a/modules/home-manager/templates.nix b/modules/home-manager/templates.nix index a1901a5..c64802a 100644 --- a/modules/home-manager/templates.nix +++ b/modules/home-manager/templates.nix @@ -1,91 +1,106 @@ -{ config, pkgs, lib, options, ... }: +{ + config, + pkgs, + lib, + options, + ... +}: let inherit (lib) mkOption mkDefault mapAttrs types - ; -in { + ; +in +{ options.sops = { templates = mkOption { description = "Templates for secret files"; - type = types.attrsOf (types.submodule ({ config, ... }: { - options = { - name = mkOption { - type = types.singleLineStr; - default = config._module.args.name; - description = '' - Name of the file used in /run/secrets/rendered - ''; - }; - path = mkOption { - description = "Path where the rendered file will be placed"; - type = types.singleLineStr; - # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` - default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}"; - }; - content = mkOption { - type = types.lines; - default = ""; - description = '' - Content of the file - ''; - }; - mode = mkOption { - type = types.singleLineStr; - default = "0400"; - description = '' - Permissions mode of the rendered secret file in octal. - ''; - }; - file = mkOption { - type = types.path; - default = pkgs.writeText config.name config.content; - defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; - example = "./configuration-template.conf"; - description = '' - File used as the template. When this value is specified, `sops.templates..content` is ignored. - ''; - }; - restartUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be restarted when the rendered template changes. - This works the same way as . - ''; - }; - reloadUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be reloaded when the rendered template changes. - This works the same way as . - ''; - }; - }; - })); + type = types.attrsOf ( + types.submodule ( + { config, ... }: + { + options = { + name = mkOption { + type = types.singleLineStr; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets/rendered + ''; + }; + path = mkOption { + description = "Path where the rendered file will be placed"; + type = types.singleLineStr; + # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` + default = "${config.xdg.configHome}/sops-nix/secrets/rendered/${config.name}"; + }; + content = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = types.singleLineStr; + default = "0400"; + description = '' + Permissions mode of the rendered secret file in octal. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; + example = "./configuration-template.conf"; + description = '' + File used as the template. When this value is specified, `sops.templates..content` is ignored. + ''; + }; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be restarted when the rendered template changes. + This works the same way as . + ''; + }; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when the rendered template changes. + This works the same way as . + ''; + }; + }; + } + ) + ); default = { }; }; placeholder = mkOption { - type = types.attrsOf (types.mkOptionType { - name = "coercibleToString"; - description = "value that can be coerced to string"; - check = lib.strings.isConvertibleWithToString; - merge = lib.mergeEqualOption; - }); + type = types.attrsOf ( + types.mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; + } + ); default = { }; visible = false; }; }; - config = lib.optionalAttrs (options ? sops.secrets) - (lib.mkIf (config.sops.templates != { }) { - sops.placeholder = mapAttrs - (name: _: mkDefault "") - config.sops.secrets; - }); + config = lib.optionalAttrs (options ? sops.secrets) ( + lib.mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs ( + name: _: mkDefault "" + ) config.sops.secrets; + } + ); } diff --git a/modules/nix-darwin/default.nix b/modules/nix-darwin/default.nix index 351bc64..4281d51 100644 --- a/modules/nix-darwin/default.nix +++ b/modules/nix-darwin/default.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.sops; @@ -7,7 +12,7 @@ let inherit cfg; inherit (pkgs) writeTextFile; }; - manifest = manifestFor "" regularSecrets regularTemplates {}; + manifest = manifestFor "" regularSecrets regularTemplates { }; # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) regularTemplates = cfg.templates; @@ -25,138 +30,165 @@ let withEnvironment = import ./with-environment.nix { inherit cfg lib; }; - secretType = lib.types.submodule ({ config, ... }: { - config = { - sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; - sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); - }; - options = { - name = lib.mkOption { - type = lib.types.str; - default = config._module.args.name; - description = '' - Name of the file used in /run/secrets - ''; + secretType = lib.types.submodule ( + { config, ... }: + { + config = { + sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; + sopsFileHash = lib.mkOptionDefault ( + lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}" + ); }; - key = lib.mkOption { - type = lib.types.str; - default = config._module.args.name; - description = '' - Key used to lookup in the sops file. - No tested data structures are supported right now. - This option is ignored if format is binary. - ''; + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets + ''; + }; + key = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = '' + Key used to lookup in the sops file. + No tested data structures are supported right now. + This option is ignored if format is binary. + ''; + }; + path = lib.mkOption { + type = lib.types.str; + default = + if config.neededForUsers then + "/run/secrets-for-users/${config.name}" + else + "/run/secrets/${config.name}"; + defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise."; + description = '' + Path where secrets are symlinked to. + If the default is kept no symlink is created. + ''; + }; + format = lib.mkOption { + type = lib.types.enum [ + "yaml" + "json" + "binary" + "dotenv" + "ini" + ]; + default = cfg.defaultSopsFormat; + description = '' + File format used to decrypt the sops secret. + Binary files are written to the target file as is. + ''; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "0400"; + description = '' + Permissions mode of the in octal. + ''; + }; + owner = lib.mkOption { + type = with lib.types; nullOr str; + default = "root"; + description = '' + User of the file. Can only be set if uid is 0. + ''; + }; + uid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. + ''; + }; + group = lib.mkOption { + type = with lib.types; nullOr str; + default = "staff"; + defaultText = "staff"; + description = '' + Group of the file. Can only be set if gid is 0. + ''; + }; + gid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. + ''; + }; + sopsFile = lib.mkOption { + type = lib.types.path; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; + description = '' + Sops file the secret is loaded from. + ''; + }; + sopsFileHash = lib.mkOption { + type = lib.types.str; + readOnly = true; + description = '' + Hash of the sops file. + ''; + }; + neededForUsers = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + **Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS. + This can be used to retrieve user's passwords from sops-nix. + Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. + ''; + }; }; - path = lib.mkOption { - type = lib.types.str; - default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}"; - defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise."; - description = '' - Path where secrets are symlinked to. - If the default is kept no symlink is created. - ''; - }; - format = lib.mkOption { - type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"]; - default = cfg.defaultSopsFormat; - description = '' - File format used to decrypt the sops secret. - Binary files are written to the target file as is. - ''; - }; - mode = lib.mkOption { - type = lib.types.str; - default = "0400"; - description = '' - Permissions mode of the in octal. - ''; - }; - owner = lib.mkOption { - type = with lib.types; nullOr str; - default = "root"; - description = '' - User of the file. Can only be set if uid is 0. - ''; - }; - uid = lib.mkOption { - type = with lib.types; nullOr int; - default = 0; - description = '' - UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. - ''; - }; - group = lib.mkOption { - type = with lib.types; nullOr str; - default = "staff"; - defaultText = "staff"; - description = '' - Group of the file. Can only be set if gid is 0. - ''; - }; - gid = lib.mkOption { - type = with lib.types; nullOr int; - default = 0; - description = '' - GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. - ''; - }; - sopsFile = lib.mkOption { - type = lib.types.path; - defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; - description = '' - Sops file the secret is loaded from. - ''; - }; - sopsFileHash = lib.mkOption { - type = lib.types.str; - readOnly = true; - description = '' - Hash of the sops file. - ''; - }; - neededForUsers = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - **Warning** This option doesn't have any effect on macOS, as nix-darwin cannot manage user passwords on macOS. - This can be used to retrieve user's passwords from sops-nix. - Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. - ''; - }; - }; - }); + } + ); - darwinSSHKeys = [{ - type = "rsa"; - path = "/etc/ssh/ssh_host_rsa_key"; - } { - type = "ed25519"; - path = "/etc/ssh/ssh_host_ed25519_key"; - }]; + darwinSSHKeys = [ + { + type = "rsa"; + path = "/etc/ssh/ssh_host_rsa_key"; + } + { + type = "ed25519"; + path = "/etc/ssh/ssh_host_ed25519_key"; + } + ]; escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; # Skip ssh keys deployed with sops to avoid a catch 22 - defaultImportKeys = algo: - map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys); + defaultImportKeys = + algo: + map (e: e.path) ( + lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) darwinSSHKeys + ); installScript = '' - ${if cfg.age.generateKey then '' - if [[ ! -f ${escapedKeyFile} ]]; then - echo generating machine-specific age key... - mkdir -p $(dirname ${escapedKeyFile}) - # age-keygen sets 0600 by default, no need to chmod. - ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} - fi - '' else ""} + ${ + if cfg.age.generateKey then + '' + if [[ ! -f ${escapedKeyFile} ]]; then + echo generating machine-specific age key... + mkdir -p $(dirname ${escapedKeyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} + fi + '' + else + "" + } echo "Setting up secrets..." ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} ''; -in { +in +{ options.sops = { secrets = lib.mkOption { type = lib.types.attrsOf secretType; - default = {}; + default = { }; description = '' Path where the latest secrets are mounted to. ''; @@ -195,14 +227,22 @@ in { }; log = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); - default = [ "keyImport" "secretChanges" ]; + type = lib.types.listOf ( + lib.types.enum [ + "keyImport" + "secretChanges" + ] + ); + default = [ + "keyImport" + "secretChanges" + ]; description = "What to log"; }; environment = lib.mkOption { type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); - default = {}; + default = { }; description = '' Environment variables to set before calling sops-install-secrets. @@ -217,7 +257,7 @@ in { package = lib.mkOption { type = lib.types.package; - default = (pkgs.callPackage ../.. {}).sops-install-secrets; + default = (pkgs.callPackage ../.. { }).sops-install-secrets; defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; description = '' sops-install-secrets package to use. @@ -227,9 +267,10 @@ in { validationPackage = lib.mkOption { type = lib.types.package; default = - if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform - then sops-install-secrets - else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets; + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then + sops-install-secrets + else + (pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets; defaultText = lib.literalExpression "config.sops.package"; description = '' @@ -296,30 +337,46 @@ in { ]; config = lib.mkMerge [ - (lib.mkIf (cfg.secrets != {}) { - assertions = [{ - assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != []; - message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; - } { - assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); - message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; - }] ++ lib.optionals cfg.validateSopsFiles ( - lib.concatLists (lib.mapAttrsToList (name: secret: [{ - assertion = builtins.pathExists secret.sopsFile; - message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; - } { - assertion = - builtins.isPath secret.sopsFile || - (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); - message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; - } { - assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; - message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; - } { - assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; - message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; - }]) cfg.secrets) - ); + (lib.mkIf (cfg.secrets != { }) { + assertions = + [ + { + assertion = + cfg.gnupg.home != null + || cfg.gnupg.sshKeyPaths != [ ] + || cfg.age.keyFile != null + || cfg.age.sshKeyPaths != [ ]; + message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; + } + { + assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]); + message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; + } + ] + ++ lib.optionals cfg.validateSopsFiles ( + lib.concatLists ( + lib.mapAttrsToList (name: secret: [ + { + assertion = builtins.pathExists secret.sopsFile; + message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; + } + { + assertion = + builtins.isPath secret.sopsFile + || (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); + message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + } + { + assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; + message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; + } + { + assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; + message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; + } + ]) cfg.secrets + ) + ); system.build.sops-nix-manifest = manifest; system.activationScripts = { @@ -336,7 +393,9 @@ in { }) { - sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); + sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) ( + lib.mkDefault "${pkgs.gnupg}/bin/gpg" + ); } ]; } diff --git a/modules/nix-darwin/manifest-for.nix b/modules/nix-darwin/manifest-for.nix index 6ab2ba0..c4a0524 100644 --- a/modules/nix-darwin/manifest-for.nix +++ b/modules/nix-darwin/manifest-for.nix @@ -4,26 +4,31 @@ suffix: secrets: extraJson: writeTextFile { name = "manifest${suffix}.json"; - text = builtins.toJSON ({ - secrets = builtins.attrValues secrets; - # Does this need to be configurable? - secretsMountPoint = "/run/secrets.d"; - symlinkPath = "/run/secrets"; - keepGenerations = cfg.keepGenerations; - gnupgHome = cfg.gnupg.home; - sshKeyPaths = cfg.gnupg.sshKeyPaths; - ageKeyFile = cfg.age.keyFile; - ageSshKeyPaths = cfg.age.sshKeyPaths; - useTmpfs = false; - templates = cfg.templates; - placeholderBySecretName = cfg.placeholder; - userMode = false; - logging = { - keyImport = builtins.elem "keyImport" cfg.log; - secretChanges = builtins.elem "secretChanges" cfg.log; - }; - } // extraJson); + text = builtins.toJSON ( + { + secrets = builtins.attrValues secrets; + # Does this need to be configurable? + secretsMountPoint = "/run/secrets.d"; + symlinkPath = "/run/secrets"; + keepGenerations = cfg.keepGenerations; + gnupgHome = cfg.gnupg.home; + sshKeyPaths = cfg.gnupg.sshKeyPaths; + ageKeyFile = cfg.age.keyFile; + ageSshKeyPaths = cfg.age.sshKeyPaths; + useTmpfs = false; + templates = cfg.templates; + placeholderBySecretName = cfg.placeholder; + userMode = false; + logging = { + keyImport = builtins.elem "keyImport" cfg.log; + secretChanges = builtins.elem "secretChanges" cfg.log; + }; + } + // extraJson + ); checkPhase = '' - ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" + ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${ + if cfg.validateSopsFiles then "sopsfile" else "manifest" + } "$out" ''; } diff --git a/modules/nix-darwin/secrets-for-users/default.nix b/modules/nix-darwin/secrets-for-users/default.nix index c026cf4..585416a 100644 --- a/modules/nix-darwin/secrets-for-users/default.nix +++ b/modules/nix-darwin/secrets-for-users/default.nix @@ -1,8 +1,14 @@ -{ lib, options, config, pkgs, ... }: +{ + lib, + options, + config, + pkgs, + ... +}: let cfg = config.sops; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; - templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. + templatesForUsers = { }; # We do not currently support `neededForUsers` for templates. manifestFor = pkgs.callPackage ../manifest-for.nix { inherit cfg; inherit (pkgs) writeTextFile; @@ -22,16 +28,21 @@ let in { - assertions = [{ - assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; - message = "neededForUsers cannot be used for secrets that are not root-owned"; - }]; + assertions = [ + { + assertion = + (lib.filterAttrs ( + _: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root") + ) secretsForUsers) == { }; + message = "neededForUsers cannot be used for secrets that are not root-owned"; + } + ]; - system.activationScripts = lib.mkIf (secretsForUsers != []) { + system.activationScripts = lib.mkIf (secretsForUsers != [ ]) { postActivation.text = lib.mkAfter installScript; }; - launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != []) { + launchd.daemons.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != [ ]) { command = installScript; serviceConfig = { RunAtLoad = true; diff --git a/modules/nix-darwin/templates/default.nix b/modules/nix-darwin/templates/default.nix index 2bb1e43..da1dcbb 100644 --- a/modules/nix-darwin/templates/default.nix +++ b/modules/nix-darwin/templates/default.nix @@ -1,87 +1,102 @@ -{ config, pkgs, lib, options, ... }: +{ + config, + pkgs, + lib, + options, + ... +}: let inherit (lib) mkOption mkDefault mapAttrs types - ; -in { + ; +in +{ options.sops = { templates = mkOption { description = "Templates for secret files"; - type = types.attrsOf (types.submodule ({ config, ... }: { - options = { - name = mkOption { - type = types.singleLineStr; - default = config._module.args.name; - description = '' - Name of the file used in /run/secrets/rendered - ''; - }; - path = mkOption { - description = "Path where the rendered file will be placed"; - type = types.singleLineStr; - default = "/run/secrets/rendered/${config.name}"; - }; - content = mkOption { - type = types.lines; - default = ""; - description = '' - Content of the file - ''; - }; - mode = mkOption { - type = types.singleLineStr; - default = "0400"; - description = '' - Permissions mode of the rendered secret file in octal. - ''; - }; - owner = mkOption { - type = types.singleLineStr; - default = "root"; - description = '' - User of the file. - ''; - }; - group = mkOption { - type = types.singleLineStr; - default = "staff"; - defaultText = "staff"; - description = '' - Group of the file. Default on darwin in staff. - ''; - }; - file = mkOption { - type = types.path; - default = pkgs.writeText config.name config.content; - defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; - example = "./configuration-template.conf"; - description = '' - File used as the template. When this value is specified, `sops.templates..content` is ignored. - ''; - }; - }; - })); + type = types.attrsOf ( + types.submodule ( + { config, ... }: + { + options = { + name = mkOption { + type = types.singleLineStr; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets/rendered + ''; + }; + path = mkOption { + description = "Path where the rendered file will be placed"; + type = types.singleLineStr; + default = "/run/secrets/rendered/${config.name}"; + }; + content = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = types.singleLineStr; + default = "0400"; + description = '' + Permissions mode of the rendered secret file in octal. + ''; + }; + owner = mkOption { + type = types.singleLineStr; + default = "root"; + description = '' + User of the file. + ''; + }; + group = mkOption { + type = types.singleLineStr; + default = "staff"; + defaultText = "staff"; + description = '' + Group of the file. Default on darwin in staff. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; + example = "./configuration-template.conf"; + description = '' + File used as the template. When this value is specified, `sops.templates..content` is ignored. + ''; + }; + }; + } + ) + ); default = { }; }; placeholder = mkOption { - type = types.attrsOf (types.mkOptionType { - name = "coercibleToString"; - description = "value that can be coerced to string"; - check = lib.strings.isConvertibleWithToString; - merge = lib.mergeEqualOption; - }); + type = types.attrsOf ( + types.mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; + } + ); default = { }; visible = false; }; }; - config = lib.optionalAttrs (options ? sops.secrets) - (lib.mkIf (config.sops.templates != { }) { - sops.placeholder = mapAttrs - (name: _: mkDefault "") - config.sops.secrets; - }); + config = lib.optionalAttrs (options ? sops.secrets) ( + lib.mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs ( + name: _: mkDefault "" + ) config.sops.secrets; + } + ); } diff --git a/modules/nix-darwin/with-environment.nix b/modules/nix-darwin/with-environment.nix index f252441..30d64f5 100644 --- a/modules/nix-darwin/with-environment.nix +++ b/modules/nix-darwin/with-environment.nix @@ -2,12 +2,13 @@ sopsCall: -if cfg.environment == {} then +if cfg.environment == { } then sopsCall -else '' - ( - # shellcheck disable=SC2030,SC2031 - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} - ${sopsCall} - ) -'' +else + '' + ( + # shellcheck disable=SC2030,SC2031 + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} + ${sopsCall} + ) + '' diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 72cb94e..860a9a9 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -1,4 +1,10 @@ -{ config, options, lib, pkgs, ... }: +{ + config, + options, + lib, + pkgs, + ... +}: let cfg = config.sops; @@ -8,7 +14,7 @@ let inherit cfg; inherit (pkgs) writeTextFile; }; - manifest = manifestFor "" regularSecrets regularTemplates {}; + manifest = manifestFor "" regularSecrets regularTemplates { }; pathNotInStore = lib.mkOptionType { name = "pathNotInStore"; @@ -23,143 +29,165 @@ let # Currently, all templates are "regular" (there's no support for `neededForUsers` for templates.) regularTemplates = cfg.templates; - useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || - (options.services ? userborn && config.services.userborn.enable); + useSystemdActivation = + (options.systemd ? sysusers && config.systemd.sysusers.enable) + || (options.services ? userborn && config.services.userborn.enable); withEnvironment = import ./with-environment.nix { inherit cfg lib; }; - secretType = lib.types.submodule ({ config, ... }: { - config = { - sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; - sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); - }; - options = { - name = lib.mkOption { - type = lib.types.str; - default = config._module.args.name; - description = '' - Name of the file used in /run/secrets - ''; + secretType = lib.types.submodule ( + { config, ... }: + { + config = { + sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; + sopsFileHash = lib.mkOptionDefault ( + lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}" + ); }; - key = lib.mkOption { - type = lib.types.str; - default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.name; - description = '' - Key used to lookup in the sops file. - No tested data structures are supported right now. - This option is ignored if format is binary. - "" means whole file. - ''; + options = { + name = lib.mkOption { + type = lib.types.str; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets + ''; + }; + key = lib.mkOption { + type = lib.types.str; + default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else config._module.args.name; + description = '' + Key used to lookup in the sops file. + No tested data structures are supported right now. + This option is ignored if format is binary. + "" means whole file. + ''; + }; + path = lib.mkOption { + type = lib.types.str; + default = + if config.neededForUsers then + "/run/secrets-for-users/${config.name}" + else + "/run/secrets/${config.name}"; + defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise."; + description = '' + Path where secrets are symlinked to. + If the default is kept no symlink is created. + ''; + }; + format = lib.mkOption { + type = lib.types.enum [ + "yaml" + "json" + "binary" + "dotenv" + "ini" + ]; + default = cfg.defaultSopsFormat; + description = '' + File format used to decrypt the sops secret. + Binary files are written to the target file as is. + ''; + }; + mode = lib.mkOption { + type = lib.types.str; + default = "0400"; + description = '' + Permissions mode of the in octal. + ''; + }; + owner = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + User of the file. Can only be set if uid is 0. + ''; + }; + uid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. + ''; + }; + group = lib.mkOption { + type = with lib.types; nullOr str; + default = if config.owner != null then users.${config.owner}.group else null; + defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`"; + description = '' + Group of the file. Can only be set if gid is 0. + ''; + }; + gid = lib.mkOption { + type = with lib.types; nullOr int; + default = 0; + description = '' + GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. + ''; + }; + sopsFile = lib.mkOption { + type = lib.types.path; + defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; + description = '' + Sops file the secret is loaded from. + ''; + }; + sopsFileHash = lib.mkOption { + type = lib.types.str; + readOnly = true; + description = '' + Hash of the sops file, useful in . + ''; + }; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be restarted when this secret changes. + This works the same way as . + ''; + }; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when this secret changes. + This works the same way as . + ''; + }; + neededForUsers = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enabling this option causes the secret to be decrypted before users and groups are created. + This can be used to retrieve user's passwords from sops-nix. + Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. + ''; + }; }; - path = lib.mkOption { - type = lib.types.str; - default = if config.neededForUsers then "/run/secrets-for-users/${config.name}" else "/run/secrets/${config.name}"; - defaultText = "/run/secrets-for-users/$name when neededForUsers is set, /run/secrets/$name when otherwise."; - description = '' - Path where secrets are symlinked to. - If the default is kept no symlink is created. - ''; - }; - format = lib.mkOption { - type = lib.types.enum ["yaml" "json" "binary" "dotenv" "ini"]; - default = cfg.defaultSopsFormat; - description = '' - File format used to decrypt the sops secret. - Binary files are written to the target file as is. - ''; - }; - mode = lib.mkOption { - type = lib.types.str; - default = "0400"; - description = '' - Permissions mode of the in octal. - ''; - }; - owner = lib.mkOption { - type = with lib.types; nullOr str; - default = null; - description = '' - User of the file. Can only be set if uid is 0. - ''; - }; - uid = lib.mkOption { - type = with lib.types; nullOr int; - default = 0; - description = '' - UID of the file, only applied when owner is null. The UID will be applied even if the corresponding user doesn't exist. - ''; - }; - group = lib.mkOption { - type = with lib.types; nullOr str; - default = if config.owner != null then users.${config.owner}.group else null; - defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`"; - description = '' - Group of the file. Can only be set if gid is 0. - ''; - }; - gid = lib.mkOption { - type = with lib.types; nullOr int; - default = 0; - description = '' - GID of the file, only applied when group is null. The GID will be applied even if the corresponding group doesn't exist. - ''; - }; - sopsFile = lib.mkOption { - type = lib.types.path; - defaultText = lib.literalExpression "\${config.sops.defaultSopsFile}"; - description = '' - Sops file the secret is loaded from. - ''; - }; - sopsFileHash = lib.mkOption { - type = lib.types.str; - readOnly = true; - description = '' - Hash of the sops file, useful in . - ''; - }; - restartUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be restarted when this secret changes. - This works the same way as . - ''; - }; - reloadUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be reloaded when this secret changes. - This works the same way as . - ''; - }; - neededForUsers = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Enabling this option causes the secret to be decrypted before users and groups are created. - This can be used to retrieve user's passwords from sops-nix. - Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. - ''; - }; - }; - }); + } + ); # Skip ssh keys deployed with sops to avoid a catch 22 - defaultImportKeys = algo: + defaultImportKeys = + algo: if config.services.openssh.enable then - map (e: e.path) (lib.filter (e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path)) config.services.openssh.hostKeys) + map (e: e.path) ( + lib.filter ( + e: e.type == algo && !(lib.hasPrefix "/run/secrets" e.path) + ) config.services.openssh.hostKeys + ) else - []; -in { + [ ]; +in +{ options.sops = { secrets = lib.mkOption { type = lib.types.attrsOf secretType; - default = {}; + default = { }; description = '' Path where the latest secrets are mounted to. ''; @@ -208,14 +236,22 @@ in { }; log = lib.mkOption { - type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); - default = [ "keyImport" "secretChanges" ]; + type = lib.types.listOf ( + lib.types.enum [ + "keyImport" + "secretChanges" + ] + ); + default = [ + "keyImport" + "secretChanges" + ]; description = "What to log"; }; environment = lib.mkOption { type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); - default = {}; + default = { }; description = '' Environment variables to set before calling sops-install-secrets. @@ -230,7 +266,7 @@ in { package = lib.mkOption { type = lib.types.package; - default = (pkgs.callPackage ../.. {}).sops-install-secrets; + default = (pkgs.callPackage ../.. { }).sops-install-secrets; defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; description = '' sops-install-secrets package to use. @@ -240,9 +276,10 @@ in { validationPackage = lib.mkOption { type = lib.types.package; default = - if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform - then sops-install-secrets - else (pkgs.pkgsBuildHost.callPackage ../.. {}).sops-install-secrets; + if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then + sops-install-secrets + else + (pkgs.pkgsBuildHost.callPackage ../.. { }).sops-install-secrets; defaultText = lib.literalExpression "config.sops.package"; description = '' @@ -326,40 +363,78 @@ in { imports = [ ./templates ./secrets-for-users - (lib.mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) - (lib.mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) + (lib.mkRenamedOptionModule + [ + "sops" + "gnupgHome" + ] + [ + "sops" + "gnupg" + "home" + ] + ) + (lib.mkRenamedOptionModule + [ + "sops" + "sshKeyPaths" + ] + [ + "sops" + "gnupg" + "sshKeyPaths" + ] + ) ]; config = lib.mkMerge [ - (lib.mkIf (cfg.secrets != {}) { - assertions = [{ - assertion = cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [] || cfg.age.keyFile != null || cfg.age.sshKeyPaths != []; - message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; - } { - assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); - message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; - }] ++ lib.optionals cfg.validateSopsFiles ( - lib.concatLists (lib.mapAttrsToList (name: secret: [{ - assertion = builtins.pathExists secret.sopsFile; - message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; - } { - assertion = - builtins.isPath secret.sopsFile || - (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); - message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; - } { - assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; - message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; - } { - assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; - message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; - }]) cfg.secrets) - ); + (lib.mkIf (cfg.secrets != { }) { + assertions = + [ + { + assertion = + cfg.gnupg.home != null + || cfg.gnupg.sshKeyPaths != [ ] + || cfg.age.keyFile != null + || cfg.age.sshKeyPaths != [ ]; + message = "No key source configured for sops. Either set services.openssh.enable or set sops.age.keyFile or sops.gnupg.home"; + } + { + assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != [ ]); + message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; + } + ] + ++ lib.optionals cfg.validateSopsFiles ( + lib.concatLists ( + lib.mapAttrsToList (name: secret: [ + { + assertion = builtins.pathExists secret.sopsFile; + message = "Cannot find path '${secret.sopsFile}' set in sops.secrets.${lib.strings.escapeNixIdentifier name}.sopsFile"; + } + { + assertion = + builtins.isPath secret.sopsFile + || (builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir secret.sopsFile); + message = "'${secret.sopsFile}' is not in the Nix store. Either add it to the Nix store or set sops.validateSopsFiles to false"; + } + { + assertion = secret.uid != null && secret.uid != 0 -> secret.owner == null; + message = "In ${secret.name} exactly one of sops.owner and sops.uid must be set"; + } + { + assertion = secret.gid != null && secret.gid != 0 -> secret.group == null; + message = "In ${secret.name} exactly one of sops.group and sops.gid must be set"; + } + ]) cfg.secrets + ) + ); - sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); + sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) ( + lib.mkDefault "${pkgs.gnupg}/bin/gpg" + ); # When using sysusers we no longer are started as an activation script because those are started in initrd while sysusers is started later. systemd.services.sops-install-secrets = lib.mkIf (regularSecrets != { } && useSystemdActivation) { - wantedBy = [ "sysinit.target" ]; + wantedBy = [ "sysinit.target" ]; after = [ "systemd-sysusers.service" ]; environment = cfg.environment; unitConfig.DefaultDependencies = "no"; @@ -372,27 +447,43 @@ in { }; system.activationScripts = { - setupSecrets = lib.mkIf (regularSecrets != {} && !useSystemdActivation) (lib.stringAfter ([ "specialfs" "users" "groups" ] ++ lib.optional cfg.age.generateKey "generate-age-key") '' - [ -e /run/current-system ] || echo setting up secrets... - ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} - '' // lib.optionalAttrs (config.system ? dryActivationScript) { - supportsDryActivation = true; - }); + setupSecrets = lib.mkIf (regularSecrets != { } && !useSystemdActivation) ( + lib.stringAfter + ( + [ + "specialfs" + "users" + "groups" + ] + ++ lib.optional cfg.age.generateKey "generate-age-key" + ) + '' + [ -e /run/current-system ] || echo setting up secrets... + ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets ${manifest}"} + '' + // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + } + ); - generate-age-key = let - escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; - in lib.mkIf cfg.age.generateKey (lib.stringAfter [] '' - if [[ ! -f ${escapedKeyFile} ]]; then - echo generating machine-specific age key... - mkdir -p $(dirname ${escapedKeyFile}) - # age-keygen sets 0600 by default, no need to chmod. - ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} - fi - ''); + generate-age-key = + let + escapedKeyFile = lib.escapeShellArg cfg.age.keyFile; + in + lib.mkIf cfg.age.generateKey ( + lib.stringAfter [ ] '' + if [[ ! -f ${escapedKeyFile} ]]; then + echo generating machine-specific age key... + mkdir -p $(dirname ${escapedKeyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} + fi + '' + ); }; }) { - system.build.sops-nix-manifest = manifest; + system.build.sops-nix-manifest = manifest; } ]; } diff --git a/modules/sops/manifest-for.nix b/modules/sops/manifest-for.nix index de62f08..c4ecea4 100644 --- a/modules/sops/manifest-for.nix +++ b/modules/sops/manifest-for.nix @@ -4,26 +4,31 @@ suffix: secrets: templates: extraJson: writeTextFile { name = "manifest${suffix}.json"; - text = builtins.toJSON ({ - secrets = builtins.attrValues secrets; - templates = builtins.attrValues templates; - # Does this need to be configurable? - secretsMountPoint = "/run/secrets.d"; - symlinkPath = "/run/secrets"; - keepGenerations = cfg.keepGenerations; - gnupgHome = cfg.gnupg.home; - sshKeyPaths = cfg.gnupg.sshKeyPaths; - ageKeyFile = cfg.age.keyFile; - ageSshKeyPaths = cfg.age.sshKeyPaths; - useTmpfs = cfg.useTmpfs; - placeholderBySecretName = cfg.placeholder; - userMode = false; - logging = { - keyImport = builtins.elem "keyImport" cfg.log; - secretChanges = builtins.elem "secretChanges" cfg.log; - }; - } // extraJson); + text = builtins.toJSON ( + { + secrets = builtins.attrValues secrets; + templates = builtins.attrValues templates; + # Does this need to be configurable? + secretsMountPoint = "/run/secrets.d"; + symlinkPath = "/run/secrets"; + keepGenerations = cfg.keepGenerations; + gnupgHome = cfg.gnupg.home; + sshKeyPaths = cfg.gnupg.sshKeyPaths; + ageKeyFile = cfg.age.keyFile; + ageSshKeyPaths = cfg.age.sshKeyPaths; + useTmpfs = cfg.useTmpfs; + placeholderBySecretName = cfg.placeholder; + userMode = false; + logging = { + keyImport = builtins.elem "keyImport" cfg.log; + secretChanges = builtins.elem "secretChanges" cfg.log; + }; + } + // extraJson + ); checkPhase = '' - ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" + ${cfg.validationPackage}/bin/sops-install-secrets -check-mode=${ + if cfg.validateSopsFiles then "sopsfile" else "manifest" + } "$out" ''; } diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix index dc49aa4..cca6a15 100644 --- a/modules/sops/secrets-for-users/default.nix +++ b/modules/sops/secrets-for-users/default.nix @@ -1,8 +1,14 @@ -{ lib, options, config, pkgs, ... }: +{ + lib, + options, + config, + pkgs, + ... +}: let cfg = config.sops; secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; - templatesForUsers = {}; # We do not currently support `neededForUsers` for templates. + templatesForUsers = { }; # We do not currently support `neededForUsers` for templates. manifestFor = pkgs.callPackage ../manifest-for.nix { inherit cfg; inherit (pkgs) writeTextFile; @@ -15,44 +21,54 @@ let symlinkPath = "/run/secrets-for-users"; }; sysusersEnabled = options.systemd ? sysusers && config.systemd.sysusers.enable; - useSystemdActivation = sysusersEnabled || - (options.services ? userborn && config.services.userborn.enable); + useSystemdActivation = + sysusersEnabled || (options.services ? userborn && config.services.userborn.enable); in { - systemd.services.sops-install-secrets-for-users = lib.mkIf (secretsForUsers != { } && useSystemdActivation) { - wantedBy = [ "systemd-sysusers.service" ]; - before = [ "systemd-sysusers.service" ]; - environment = cfg.environment; - unitConfig.DefaultDependencies = "no"; + systemd.services.sops-install-secrets-for-users = + lib.mkIf (secretsForUsers != { } && useSystemdActivation) + { + wantedBy = [ "systemd-sysusers.service" ]; + before = [ "systemd-sysusers.service" ]; + environment = cfg.environment; + unitConfig.DefaultDependencies = "no"; - serviceConfig = { - Type = "oneshot"; - ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ]; - RemainAfterExit = true; - }; - }; + serviceConfig = { + Type = "oneshot"; + ExecStart = [ "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}" ]; + RemainAfterExit = true; + }; + }; system.activationScripts = lib.mkIf (secretsForUsers != { } && !useSystemdActivation) { - setupSecretsForUsers = lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") '' - [ -e /run/current-system ] || echo setting up secrets for users... - ${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"} - '' // lib.optionalAttrs (config.system ? dryActivationScript) { - supportsDryActivation = true; - }; + setupSecretsForUsers = + lib.stringAfter ([ "specialfs" ] ++ lib.optional cfg.age.generateKey "generate-age-key") '' + [ -e /run/current-system ] || echo setting up secrets for users... + ${withEnvironment "${cfg.package}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"} + '' + // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + }; users.deps = [ "setupSecretsForUsers" ]; }; - assertions = [{ - assertion = (lib.filterAttrs (_: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root")) secretsForUsers) == { }; - message = "neededForUsers cannot be used for secrets that are not root-owned"; - } { - assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers; - message = '' - systemd.sysusers.enable in combination with sops.secrets..neededForUsers can only work with config.users.mutableUsers enabled. - See https://github.com/Mic92/sops-nix/issues/475 - ''; - }]; + assertions = [ + { + assertion = + (lib.filterAttrs ( + _: v: (v.uid != 0 && v.owner != "root") || (v.gid != 0 && v.group != "root") + ) secretsForUsers) == { }; + message = "neededForUsers cannot be used for secrets that are not root-owned"; + } + { + assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers; + message = '' + systemd.sysusers.enable in combination with sops.secrets..neededForUsers can only work with config.users.mutableUsers enabled. + See https://github.com/Mic92/sops-nix/issues/475 + ''; + } + ]; system.build.sops-nix-users-manifest = manifestForUsers; } diff --git a/modules/sops/templates/default.nix b/modules/sops/templates/default.nix index 97952d2..fb3fb2c 100644 --- a/modules/sops/templates/default.nix +++ b/modules/sops/templates/default.nix @@ -1,108 +1,123 @@ -{ config, pkgs, lib, options, ... }: +{ + config, + pkgs, + lib, + options, + ... +}: let inherit (lib) mkOption mkDefault mapAttrs types - ; + ; users = config.users.users; -in { +in +{ options.sops = { templates = mkOption { description = "Templates for secret files"; - type = types.attrsOf (types.submodule ({ config, ... }: { - options = { - name = mkOption { - type = types.singleLineStr; - default = config._module.args.name; - description = '' - Name of the file used in /run/secrets/rendered - ''; - }; - path = mkOption { - description = "Path where the rendered file will be placed"; - type = types.singleLineStr; - # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` - default = "/run/secrets/rendered/${config.name}"; - }; - content = mkOption { - type = types.lines; - default = ""; - description = '' - Content of the file - ''; - }; - mode = mkOption { - type = types.singleLineStr; - default = "0400"; - description = '' - Permissions mode of the rendered secret file in octal. - ''; - }; - owner = mkOption { - type = types.singleLineStr; - default = "root"; - description = '' - User of the file. - ''; - }; - group = mkOption { - type = types.singleLineStr; - default = users.${config.owner}.group; - defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group''; - description = '' - Group of the file. - ''; - }; - file = mkOption { - type = types.path; - default = pkgs.writeText config.name config.content; - defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; - example = "./configuration-template.conf"; - description = '' - File used as the template. When this value is specified, `sops.templates..content` is ignored. - ''; - }; - restartUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be restarted when the rendered template changes. - This works the same way as . - ''; - }; - reloadUnits = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "sshd.service" ]; - description = '' - Names of units that should be reloaded when the rendered template changes. - This works the same way as . - ''; - }; - }; - })); + type = types.attrsOf ( + types.submodule ( + { config, ... }: + { + options = { + name = mkOption { + type = types.singleLineStr; + default = config._module.args.name; + description = '' + Name of the file used in /run/secrets/rendered + ''; + }; + path = mkOption { + description = "Path where the rendered file will be placed"; + type = types.singleLineStr; + # Keep this in sync with `RenderedSubdir` in `pkgs/sops-install-secrets/main.go` + default = "/run/secrets/rendered/${config.name}"; + }; + content = mkOption { + type = types.lines; + default = ""; + description = '' + Content of the file + ''; + }; + mode = mkOption { + type = types.singleLineStr; + default = "0400"; + description = '' + Permissions mode of the rendered secret file in octal. + ''; + }; + owner = mkOption { + type = types.singleLineStr; + default = "root"; + description = '' + User of the file. + ''; + }; + group = mkOption { + type = types.singleLineStr; + default = users.${config.owner}.group; + defaultText = lib.literalExpression ''config.users.users.''${cfg.owner}.group''; + description = '' + Group of the file. + ''; + }; + file = mkOption { + type = types.path; + default = pkgs.writeText config.name config.content; + defaultText = lib.literalExpression ''pkgs.writeText config.name config.content''; + example = "./configuration-template.conf"; + description = '' + File used as the template. When this value is specified, `sops.templates..content` is ignored. + ''; + }; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be restarted when the rendered template changes. + This works the same way as . + ''; + }; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "sshd.service" ]; + description = '' + Names of units that should be reloaded when the rendered template changes. + This works the same way as . + ''; + }; + }; + } + ) + ); default = { }; }; placeholder = mkOption { - type = types.attrsOf (types.mkOptionType { - name = "coercibleToString"; - description = "value that can be coerced to string"; - check = lib.strings.isConvertibleWithToString; - merge = lib.mergeEqualOption; - }); + type = types.attrsOf ( + types.mkOptionType { + name = "coercibleToString"; + description = "value that can be coerced to string"; + check = lib.strings.isConvertibleWithToString; + merge = lib.mergeEqualOption; + } + ); default = { }; visible = false; }; }; - config = lib.optionalAttrs (options ? sops.secrets) - (lib.mkIf (config.sops.templates != { }) { - sops.placeholder = mapAttrs - (name: _: mkDefault "") - config.sops.secrets; - }); + config = lib.optionalAttrs (options ? sops.secrets) ( + lib.mkIf (config.sops.templates != { }) { + sops.placeholder = mapAttrs ( + name: _: mkDefault "" + ) config.sops.secrets; + } + ); } diff --git a/modules/sops/with-environment.nix b/modules/sops/with-environment.nix index d19d5fd..1306e97 100644 --- a/modules/sops/with-environment.nix +++ b/modules/sops/with-environment.nix @@ -2,11 +2,12 @@ sopsCall: -if cfg.environment == {} then +if cfg.environment == { } then sopsCall -else '' - ( - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} - ${sopsCall} - ) -'' +else + '' + ( + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} + ${sopsCall} + ) + '' diff --git a/pkgs/sops-import-keys-hook/default.nix b/pkgs/sops-import-keys-hook/default.nix index 1e6d10d..940968c 100644 --- a/pkgs/sops-import-keys-hook/default.nix +++ b/pkgs/sops-import-keys-hook/default.nix @@ -1,13 +1,25 @@ -{ makeSetupHook, gnupg, sops, lib }: +{ + makeSetupHook, + gnupg, + sops, + lib, +}: let # FIXME: drop after 23.05 - propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs"; + propagatedBuildInputs = + if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then + "deps" + else + "propagatedBuildInputs"; in (makeSetupHook { name = "sops-import-keys-hook"; substitutions = { gpg = "${gnupg}/bin/gpg"; }; - ${propagatedBuildInputs} = [ sops gnupg ]; + ${propagatedBuildInputs} = [ + sops + gnupg + ]; } ./sops-import-keys-hook.bash) diff --git a/pkgs/sops-import-keys-hook/test-assets/shell.nix b/pkgs/sops-import-keys-hook/test-assets/shell.nix index 88c4b25..6c8fec6 100644 --- a/pkgs/sops-import-keys-hook/test-assets/shell.nix +++ b/pkgs/sops-import-keys-hook/test-assets/shell.nix @@ -1,5 +1,5 @@ # shell.nix -with import {}; +with import { }; mkShell { sopsPGPKeyDirs = [ "./keys" @@ -10,6 +10,6 @@ mkShell { ]; sopsCreateGPGHome = "1"; nativeBuildInputs = [ - (pkgs.callPackage ../../.. {}).sops-import-keys-hook + (pkgs.callPackage ../../.. { }).sops-import-keys-hook ]; } diff --git a/pkgs/sops-init-gpg-key/default.nix b/pkgs/sops-init-gpg-key/default.nix index 9e0ed1a..1860aae 100644 --- a/pkgs/sops-init-gpg-key/default.nix +++ b/pkgs/sops-init-gpg-key/default.nix @@ -1,4 +1,12 @@ -{ stdenv, lib, makeWrapper, gnupg, coreutils, util-linux, unixtools }: +{ + stdenv, + lib, + makeWrapper, + gnupg, + coreutils, + util-linux, + unixtools, +}: stdenv.mkDerivation { name = "sops-init-gpg-key"; @@ -11,9 +19,14 @@ stdenv.mkDerivation { installPhase = '' install -m755 -D $src $out/bin/sops-init-gpg-key wrapProgram $out/bin/sops-init-gpg-key \ - --prefix PATH : ${lib.makeBinPath [ - coreutils util-linux gnupg unixtools.hostname - ]} + --prefix PATH : ${ + lib.makeBinPath [ + coreutils + util-linux + gnupg + unixtools.hostname + ] + } ''; doInstallCheck = true; diff --git a/pkgs/sops-install-secrets/default.nix b/pkgs/sops-install-secrets/default.nix index fbe1fbd..1badfb6 100644 --- a/pkgs/sops-install-secrets/default.nix +++ b/pkgs/sops-install-secrets/default.nix @@ -1,9 +1,20 @@ -{ lib, buildGoModule, stdenv, vendorHash, go, callPackages }: +{ + lib, + buildGoModule, + stdenv, + vendorHash, + go, + callPackages, +}: buildGoModule { pname = "sops-install-secrets"; version = "0.0.1"; - src = lib.sourceByRegex ../.. [ "go\.(mod|sum)" "pkgs" "pkgs/sops-install-secrets.*" ]; + src = lib.sourceByRegex ../.. [ + "go\.(mod|sum)" + "pkgs" + "pkgs/sops-install-secrets.*" + ]; subPackages = [ "pkgs/sops-install-secrets" ]; @@ -12,19 +23,20 @@ buildGoModule { passthru.tests = callPackages ./nixos-test.nix { }; - outputs = [ "out" ] ++ - lib.lists.optionals (stdenv.isLinux) [ "unittest" ]; + outputs = [ "out" ] ++ lib.lists.optionals (stdenv.isLinux) [ "unittest" ]; - postInstall = '' - go test -c ./pkgs/sops-install-secrets - '' + lib.optionalString (stdenv.isLinux) '' - # *.test is only tested on linux. $unittest does not exist on darwin. - install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test - # newer versions of nixpkgs no longer require this step - if command -v remove-references-to; then - remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test - fi - ''; + postInstall = + '' + go test -c ./pkgs/sops-install-secrets + '' + + lib.optionalString (stdenv.isLinux) '' + # *.test is only tested on linux. $unittest does not exist on darwin. + install -D ./sops-install-secrets.test $unittest/bin/sops-install-secrets.test + # newer versions of nixpkgs no longer require this step + if command -v remove-references-to; then + remove-references-to -t ${go} $unittest/bin/sops-install-secrets.test + fi + ''; inherit vendorHash; diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index dc246b6..4fcbfeb 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -1,64 +1,75 @@ { lib, testers }: let - userPasswordTest = name: extraConfig: testers.runNixOSTest { - inherit name; - nodes.machine = { config, lib, ... }: { - imports = [ - ../../modules/sops - extraConfig - ]; - sops = { - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key.neededForUsers = true; - secrets."nested/test/file".owner = "example-user"; - }; - system.switch.enable = true; + userPasswordTest = + name: extraConfig: + testers.runNixOSTest { + inherit name; + nodes.machine = + { config, lib, ... }: + { + imports = [ + ../../modules/sops + extraConfig + ]; + sops = { + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key.neededForUsers = true; + secrets."nested/test/file".owner = "example-user"; + }; + system.switch.enable = true; - users.users.example-user = lib.mkMerge [ - (lib.mkIf (! config.systemd.sysusers.enable) { - isNormalUser = true; - hashedPasswordFile = config.sops.secrets.test_key.path; - }) - (lib.mkIf config.systemd.sysusers.enable { - isSystemUser = true; - group = "users"; - hashedPasswordFile = config.sops.secrets.test_key.path; - }) - ]; + users.users.example-user = lib.mkMerge [ + (lib.mkIf (!config.systemd.sysusers.enable) { + isNormalUser = true; + hashedPasswordFile = config.sops.secrets.test_key.path; + }) + (lib.mkIf config.systemd.sysusers.enable { + isSystemUser = true; + group = "users"; + hashedPasswordFile = config.sops.secrets.test_key.path; + }) + ]; + }; + + testScript = + '' + start_all() + machine.wait_for_unit("multi-user.target") + + machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set + machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work... + user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned... + assert user == "example-user", f"Expected 'example-user', got '{user}'" + machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists + + # BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test + '' + + lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) '' + machine.succeed("/run/current-system/bin/switch-to-configuration test") + machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch + machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch + ''; }; - - testScript = '' - start_all() - machine.wait_for_unit("multi-user.target") - - machine.succeed("getent shadow example-user | grep -q :test_value:") # password was set - machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # regular secrets work... - user = machine.succeed("stat -c%U /run/secrets/nested/test/file").strip() # ...and are owned... - assert user == "example-user", f"Expected 'example-user', got '{user}'" - machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password still exists - - # BUG in nixos's overlayfs... systemd crashes on switch-to-configuration test - '' + lib.optionalString (!(extraConfig ? system.etc.overlay.enable)) '' - machine.succeed("/run/current-system/bin/switch-to-configuration test") - machine.succeed("cat /run/secrets/nested/test/file | grep -q 'another value'") # the regular secrets still work after a switch - machine.succeed("cat /run/secrets-for-users/test_key | grep -q 'test_value'") # the user password is still present after a switch - ''; - }; -in { +in +{ ssh-keys = testers.runNixOSTest { name = "sops-ssh-keys"; - nodes.server = { ... }: { - imports = [ ../../modules/sops ]; - services.openssh.enable = true; - services.openssh.hostKeys = [{ - type = "rsa"; - bits = 4096; - path = ./test-assets/ssh-key; - }]; - sops.defaultSopsFile = ./test-assets/secrets.yaml; - sops.secrets.test_key = { }; - }; + nodes.server = + { ... }: + { + imports = [ ../../modules/sops ]; + services.openssh.enable = true; + services.openssh.hostKeys = [ + { + type = "rsa"; + bits = 4096; + path = ./test-assets/ssh-key; + } + ]; + sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.secrets.test_key = { }; + }; testScript = '' start_all() @@ -68,24 +79,26 @@ in { pruning = testers.runNixOSTest { name = "sops-pruning"; - nodes.machine = { lib, ... }: { - imports = [ ../../modules/sops ]; - sops = { - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key = { }; - keepGenerations = lib.mkDefault 0; + nodes.machine = + { lib, ... }: + { + imports = [ ../../modules/sops ]; + sops = { + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key = { }; + keepGenerations = lib.mkDefault 0; + }; + + # must run before sops sets up keys + boot.initrd.postDeviceCommands = '' + cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + chmod -R 700 /run/age-keys.txt + ''; + + specialisation.pruning.configuration.sops.keepGenerations = 10; }; - # must run before sops sets up keys - boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - - specialisation.pruning.configuration.sops.keepGenerations = 10; - }; - testScript = '' # Force us to generation 100 machine.succeed("mkdir /run/secrets.d/{2..99} /run/secrets.d/non-numeric") @@ -112,49 +125,51 @@ in { age-keys = testers.runNixOSTest { name = "sops-age-keys"; - nodes.machine = { config, ... }: { - imports = [ ../../modules/sops ]; - sops = { - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; - secrets = { - test_key = { }; + nodes.machine = + { config, ... }: + { + imports = [ ../../modules/sops ]; + sops = { + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets = { + test_key = { }; - test_key_someuser_somegroup = { - uid = config.users.users."someuser".uid; - gid = config.users.groups."somegroup".gid; - key = "test_key"; - }; - test_key_someuser_root = { - uid = config.users.users."someuser".uid; - key = "test_key"; - }; - test_key_root_root = { - key = "test_key"; - }; - test_key_1001_1001 = { - uid = 1001; - gid = 1001; - key = "test_key"; + test_key_someuser_somegroup = { + uid = config.users.users."someuser".uid; + gid = config.users.groups."somegroup".gid; + key = "test_key"; + }; + test_key_someuser_root = { + uid = config.users.users."someuser".uid; + key = "test_key"; + }; + test_key_root_root = { + key = "test_key"; + }; + test_key_1001_1001 = { + uid = 1001; + gid = 1001; + key = "test_key"; + }; }; }; - }; - users.users."someuser" = { - uid = 1000; - group = "somegroup"; - isNormalUser = true; - }; - users.groups."somegroup" = { - gid = 1000; - }; + users.users."someuser" = { + uid = 1000; + group = "somegroup"; + isNormalUser = true; + }; + users.groups."somegroup" = { + gid = 1000; + }; - # must run before sops sets up keys - boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - }; + # must run before sops sets up keys + boot.initrd.postDeviceCommands = '' + cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + chmod -R 700 /run/age-keys.txt + ''; + }; testScript = '' start_all() @@ -183,10 +198,12 @@ in { nodes.machine = { imports = [ ../../modules/sops ]; services.openssh.enable = true; - services.openssh.hostKeys = [{ - type = "ed25519"; - path = ./test-assets/ssh-ed25519-key; - }]; + services.openssh.hostKeys = [ + { + type = "ed25519"; + path = ./test-assets/ssh-ed25519-key; + } + ]; sops = { defaultSopsFile = ./test-assets/secrets.yaml; @@ -207,37 +224,39 @@ in { pgp-keys = testers.runNixOSTest { name = "sops-pgp-keys"; - nodes.server = { lib, config, ... }: { - imports = [ ../../modules/sops ]; + nodes.server = + { lib, config, ... }: + { + imports = [ ../../modules/sops ]; - users.users.someuser = { - isSystemUser = true; - group = "nogroup"; + users.users.someuser = { + isSystemUser = true; + group = "nogroup"; + }; + + sops.gnupg.home = "/run/gpghome"; + sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.secrets.test_key.owner = config.users.users.someuser.name; + sops.secrets."nested/test/file".owner = config.users.users.someuser.name; + sops.secrets.existing-file = { + key = "test_key"; + path = "/run/existing-file"; + }; + # must run before sops + system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] '' + cp -r ${./test-assets/gnupghome} /run/gpghome + chmod -R 700 /run/gpghome + + touch /run/existing-file + ''; + # Useful for debugging + #environment.systemPackages = [ pkgs.gnupg pkgs.sops ]; + #environment.variables = { + # GNUPGHOME = "/run/gpghome"; + # SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg"; + # SOPSFILE = "${./test-assets/secrets.yaml}"; + #}; }; - - sops.gnupg.home = "/run/gpghome"; - sops.defaultSopsFile = ./test-assets/secrets.yaml; - sops.secrets.test_key.owner = config.users.users.someuser.name; - sops.secrets."nested/test/file".owner = config.users.users.someuser.name; - sops.secrets.existing-file = { - key = "test_key"; - path = "/run/existing-file"; - }; - # must run before sops - system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] '' - cp -r ${./test-assets/gnupghome} /run/gpghome - chmod -R 700 /run/gpghome - - touch /run/existing-file - ''; - # Useful for debugging - #environment.systemPackages = [ pkgs.gnupg pkgs.sops ]; - #environment.variables = { - # GNUPGHOME = "/run/gpghome"; - # SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg"; - # SOPSFILE = "${./test-assets/secrets.yaml}"; - #}; - }; testScript = '' def assertEqual(exp: str, act: str) -> None: if exp != act: @@ -260,47 +279,49 @@ in { templates = testers.runNixOSTest { name = "sops-templates"; - nodes.machine = { config, ... }: { - imports = [ ../../modules/sops ]; - sops = { - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key = { }; + nodes.machine = + { config, ... }: + { + imports = [ ../../modules/sops ]; + sops = { + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key = { }; - # Verify that things work even with `neededForUsers` secrets. See - # . - secrets."nested/test/file".neededForUsers = true; - }; + # Verify that things work even with `neededForUsers` secrets. See + # . + secrets."nested/test/file".neededForUsers = true; + }; - # must run before sops sets up keys - boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - - sops.templates.test_template = { - content = '' - This line is not modified. - The next value will be replaced by ${config.sops.placeholder.test_key} - This line is also not modified. + # must run before sops sets up keys + boot.initrd.postDeviceCommands = '' + cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + chmod -R 700 /run/age-keys.txt ''; - mode = "0400"; - owner = "someuser"; - group = "somegroup"; - }; - sops.templates.test_default = { - content = '' - Test value: ${config.sops.placeholder.test_key} - ''; - path = "/etc/externally/linked"; - }; - users.groups.somegroup = {}; - users.users.someuser = { - isSystemUser = true; - group = "somegroup"; + sops.templates.test_template = { + content = '' + This line is not modified. + The next value will be replaced by ${config.sops.placeholder.test_key} + This line is also not modified. + ''; + mode = "0400"; + owner = "someuser"; + group = "somegroup"; + }; + sops.templates.test_default = { + content = '' + Test value: ${config.sops.placeholder.test_key} + ''; + path = "/etc/externally/linked"; + }; + + users.groups.somegroup = { }; + users.users.someuser = { + isSystemUser = true; + group = "somegroup"; + }; }; - }; testScript = '' def assertEqual(exp: str, act: str) -> None: @@ -337,62 +358,72 @@ in { restart-and-reload = testers.runNixOSTest { name = "sops-restart-and-reload"; - nodes.machine = {config, ...}: { - imports = [ ../../modules/sops ]; + nodes.machine = + { config, ... }: + { + imports = [ ../../modules/sops ]; - sops = { - age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key = { - restartUnits = [ "restart-unit.service" "reload-unit.service" ]; - reloadUnits = [ "reload-trigger.service" ]; + sops = { + age.keyFile = "/run/age-keys.txt"; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key = { + restartUnits = [ + "restart-unit.service" + "reload-unit.service" + ]; + reloadUnits = [ "reload-trigger.service" ]; + }; + + templates.test_template = { + content = '' + this is a template with + a secret: ${config.sops.placeholder.test_key} + ''; + restartUnits = [ + "restart-unit.service" + "reload-unit.service" + ]; + reloadUnits = [ "reload-trigger.service" ]; + }; + }; + system.switch.enable = true; + + # must run before sops sets up keys + boot.initrd.postDeviceCommands = '' + cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + chmod -R 700 /run/age-keys.txt + ''; + + systemd.services."restart-unit" = { + description = "Restart unit"; + # not started on boot + serviceConfig = { + ExecStart = "/bin/sh -c 'echo ok > /restarted'"; + }; + }; + systemd.services."reload-unit" = { + description = "Reload unit"; + wantedBy = [ "multi-user.target" ]; + reloadIfChanged = true; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "/bin/sh -c true"; + ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; + }; + }; + systemd.services."reload-trigger" = { + description = "Reload trigger unit"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "/bin/sh -c true"; + ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; + }; }; - templates.test_template = { - content = '' - this is a template with - a secret: ${config.sops.placeholder.test_key} - ''; - restartUnits = [ "restart-unit.service" "reload-unit.service" ]; - reloadUnits = [ "reload-trigger.service" ]; - }; }; - system.switch.enable = true; - - # must run before sops sets up keys - boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - - systemd.services."restart-unit" = { - description = "Restart unit"; - # not started on boot - serviceConfig = { ExecStart = "/bin/sh -c 'echo ok > /restarted'"; }; - }; - systemd.services."reload-unit" = { - description = "Reload unit"; - wantedBy = [ "multi-user.target" ]; - reloadIfChanged = true; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = "/bin/sh -c true"; - ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; - }; - }; - systemd.services."reload-trigger" = { - description = "Reload trigger unit"; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStart = "/bin/sh -c true"; - ExecReload = "/bin/sh -c 'echo ok > /reloaded'"; - }; - }; - - }; testScript = '' def assertOutput(output, *expected_lines): expected_lines = list(expected_lines) @@ -524,32 +555,40 @@ in { chmod -R 700 /run/age-keys.txt ''; }; -} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") { - user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" ({ pkgs, ... }: { - systemd.sysusers.enable = true; - users.mutableUsers = true; - system.etc.overlay.enable = true; - boot.initrd.systemd.enable = true; - boot.kernelPackages = pkgs.linuxPackages_latest; - - # must run before sops sets up keys - systemd.services."sops-install-secrets-for-users".preStart = '' - printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - }); -} // lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") { - user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" ({ pkgs, ... }: { - services.userborn.enable = true; - users.mutableUsers = false; - system.etc.overlay.enable = true; - boot.initrd.systemd.enable = true; - boot.kernelPackages = pkgs.linuxPackages_latest; - - # must run before sops sets up keys - systemd.services."sops-install-secrets-for-users".preStart = '' - printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt - chmod -R 700 /run/age-keys.txt - ''; - }); +} +// lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.05") { + user-passwords-sysusers = userPasswordTest "sops-user-passwords-sysusers" ( + { pkgs, ... }: + { + systemd.sysusers.enable = true; + users.mutableUsers = true; + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + # must run before sops sets up keys + systemd.services."sops-install-secrets-for-users".preStart = '' + printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt + chmod -R 700 /run/age-keys.txt + ''; + } + ); +} +// lib.optionalAttrs (lib.versionAtLeast (lib.versions.majorMinor lib.version) "24.11") { + user-passwords-userborn = userPasswordTest "sops-user-passwords-userborn" ( + { pkgs, ... }: + { + services.userborn.enable = true; + users.mutableUsers = false; + system.etc.overlay.enable = true; + boot.initrd.systemd.enable = true; + boot.kernelPackages = pkgs.linuxPackages_latest; + + # must run before sops sets up keys + systemd.services."sops-install-secrets-for-users".preStart = '' + printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt + chmod -R 700 /run/age-keys.txt + ''; + } + ); } diff --git a/pkgs/sops-install-secrets/shell.nix b/pkgs/sops-install-secrets/shell.nix index 59b0edf..10e4e81 100644 --- a/pkgs/sops-install-secrets/shell.nix +++ b/pkgs/sops-install-secrets/shell.nix @@ -1,4 +1,11 @@ -{ pkgs ? import {} }: +{ + pkgs ? import { }, +}: pkgs.mkShell { - nativeBuildInputs = with pkgs; [ go delve util-linux gnupg ]; + nativeBuildInputs = with pkgs; [ + go + delve + util-linux + gnupg + ]; } diff --git a/pkgs/sops-pgp-hook/default.nix b/pkgs/sops-pgp-hook/default.nix index b088d39..300b3c4 100644 --- a/pkgs/sops-pgp-hook/default.nix +++ b/pkgs/sops-pgp-hook/default.nix @@ -1,13 +1,25 @@ -{ makeSetupHook, gnupg, sops, lib }: +{ + makeSetupHook, + gnupg, + sops, + lib, +}: let # FIXME: drop after 23.05 - propagatedBuildInputs = if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then "deps" else "propagatedBuildInputs"; + propagatedBuildInputs = + if (lib.versionOlder (lib.versions.majorMinor lib.version) "23.05") then + "deps" + else + "propagatedBuildInputs"; in (makeSetupHook { name = "sops-pgp-hook"; substitutions = { gpg = "${gnupg}/bin/gpg"; }; - ${propagatedBuildInputs} = [ sops gnupg ]; + ${propagatedBuildInputs} = [ + sops + gnupg + ]; } ./sops-pgp-hook.bash) diff --git a/pkgs/sops-pgp-hook/test-assets/shell.nix b/pkgs/sops-pgp-hook/test-assets/shell.nix index 628a019..71173fd 100644 --- a/pkgs/sops-pgp-hook/test-assets/shell.nix +++ b/pkgs/sops-pgp-hook/test-assets/shell.nix @@ -1,5 +1,5 @@ # shell.nix -with import {}; +with import { }; mkShell { sopsPGPKeyDirs = [ "./keys" @@ -9,6 +9,6 @@ mkShell { "./non-existing-key.gpg" ]; nativeBuildInputs = [ - (pkgs.callPackage ../../.. {}).sops-pgp-hook + (pkgs.callPackage ../../.. { }).sops-pgp-hook ]; } diff --git a/pkgs/unit-tests.nix b/pkgs/unit-tests.nix index dce7294..f3d3678 100644 --- a/pkgs/unit-tests.nix +++ b/pkgs/unit-tests.nix @@ -1,16 +1,21 @@ -{ pkgs ? import {} +{ + pkgs ? import { }, }: let sopsPkgs = import ../. { inherit pkgs; }; -in pkgs.stdenv.mkDerivation { +in +pkgs.stdenv.mkDerivation { name = "env"; - nativeBuildInputs = with pkgs; [ - bashInteractive - gnupg - util-linux - nix - sopsPkgs.sops-pgp-hook-test - ] ++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest; + nativeBuildInputs = + with pkgs; + [ + bashInteractive + gnupg + util-linux + nix + sopsPkgs.sops-pgp-hook-test + ] + ++ pkgs.lib.optional (pkgs.stdenv.isLinux) sopsPkgs.sops-install-secrets.unittest; # allow to prefetch shell dependencies in build phase dontUnpack = true; installPhase = '' diff --git a/shell.nix b/shell.nix index 11bb906..30505fe 100644 --- a/shell.nix +++ b/shell.nix @@ -1,4 +1,6 @@ -{ pkgs ? import {} }: +{ + pkgs ? import { }, +}: pkgs.mkShell { nativeBuildInputs = with pkgs; [ bashInteractive From d76a2f002f46ed68a061961c063aeb1c47e040f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 12:23:34 +0100 Subject: [PATCH 39/53] nix-darwin: remove unused variable --- modules/nix-darwin/secrets-for-users/default.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/nix-darwin/secrets-for-users/default.nix b/modules/nix-darwin/secrets-for-users/default.nix index 585416a..b00395a 100644 --- a/modules/nix-darwin/secrets-for-users/default.nix +++ b/modules/nix-darwin/secrets-for-users/default.nix @@ -1,6 +1,5 @@ { lib, - options, config, pkgs, ... From 77697276340fbfa7244cafb58ffe3b28e1002572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 12:42:28 +0100 Subject: [PATCH 40/53] move nixpkgs-stable to private flake inputs now with home-manager and nix-darwin tests, we don't want to increase the number of dependencies a user has to override in their flake.lock. --- dev/private.narHash | 1 + dev/private/flake.lock | 48 +++++++++++++++++++++ dev/private/flake.nix | 9 ++++ flake.lock | 19 +------- flake.nix | 98 ++++++++++++++++++++++++------------------ shell.nix | 5 +++ 6 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 dev/private.narHash create mode 100644 dev/private/flake.lock create mode 100644 dev/private/flake.nix diff --git a/dev/private.narHash b/dev/private.narHash new file mode 100644 index 0000000..b5ccbc3 --- /dev/null +++ b/dev/private.narHash @@ -0,0 +1 @@ +sha256-qF9EiqHqJARLtA+ZABXa2mstgbza762DwoGEIGkyqVY= \ No newline at end of file diff --git a/dev/private/flake.lock b/dev/private/flake.lock new file mode 100644 index 0000000..eb3008e --- /dev/null +++ b/dev/private/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "nixpkgs-stable": { + "locked": { + "lastModified": 1731842749, + "narHash": "sha256-aNc8irVBH7sM5cGDvqdOueg8S+fGakf0rEMRGfGwWZw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bf6132dc791dbdff8b6894c3a85eb27ad8255682", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs-stable": "nixpkgs-stable", + "treefmt-nix": "treefmt-nix" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs-stable" + ] + }, + "locked": { + "lastModified": 1730321837, + "narHash": "sha256-vK+a09qq19QNu2MlLcvN4qcRctJbqWkX7ahgPZ/+maI=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "746901bb8dba96d154b66492a29f5db0693dbfcc", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/dev/private/flake.nix b/dev/private/flake.nix new file mode 100644 index 0000000..e3a4865 --- /dev/null +++ b/dev/private/flake.nix @@ -0,0 +1,9 @@ +{ + description = "private inputs"; + inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05"; + + inputs.treefmt-nix.url = "github:numtide/treefmt-nix"; + inputs.treefmt-nix.inputs.nixpkgs.follows = "nixpkgs-stable"; + + outputs = _: { }; +} diff --git a/flake.lock b/flake.lock index 01c146b..eda6a5f 100644 --- a/flake.lock +++ b/flake.lock @@ -16,26 +16,9 @@ "type": "github" } }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1731797254, - "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "release-24.05", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { - "nixpkgs": "nixpkgs", - "nixpkgs-stable": "nixpkgs-stable" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 2777d50..cb212ea 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { description = "Integrates sops into nixos"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-24.05"; + nixConfig.extra-substituters = [ "https://cache.thalheim.io" ]; nixConfig.extra-trusted-public-keys = [ "cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc=" @@ -10,21 +10,41 @@ { self, nixpkgs, - nixpkgs-stable, - }: + }@inputs: let + loadPrivateFlake = + path: + let + flakeHash = builtins.readFile "${toString path}.narHash"; + flakePath = "path:${toString path}?narHash=${flakeHash}"; + in + builtins.getFlake (builtins.unsafeDiscardStringContext flakePath); + + privateFlake = loadPrivateFlake ./dev/private; + + privateInputs = privateFlake.inputs; + systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux" ]; - forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); - suffix-version = - version: attrs: - nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; - suffix-stable = suffix-version "-24_05"; + + eachSystem = + f: + builtins.listToAttrs ( + builtins.map (system: { + name = system; + value = f { + pkgs = inputs.nixpkgs.legacyPackages.${system}; + inherit system; + }; + }) systems + ); + in + # public outputs { overlays.default = final: prev: @@ -52,39 +72,33 @@ sops = ./modules/nix-darwin; default = self.darwinModules.sops; }; - packages = forAllSystems ( - system: - import ./default.nix { - pkgs = import nixpkgs { inherit system; }; - } - ); - checks = - nixpkgs.lib.genAttrs - [ - "x86_64-linux" - "aarch64-linux" - ] - ( - system: - let - tests = self.packages.${system}.sops-install-secrets.tests; - packages-stable = import ./default.nix { - pkgs = import nixpkgs-stable { inherit system; }; - }; - tests-stable = packages-stable.sops-install-secrets.tests; - in - tests // (suffix-stable tests-stable) // (suffix-stable packages-stable) - ); + packages = eachSystem ({ pkgs, ... }: import ./default.nix { inherit pkgs; }); + } + // + # dev outputs + { + checks = eachSystem ( + { system, ... }: + let + tests = self.packages.${system}.sops-install-secrets.tests; + packages-stable = import ./default.nix { + pkgs = privateInputs.nixpkgs-stable.legacyPackages.${system}; + }; + tests-stable = packages-stable.sops-install-secrets.tests; + suffix-version = + version: attrs: + nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; + suffix-stable = suffix-version "-24_05"; + in + tests // (suffix-stable tests-stable) // (suffix-stable packages-stable) + ); - devShells = forAllSystems ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in - { - unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; - default = pkgs.callPackage ./shell.nix { }; - } - ); - }; + devShells = eachSystem ( + { pkgs, ... }: + { + unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; + default = pkgs.callPackage ./shell.nix { }; + } + ); + }; } diff --git a/shell.nix b/shell.nix index 30505fe..02083a6 100644 --- a/shell.nix +++ b/shell.nix @@ -10,6 +10,11 @@ pkgs.mkShell { util-linux nix golangci-lint + + (pkgs.writeScriptBin "update-dev-private-narHash" '' + nix --extra-experimental-features "nix-command flakes" flake lock ./dev/private + nix --extra-experimental-features "nix-command flakes" hash path ./dev/private | tr -d '\n' > ./dev/private.narHash + '') ]; # delve does not compile with hardening enabled hardeningDisable = [ "all" ]; From 5f3869dfd2cdaf78d0fe48d0ac10949ee7534a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 13:02:33 +0100 Subject: [PATCH 41/53] update github action to also update private flake --- .github/workflows/upgrade-flakes.yml | 19 +++++++++++++------ flake.nix | 15 ++++++++++++++- shell.nix | 5 ----- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.github/workflows/upgrade-flakes.yml b/.github/workflows/upgrade-flakes.yml index 6fac218..ed3d891 100644 --- a/.github/workflows/upgrade-flakes.yml +++ b/.github/workflows/upgrade-flakes.yml @@ -4,6 +4,10 @@ on: workflow_dispatch: schedule: - cron: '51 2 * * 0' + +permissions: + pull-requests: write + jobs: createPullRequest: runs-on: ubuntu-latest @@ -14,9 +18,12 @@ jobs: with: extra_nix_config: | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - name: Update flake.lock - uses: DeterminateSystems/update-flake-lock@v24 - with: - token: ${{ secrets.GH_TOKEN_FOR_UPDATES }} - pr-labels: | - merge-queue + - name: Update flakes + run: | + nix flake update + pushd dev/private + nix flake update + popd + nix run .#update-dev-private-narHash + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 diff --git a/flake.nix b/flake.nix index cb212ea..9e4da72 100644 --- a/flake.nix +++ b/flake.nix @@ -93,11 +93,24 @@ tests // (suffix-stable tests-stable) // (suffix-stable packages-stable) ); + apps = eachSystem ( + { pkgs, ... }: + { + update-dev-private-narHash = { + type = "app"; + program = "${pkgs.writeShellScript "update-dev-private-narHash" '' + nix --extra-experimental-features "nix-command flakes" flake lock ./dev/private + nix --extra-experimental-features "nix-command flakes" hash path ./dev/private | tr -d '\n' > ./dev/private.narHash + ''}"; + }; + } + ); + devShells = eachSystem ( { pkgs, ... }: { unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; - default = pkgs.callPackage ./shell.nix { }; + default = pkgs.callPackage ./shell.nix {}; } ); }; diff --git a/shell.nix b/shell.nix index 02083a6..30505fe 100644 --- a/shell.nix +++ b/shell.nix @@ -10,11 +10,6 @@ pkgs.mkShell { util-linux nix golangci-lint - - (pkgs.writeScriptBin "update-dev-private-narHash" '' - nix --extra-experimental-features "nix-command flakes" flake lock ./dev/private - nix --extra-experimental-features "nix-command flakes" hash path ./dev/private | tr -d '\n' > ./dev/private.narHash - '') ]; # delve does not compile with hardening enabled hardeningDisable = [ "all" ]; From dfcebb55c8e8165208c4a802335353d2e7517341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 13:14:19 +0100 Subject: [PATCH 42/53] only export nixos tests on Linux --- pkgs/sops-install-secrets/default.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkgs/sops-install-secrets/default.nix b/pkgs/sops-install-secrets/default.nix index 1badfb6..52025b0 100644 --- a/pkgs/sops-install-secrets/default.nix +++ b/pkgs/sops-install-secrets/default.nix @@ -21,7 +21,9 @@ buildGoModule { # requires root privileges for tests doCheck = false; - passthru.tests = callPackages ./nixos-test.nix { }; + passthru = { + tests = lib.optionalAttrs stdenv.isLinux (callPackages ./nixos-test.nix { }); + }; outputs = [ "out" ] ++ lib.lists.optionals (stdenv.isLinux) [ "unittest" ]; From fe6a1bb9229253f2f7b8405bb87f57543e423ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 13:51:11 +0100 Subject: [PATCH 43/53] add home-manager and sops-nix to ci --- checks/darwin.nix | 11 +++++++++++ checks/home-manager.nix | 15 +++++++++++++++ dev/private.narHash | 2 +- dev/private/flake.lock | 42 +++++++++++++++++++++++++++++++++++++++++ dev/private/flake.nix | 6 ++++++ flake.nix | 42 ++++++++++++++++++++++++++++++++++++++--- 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 checks/darwin.nix create mode 100644 checks/home-manager.nix diff --git a/checks/darwin.nix b/checks/darwin.nix new file mode 100644 index 0000000..8b74e33 --- /dev/null +++ b/checks/darwin.nix @@ -0,0 +1,11 @@ + +{ + imports = [ + ../modules/nix-darwin/default.nix + ]; + documentation.enable = false; + sops.secrets.test_key = { }; + sops.defaultSopsFile = ../pkgs/sops-install-secrets/test-assets/secrets.yaml; + sops.age.generateKey = true; + system.stateVersion = 5; +} diff --git a/checks/home-manager.nix b/checks/home-manager.nix new file mode 100644 index 0000000..d1e6355 --- /dev/null +++ b/checks/home-manager.nix @@ -0,0 +1,15 @@ + +{ config, ... }: { + imports = [ + ../modules/home-manager/sops.nix + ]; + home.stateVersion = "25.05"; + home.username = "sops-user"; + home.homeDirectory = "/home/sops-user"; + home.enableNixpkgsReleaseCheck = false; + + sops.age.generateKey = true; + sops.age.keyFile = "${config.home.homeDirectory}/.age-key.txt"; + sops.secrets.test_key = { }; + sops.defaultSopsFile = ../pkgs/sops-install-secrets/test-assets/secrets.yaml; +} diff --git a/dev/private.narHash b/dev/private.narHash index b5ccbc3..0765e20 100644 --- a/dev/private.narHash +++ b/dev/private.narHash @@ -1 +1 @@ -sha256-qF9EiqHqJARLtA+ZABXa2mstgbza762DwoGEIGkyqVY= \ No newline at end of file +sha256-rXlTQPa9c8Ou52KO5S36sOyKUzurr5fuZcXnHr7g6YY= \ No newline at end of file diff --git a/dev/private/flake.lock b/dev/private/flake.lock index eb3008e..8e31b4f 100644 --- a/dev/private/flake.lock +++ b/dev/private/flake.lock @@ -1,5 +1,45 @@ { "nodes": { + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs-stable" + ] + }, + "locked": { + "lastModified": 1731832479, + "narHash": "sha256-icDDuYwJ0avTMZTxe1qyU/Baht5JOqw4pb5mWpR+hT0=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "5056a1cf0ce7c2a08ab50713b6c4af77975f6111", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nix-darwin": { + "inputs": { + "nixpkgs": [ + "nixpkgs-stable" + ] + }, + "locked": { + "lastModified": 1731809072, + "narHash": "sha256-pOsDJQR0imnFLfpvTmRpHcP0tflyxtP/QIzokrKSP8U=", + "owner": "LnL7", + "repo": "nix-darwin", + "rev": "34588d57cfc41c6953c54c93b6b685cab3b548ee", + "type": "github" + }, + "original": { + "owner": "LnL7", + "repo": "nix-darwin", + "type": "github" + } + }, "nixpkgs-stable": { "locked": { "lastModified": 1731842749, @@ -18,6 +58,8 @@ }, "root": { "inputs": { + "home-manager": "home-manager", + "nix-darwin": "nix-darwin", "nixpkgs-stable": "nixpkgs-stable", "treefmt-nix": "treefmt-nix" } diff --git a/dev/private/flake.nix b/dev/private/flake.nix index e3a4865..53b2439 100644 --- a/dev/private/flake.nix +++ b/dev/private/flake.nix @@ -5,5 +5,11 @@ inputs.treefmt-nix.url = "github:numtide/treefmt-nix"; inputs.treefmt-nix.inputs.nixpkgs.follows = "nixpkgs-stable"; + inputs.nix-darwin.url = "github:LnL7/nix-darwin"; + inputs.nix-darwin.inputs.nixpkgs.follows = "nixpkgs-stable"; + + inputs.home-manager.url = "github:nix-community/home-manager"; + inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs-stable"; + outputs = _: { }; } diff --git a/flake.nix b/flake.nix index 9e4da72..8fb6170 100644 --- a/flake.nix +++ b/flake.nix @@ -78,7 +78,7 @@ # dev outputs { checks = eachSystem ( - { system, ... }: + { pkgs, system, ... }: let tests = self.packages.${system}.sops-install-secrets.tests; packages-stable = import ./default.nix { @@ -90,7 +90,43 @@ nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; suffix-stable = suffix-version "-24_05"; in - tests // (suffix-stable tests-stable) // (suffix-stable packages-stable) + tests + // (suffix-stable tests-stable) + // (suffix-stable packages-stable) + // { + home-manager = self.legacyPackages.${system}.homeConfigurations.sops.activation-script; + } + // nixpkgs.lib.optionalAttrs pkgs.stdenv.isDarwin { + darwin-sops = + self.darwinConfigurations."sops-${pkgs.hostPlatform.darwinArch}".config.system.build.toplevel; + } + ); + + darwinConfigurations.sops-arm64 = privateInputs.nix-darwin.lib.darwinSystem { + modules = [ + ./checks/darwin.nix + #{ nixpkgs.pkgs = nixpkgs.legacyPackages.aarch64-darwin; } + { nixpkgs.hostPlatform = "aarch64-darwin"; } + ]; + }; + + darwinConfigurations.sops-x86_64 = privateInputs.nix-darwin.lib.darwinSystem { + modules = [ + ./checks/darwin.nix + { nixpkgs.hostPlatform = "x86_64-darwin"; } + ]; + }; + + legacyPackages = eachSystem ( + { pkgs, ... }: + { + homeConfigurations.sops = privateInputs.home-manager.lib.homeManagerConfiguration { + modules = [ + ./checks/home-manager.nix + ]; + inherit pkgs; + }; + } ); apps = eachSystem ( @@ -110,7 +146,7 @@ { pkgs, ... }: { unit-tests = pkgs.callPackage ./pkgs/unit-tests.nix { }; - default = pkgs.callPackage ./shell.nix {}; + default = pkgs.callPackage ./shell.nix { }; } ); }; From 1c75c1c13aea0b05981b6254fe1cf01c9e1dce6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 14:11:30 +0100 Subject: [PATCH 44/53] fix darwin evaluation --- modules/nix-darwin/manifest-for.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/nix-darwin/manifest-for.nix b/modules/nix-darwin/manifest-for.nix index c4a0524..5015659 100644 --- a/modules/nix-darwin/manifest-for.nix +++ b/modules/nix-darwin/manifest-for.nix @@ -1,12 +1,13 @@ { writeTextFile, cfg }: -suffix: secrets: extraJson: +suffix: secrets: templates: extraJson: writeTextFile { name = "manifest${suffix}.json"; text = builtins.toJSON ( { secrets = builtins.attrValues secrets; + templates = builtins.attrValues templates; # Does this need to be configurable? secretsMountPoint = "/run/secrets.d"; symlinkPath = "/run/secrets"; @@ -16,7 +17,6 @@ writeTextFile { ageKeyFile = cfg.age.keyFile; ageSshKeyPaths = cfg.age.sshKeyPaths; useTmpfs = false; - templates = cfg.templates; placeholderBySecretName = cfg.placeholder; userMode = false; logging = { From 793c07f331a831e4321038e3e8ac2e503167af8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 14:36:17 +0100 Subject: [PATCH 45/53] nix-darwin: fix shellcheck warning of activation script --- modules/nix-darwin/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/nix-darwin/default.nix b/modules/nix-darwin/default.nix index 4281d51..9e2f9e4 100644 --- a/modules/nix-darwin/default.nix +++ b/modules/nix-darwin/default.nix @@ -171,7 +171,7 @@ let '' if [[ ! -f ${escapedKeyFile} ]]; then echo generating machine-specific age key... - mkdir -p $(dirname ${escapedKeyFile}) + mkdir -p "$(dirname ${escapedKeyFile})" # age-keygen sets 0600 by default, no need to chmod. ${pkgs.age}/bin/age-keygen -o ${escapedKeyFile} fi From 420737291e2ec58a40c4a392bc4d1e29d87f1bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 14:59:50 +0100 Subject: [PATCH 46/53] load devshell from flake --- .envrc | 1 + shell.nix | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/shell.nix b/shell.nix index 30505fe..966d586 100644 --- a/shell.nix +++ b/shell.nix @@ -1,8 +1,15 @@ { - pkgs ? import { }, + mkShell, + bashInteractive, + go, + delve, + gnupg, + util-linux, + nix, + golangci-lint, }: -pkgs.mkShell { - nativeBuildInputs = with pkgs; [ +mkShell { + nativeBuildInputs = [ bashInteractive go delve From 799b572ef1ce4c6ed8efa806b9b542ae8d9cfe6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 15:08:13 +0100 Subject: [PATCH 47/53] move checks out of pkgs --- .../nixos-test.nix | 56 ++++++++++--------- flake.nix | 8 ++- pkgs/sops-install-secrets/default.nix | 5 -- 3 files changed, 34 insertions(+), 35 deletions(-) rename {pkgs/sops-install-secrets => checks}/nixos-test.nix (92%) diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/checks/nixos-test.nix similarity index 92% rename from pkgs/sops-install-secrets/nixos-test.nix rename to checks/nixos-test.nix index 4fcbfeb..f6409f5 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/checks/nixos-test.nix @@ -1,5 +1,7 @@ { lib, testers }: let + testAssets = ../pkgs/sops-install-secrets/test-assets; + userPasswordTest = name: extraConfig: testers.runNixOSTest { @@ -8,12 +10,12 @@ let { config, lib, ... }: { imports = [ - ../../modules/sops + ../modules/sops extraConfig ]; sops = { age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets.test_key.neededForUsers = true; secrets."nested/test/file".owner = "example-user"; }; @@ -58,16 +60,16 @@ in nodes.server = { ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; services.openssh.enable = true; services.openssh.hostKeys = [ { type = "rsa"; bits = 4096; - path = ./test-assets/ssh-key; + path = testAssets + "/ssh-key"; } ]; - sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.defaultSopsFile = testAssets + "/secrets.yaml"; sops.secrets.test_key = { }; }; @@ -82,17 +84,17 @@ in nodes.machine = { lib, ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets.test_key = { }; keepGenerations = lib.mkDefault 0; }; # must run before sops sets up keys boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; @@ -128,10 +130,10 @@ in nodes.machine = { config, ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets = { test_key = { }; @@ -166,7 +168,7 @@ in # must run before sops sets up keys boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; }; @@ -196,17 +198,17 @@ in age-ssh-keys = testers.runNixOSTest { name = "sops-age-ssh-keys"; nodes.machine = { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; services.openssh.enable = true; services.openssh.hostKeys = [ { type = "ed25519"; - path = ./test-assets/ssh-ed25519-key; + path = testAssets + "/ssh-ed25519-key"; } ]; sops = { - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets.test_key = { }; # Generate a key and append it to make sure it appending doesn't break anything age = { @@ -227,7 +229,7 @@ in nodes.server = { lib, config, ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; users.users.someuser = { isSystemUser = true; @@ -235,7 +237,7 @@ in }; sops.gnupg.home = "/run/gpghome"; - sops.defaultSopsFile = ./test-assets/secrets.yaml; + sops.defaultSopsFile = testAssets + "/secrets.yaml"; sops.secrets.test_key.owner = config.users.users.someuser.name; sops.secrets."nested/test/file".owner = config.users.users.someuser.name; sops.secrets.existing-file = { @@ -244,7 +246,7 @@ in }; # must run before sops system.activationScripts.gnupghome = lib.stringAfter [ "etc" ] '' - cp -r ${./test-assets/gnupghome} /run/gpghome + cp -r ${testAssets + "/gnupghome"} /run/gpghome chmod -R 700 /run/gpghome touch /run/existing-file @@ -254,7 +256,7 @@ in #environment.variables = { # GNUPGHOME = "/run/gpghome"; # SOPS_GPG_EXEC="${pkgs.gnupg}/bin/gpg"; - # SOPSFILE = "${./test-assets/secrets.yaml}"; + # SOPSFILE = "${testAssets + "/secrets.yaml"}"; #}; }; testScript = '' @@ -282,10 +284,10 @@ in nodes.machine = { config, ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets.test_key = { }; # Verify that things work even with `neededForUsers` secrets. See @@ -295,7 +297,7 @@ in # must run before sops sets up keys boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; @@ -361,11 +363,11 @@ in nodes.machine = { config, ... }: { - imports = [ ../../modules/sops ]; + imports = [ ../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; - defaultSopsFile = ./test-assets/secrets.yaml; + defaultSopsFile = testAssets + "/secrets.yaml"; secrets.test_key = { restartUnits = [ "restart-unit.service" @@ -390,7 +392,7 @@ in # must run before sops sets up keys boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; @@ -551,7 +553,7 @@ in user-passwords = userPasswordTest "sops-user-passwords" { # must run before sops sets up keys boot.initrd.postDeviceCommands = '' - cp -r ${./test-assets/age-keys.txt} /run/age-keys.txt + cp -r ${testAssets + "/age-keys.txt"} /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; }; @@ -568,7 +570,7 @@ in # must run before sops sets up keys systemd.services."sops-install-secrets-for-users".preStart = '' - printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt + printf '${builtins.readFile (testAssets + "/age-keys.txt")}' > /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; } @@ -586,7 +588,7 @@ in # must run before sops sets up keys systemd.services."sops-install-secrets-for-users".preStart = '' - printf '${builtins.readFile ./test-assets/age-keys.txt}' > /run/age-keys.txt + printf '${builtins.readFile testAssets + "/age-keys.txt"}' > /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; } diff --git a/flake.nix b/flake.nix index 8fb6170..72b306b 100644 --- a/flake.nix +++ b/flake.nix @@ -80,11 +80,14 @@ checks = eachSystem ( { pkgs, system, ... }: let - tests = self.packages.${system}.sops-install-secrets.tests; packages-stable = import ./default.nix { pkgs = privateInputs.nixpkgs-stable.legacyPackages.${system}; }; - tests-stable = packages-stable.sops-install-secrets.tests; + dropOverride = attrs: nixpkgs.lib.removeAttrs attrs [ "override" ]; + tests = dropOverride (pkgs.callPackage ./checks/nixos-test.nix { }); + tests-stable = dropOverride ( + privateInputs.nixpkgs-stable.legacyPackages.${system}.callPackage ./checks/nixos-test.nix { } + ); suffix-version = version: attrs: nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; @@ -105,7 +108,6 @@ darwinConfigurations.sops-arm64 = privateInputs.nix-darwin.lib.darwinSystem { modules = [ ./checks/darwin.nix - #{ nixpkgs.pkgs = nixpkgs.legacyPackages.aarch64-darwin; } { nixpkgs.hostPlatform = "aarch64-darwin"; } ]; }; diff --git a/pkgs/sops-install-secrets/default.nix b/pkgs/sops-install-secrets/default.nix index 52025b0..66289ce 100644 --- a/pkgs/sops-install-secrets/default.nix +++ b/pkgs/sops-install-secrets/default.nix @@ -4,7 +4,6 @@ stdenv, vendorHash, go, - callPackages, }: buildGoModule { pname = "sops-install-secrets"; @@ -21,10 +20,6 @@ buildGoModule { # requires root privileges for tests doCheck = false; - passthru = { - tests = lib.optionalAttrs stdenv.isLinux (callPackages ./nixos-test.nix { }); - }; - outputs = [ "out" ] ++ lib.lists.optionals (stdenv.isLinux) [ "unittest" ]; postInstall = From 0ec0d5d3c58ccafc622cb273e5458471931c65b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 15:08:57 +0100 Subject: [PATCH 48/53] remove obsolete sops-pgp-hook alias --- default.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/default.nix b/default.nix index 0c4e2ed..63abb9b 100644 --- a/default.nix +++ b/default.nix @@ -12,10 +12,6 @@ rec { sops-init-gpg-key = pkgs.callPackage ./pkgs/sops-init-gpg-key { }; default = sops-init-gpg-key; - sops-pgp-hook = pkgs.lib.warn '' - sops-pgp-hook is deprecated, use sops-import-keys-hook instead. - Also see https://github.com/Mic92/sops-nix/issues/98 - '' pkgs.callPackage ./pkgs/sops-pgp-hook { }; sops-import-keys-hook = pkgs.callPackage ./pkgs/sops-import-keys-hook { }; # backwards compatibility From 472741cf3fee089241ac9ea705bb2b9e0bfa2978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Nov 2024 17:51:52 +0100 Subject: [PATCH 49/53] fix eval of tests (#674) --- checks/nixos-test.nix | 2 +- flake.nix | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/checks/nixos-test.nix b/checks/nixos-test.nix index f6409f5..1d70bde 100644 --- a/checks/nixos-test.nix +++ b/checks/nixos-test.nix @@ -588,7 +588,7 @@ in # must run before sops sets up keys systemd.services."sops-install-secrets-for-users".preStart = '' - printf '${builtins.readFile testAssets + "/age-keys.txt"}' > /run/age-keys.txt + printf '${builtins.readFile (testAssets + "/age-keys.txt")}' > /run/age-keys.txt chmod -R 700 /run/age-keys.txt ''; } diff --git a/flake.nix b/flake.nix index 72b306b..b389c09 100644 --- a/flake.nix +++ b/flake.nix @@ -93,12 +93,12 @@ nixpkgs.lib.mapAttrs' (name: value: nixpkgs.lib.nameValuePair (name + version) value) attrs; suffix-stable = suffix-version "-24_05"; in - tests - // (suffix-stable tests-stable) - // (suffix-stable packages-stable) - // { + { home-manager = self.legacyPackages.${system}.homeConfigurations.sops.activation-script; } + // (suffix-stable packages-stable) + // nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux tests + // nixpkgs.lib.optionalAttrs pkgs.stdenv.isLinux (suffix-stable tests-stable) // nixpkgs.lib.optionalAttrs pkgs.stdenv.isDarwin { darwin-sops = self.darwinConfigurations."sops-${pkgs.hostPlatform.darwinArch}".config.system.build.toplevel; From e39947d0ee8e341fa7108bd02a33cdfa24a1360e Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Mon, 18 Nov 2024 10:08:10 -0800 Subject: [PATCH 50/53] allow for missing switch-to-configuration directory NixOS' switch-to-configuration program creates the /run/nixos directory, which may not be present if `system.switch.enable` is `false`. --- pkgs/sops-install-secrets/main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index f4764a0..280df4c 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -978,6 +978,13 @@ func handleModifications(isDry bool, logcfg loggingConfig, symlinkPath string, s writeLines := func(list []string, file string) error { if len(list) != 0 { + if _, err := os.Stat(filepath.Dir(file)); err != nil { + if os.IsNotExist(err) { + return nil + } else { + return err + } + } f, err := os.OpenFile(file, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600) if err != nil { return err From 53c853fb1a7e4f25f68805ee25c83d5de18dc699 Mon Sep 17 00:00:00 2001 From: Mergify <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:15:44 +0000 Subject: [PATCH 51/53] ci(mergify): upgrade configuration to current format --- .mergify.yml | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 0144107..2fc1e2e 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,16 +1,14 @@ queue_rules: - name: default - merge_conditions: - - check-success=buildbot/nix-build -defaults: - actions: - queue: - merge_method: rebase -pull_request_rules: - - name: merge using the merge queue - conditions: + queue_conditions: - base=master - label~=merge-queue|dependencies + merge_conditions: + - check-success=buildbot/nix-build + merge_method: rebase + +pull_request_rules: + - name: refactored queue action rule + conditions: [] actions: queue: - From 6ecde343ef558861da82936283dd4523478b30e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:55:02 +0000 Subject: [PATCH 52/53] build(deps): bump github.com/ProtonMail/go-crypto from 1.1.2 to 1.1.3 Bumps [github.com/ProtonMail/go-crypto](https://github.com/ProtonMail/go-crypto) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/ProtonMail/go-crypto/releases) - [Commits](https://github.com/ProtonMail/go-crypto/compare/v1.1.2...v1.1.3) --- updated-dependencies: - dependency-name: github.com/ProtonMail/go-crypto dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 060734f..64fd8c9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 - github.com/ProtonMail/go-crypto v1.1.2 + github.com/ProtonMail/go-crypto v1.1.3 github.com/getsops/sops/v3 v3.8.1 github.com/joho/godotenv v1.5.1 github.com/mozilla-services/yaml v0.0.0-20201007153854-c369669a6625 diff --git a/go.sum b/go.sum index 477e937..7431234 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0 h1:zF3WQbETL3cLvt github.com/Mic92/ssh-to-age v0.0.0-20240115094500-460a2109aaf0/go.mod h1:OUOla4dJLQ5FfdB07jnjawnMEqI0M3Q4WuD2W/DjhLo= github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/ProtonMail/go-crypto v1.1.2 h1:A7JbD57ThNqh7XjmHE+PXpQ3Dqt3BrSAC0AL0Go3KS0= -github.com/ProtonMail/go-crypto v1.1.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= +github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= From 3433ea14fbd9e6671d0ff0dd45ed15ee4c156ffa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 22:56:03 +0000 Subject: [PATCH 53/53] update vendorHash --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 63abb9b..cc61b7f 100644 --- a/default.nix +++ b/default.nix @@ -1,6 +1,6 @@ { pkgs ? import { }, - vendorHash ? "sha256-xHScXL3i2oxJSJsvOC+KqLCA5Psu3ht7DQNrh0rB1rA=", + vendorHash ? "sha256-7xnbw5tH3MYD/aA8yBNG327IONjUoBarTluLeqTH/8A=", }: let sops-install-secrets = pkgs.callPackage ./pkgs/sops-install-secrets {