From 351e87491837a03870bb583b30c8547a3e7e410e Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Sun, 29 Jan 2023 15:42:58 -0700 Subject: [PATCH 01/12] Try to add nix-darwin support to agenix Merges work by @montchr, @cmhamill, and @rtimush and rebases on main. - fixes https://github.com/ryantm/agenix/issues/60 - fixes https://github.com/ryantm/agenix/issues/120 - closes https://github.com/ryantm/agenix/pull/107 --- .github/workflows/ci.yaml | 5 + flake.lock | 22 ++++ flake.nix | 26 ++++- modules/age.nix | 153 ++++++++++++++++++-------- test/install_ssh_host_keys_darwin.nix | 10 ++ test/integration_darwin.nix | 24 ++++ 6 files changed, 191 insertions(+), 49 deletions(-) create mode 100644 test/install_ssh_host_keys_darwin.nix create mode 100644 test/integration_darwin.nix diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d00e84e..a54ed78 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,3 +23,8 @@ jobs: - run: nix build - run: nix fmt . -- --check - run: nix flake check + - run: | + system=$(nix build --no-link --print-out-paths .#checks.x86_64-darwin.integration) + ${system}/activate-user + sudo ${system}/activate + - run: sudo /run/current-system/sw/bin/agenix-integration diff --git a/flake.lock b/flake.lock index 146ba0c..df8a306 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,26 @@ { "nodes": { + "darwin": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1673295039, + "narHash": "sha256-AsdYgE8/GPwcelGgrntlijMg4t3hLFJFCRF3tL5WVjA=", + "owner": "lnl7", + "repo": "nix-darwin", + "rev": "87b9d090ad39b25b2400029c64825fc2a8868943", + "type": "github" + }, + "original": { + "owner": "lnl7", + "ref": "master", + "repo": "nix-darwin", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1674641431, @@ -18,6 +39,7 @@ }, "root": { "inputs": { + "darwin": "darwin", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index b2e2c2b..143dd5f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,17 +1,27 @@ { description = "Secret management with age"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + darwin = { + url = "github:lnl7/nix-darwin/master"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; outputs = { self, nixpkgs, + darwin, }: let agenix = system: nixpkgs.legacyPackages.${system}.callPackage ./pkgs/agenix.nix {}; in { nixosModules.age = import ./modules/age.nix; nixosModules.default = self.nixosModules.age; + darwinModules.age = import ./modules/age.nix; + darwinModules.default = self.darwinModules.age; + overlays.default = import ./overlay.nix; formatter.x86_64-darwin = nixpkgs.legacyPackages.x86_64-darwin.alejandra; @@ -38,5 +48,19 @@ pkgs = nixpkgs.legacyPackages.x86_64-linux; system = "x86_64-linux"; }; + checks."aarch64-darwin".integration = + (darwin.lib.darwinSystem { + system = "aarch64-darwin"; + modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"]; + }) + .system; + checks."x86_64-darwin".integration = + (darwin.lib.darwinSystem { + system = "x86_64-darwin"; + modules = [./test/integration_darwin.nix "${darwin.outPath}/pkgs/darwin-installer/installer.nix"]; + }) + .system; + + darwinConfigurations.integration.system = self.checks."x86_64-darwin".integration; }; } diff --git a/modules/age.nix b/modules/age.nix index 1b8d0bf..eb8e7f8 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -8,6 +8,8 @@ with lib; let cfg = config.age; + isDarwin = builtins.hasAttr "darwinConfig" options.environment; + # we need at least rage 0.5.0 to support ssh keys rage = if lib.versionOlder pkgs.rage.version "0.5.0" @@ -17,17 +19,40 @@ with lib; let users = config.users.users; + mountCommand = + if isDarwin + then '' + if ! diskutil info "${cfg.secretsMountPoint}"; then + dev="$(hdiutil attach -nomount ram://1048576 | awk '{print $1}')" + newfs_hfs "$dev" + mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" + fi + '' + else '' + grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || + mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751 + ''; newGeneration = '' _agenix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)" (( ++_agenix_generation )) echo "[agenix] creating new generation in ${cfg.secretsMountPoint}/$_agenix_generation" mkdir -p "${cfg.secretsMountPoint}" chmod 0751 "${cfg.secretsMountPoint}" - grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751 + ${mountCommand} mkdir -p "${cfg.secretsMountPoint}/$_agenix_generation" chmod 0751 "${cfg.secretsMountPoint}/$_agenix_generation" ''; + chownGroup = + if isDarwin + then "admin" + else "keys"; + # chown the secrets mountpoint and the current generation to the keys group + # instead of leaving it root:root. + chownMountPoint = '' + chown :${chownGroup} "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation" + ''; + identities = builtins.concatStringsSep " " (map (path: "-i ${path}") cfg.identityPaths); setTruePath = secretType: '' @@ -52,7 +77,7 @@ with lib; let umask u=r,g=,o= test -f "${secretType.file}" || echo '[agenix] WARNING: encrypted file ${secretType.file} does not exist!' test -d "$(dirname "$TMP_FILE")" || echo "[agenix] WARNING: $(dirname "$TMP_FILE") does not exist!" - LANG=${config.i18n.defaultLocale} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}" + LANG=${config.i18n.defaultLocale or "C"} ${ageBin} --decrypt ${identities} -o "$TMP_FILE" "${secretType.file}" ) chmod ${secretType.mode} "$TMP_FILE" mv -f "$TMP_FILE" "$_truePath" @@ -92,12 +117,6 @@ with lib; let chown ${secretType.owner}:${secretType.group} "$_truePath" ''; - # chown the secrets mountpoint and the current generation to the keys group - # instead of leaving it root:root. - chownMountPoint = '' - chown :keys "${cfg.secretsMountPoint}" "${cfg.secretsMountPoint}/$_agenix_generation" - ''; - chownSecrets = builtins.concatStringsSep "\n" ( ["echo '[agenix] chowning...'"] ++ [chownMountPoint] @@ -194,8 +213,13 @@ in { identityPaths = mkOption { type = types.listOf types.path; default = - if config.services.openssh.enable + if (config.services.openssh.enable or false) then map (e: e.path) (lib.filter (e: e.type == "rsa" || e.type == "ed25519") config.services.openssh.hostKeys) + else if isDarwin + then [ + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_rsa_key" + ] else []; description = '' Path to SSH keys to be used as identities in age decryption. @@ -203,48 +227,81 @@ in { }; }; - config = mkIf (cfg.secrets != {}) { - assertions = [ - { - assertion = cfg.identityPaths != []; - message = "age.identityPaths must be set."; - } - ]; - - # Create a new directory full of secrets for symlinking (this helps - # ensure removed secrets are actually removed, or at least become - # invalid symlinks). - system.activationScripts.agenixNewGeneration = { - text = newGeneration; - deps = [ - "specialfs" + config = mkIf (cfg.secrets != {}) (mkMerge [ + { + assertions = [ + { + assertion = cfg.identityPaths != []; + message = "age.identityPaths must be set."; + } ]; - }; + } - system.activationScripts.agenixInstall = { - text = installSecrets; - deps = [ - "agenixNewGeneration" - "specialfs" - ]; - }; + (optionalAttrs (!isDarwin) { + # Create a new directory full of secrets for symlinking (this helps + # ensure removed secrets are actually removed, or at least become + # invalid symlinks). + system.activationScripts.agenixNewGeneration = { + text = newGeneration; + deps = [ + "specialfs" + ]; + }; - # So user passwords can be encrypted. - system.activationScripts.users.deps = ["agenixInstall"]; + system.activationScripts.agenixInstall = { + text = installSecrets; + deps = [ + "agenixNewGeneration" + "specialfs" + ]; + }; - # Change ownership and group after users and groups are made. - system.activationScripts.agenixChown = { - text = chownSecrets; - deps = [ - "users" - "groups" - ]; - }; + # So user passwords can be encrypted. + system.activationScripts.users.deps = ["agenixInstall"]; - # So other activation scripts can depend on agenix being done. - system.activationScripts.agenix = { - text = ""; - deps = ["agenixChown"]; - }; - }; + # Change ownership and group after users and groups are made. + system.activationScripts.agenixChown = { + text = chownSecrets; + deps = [ + "users" + "groups" + ]; + }; + + # So other activation scripts can depend on agenix being done. + system.activationScripts.agenix = { + text = ""; + deps = ["agenixChown"]; + }; + }) + (optionalAttrs isDarwin { + system.activationScripts = { + # Secrets with root owner and group can be installed before users + # exist. This allows user password files to be encrypted. + preActivation.text = builtins.concatStringsSep "\n" [ + newGeneration + installSecrets + ]; + + # Other secrets need to wait for users and groups to exist. + users.text = lib.mkAfter '' + ${chownSecrets} + ''; + }; + + launchd.daemons.activate-agenix = { + script = '' + set -e + set -o pipefail + export PATH="${pkgs.gnugrep}/bin:${pkgs.coreutils}/bin:@out@/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin" + ${newGeneration} + ${installSecrets} + ${chownSecrets} + exit 0 + ''; + serviceConfig.RunAtLoad = true; + serviceConfig.KeepAlive.SuccessfulExit = false; + }; + }) + ]); } diff --git a/test/install_ssh_host_keys_darwin.nix b/test/install_ssh_host_keys_darwin.nix new file mode 100644 index 0000000..78e1567 --- /dev/null +++ b/test/install_ssh_host_keys_darwin.nix @@ -0,0 +1,10 @@ +# Do not copy this! It is insecure. This is only okay because we are testing. +{ + system.activationScripts.extraUserActivation.text = '' + echo "Installing SSH host key" + sudo cp ${../example_keys/system1.pub} /etc/ssh/ssh_host_ed25519_key.pub + sudo cp ${../example_keys/system1} /etc/ssh/ssh_host_ed25519_key + sudo chmod 644 /etc/ssh/ssh_host_ed25519_key.pub + sudo chmod 600 /etc/ssh/ssh_host_ed25519_key + ''; +} diff --git a/test/integration_darwin.nix b/test/integration_darwin.nix new file mode 100644 index 0000000..24a8579 --- /dev/null +++ b/test/integration_darwin.nix @@ -0,0 +1,24 @@ +{ + config, + pkgs, + ... +}: let + secret = "hello"; + testScript = pkgs.writeShellApplication { + name = "agenix-integration"; + text = '' + grep ${secret} ${config.age.secrets.secret1.path} + ''; + }; +in { + imports = [ + ./install_ssh_host_keys_darwin.nix + ../modules/age.nix + ]; + + services.nix-daemon.enable = true; + + age.secrets.secret1.file = ../example/secret1.age; + + environment.systemPackages = [testScript]; +} From 4532604741128adce7e86e9b9febb6d5f97832c1 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 09:06:03 -0700 Subject: [PATCH 02/12] Silence output --- modules/age.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/age.nix b/modules/age.nix index eb8e7f8..88489ca 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -22,9 +22,9 @@ with lib; let mountCommand = if isDarwin then '' - if ! diskutil info "${cfg.secretsMountPoint}"; then dev="$(hdiutil attach -nomount ram://1048576 | awk '{print $1}')" newfs_hfs "$dev" + if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi '' From 8867c12d72c9f5d77248877a56f62078036101d6 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 09:06:39 -0700 Subject: [PATCH 03/12] Cleanup, improve readability --- modules/age.nix | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index 88489ca..422346e 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -20,11 +20,9 @@ with lib; let users = config.users.users; mountCommand = - if isDarwin - then '' - dev="$(hdiutil attach -nomount ram://1048576 | awk '{print $1}')" - newfs_hfs "$dev" + if isDarwin then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then + dev=$(hdiutil attach -nomount ram://1MiB) mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi '' From 019784cb7e9df065332e306a4262124f97394e16 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 09:06:59 -0700 Subject: [PATCH 04/12] Give volume a name --- modules/age.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/age.nix b/modules/age.nix index 422346e..3770250 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -23,6 +23,7 @@ with lib; let if isDarwin then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then dev=$(hdiutil attach -nomount ram://1MiB) + newfs_hfs -v agenix "$dev" mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi '' From b818ac2e7df04ce53e822f584d660c4081bf27b3 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 09:18:56 -0700 Subject: [PATCH 05/12] fmt --- modules/age.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/age.nix b/modules/age.nix index 3770250..90637ad 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -20,7 +20,8 @@ with lib; let users = config.users.users; mountCommand = - if isDarwin then '' + if isDarwin + then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then dev=$(hdiutil attach -nomount ram://1MiB) newfs_hfs -v agenix "$dev" From c69689da5885f2405abe3947dfe73b687f04da6d Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 14:21:33 -0700 Subject: [PATCH 06/12] Use diskutil for more convenient sizes, strip trailing tabs --- modules/age.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/age.nix b/modules/age.nix index 90637ad..ecdd8ee 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -23,7 +23,9 @@ with lib; let if isDarwin then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then - dev=$(hdiutil attach -nomount ram://1MiB) + dev=$(diskutil image attach -mountPolicy=noMount ram://1MiB) + # Remove trailing tabs + dev=''${dev%%[[:space:]]*} newfs_hfs -v agenix "$dev" mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi From 9b94b43971e0363905f13acb8537a03a7b1fe90d Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 14:21:42 -0700 Subject: [PATCH 07/12] format --- modules/age.nix | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index ecdd8ee..277d286 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -90,7 +90,8 @@ with lib; let ''; testIdentities = - map (path: '' + map + (path: '' test -f ${path} || echo '[agenix] WARNING: config.age.identityPaths entry ${path} not present!' '') cfg.identityPaths; @@ -301,8 +302,10 @@ in { ${chownSecrets} exit 0 ''; - serviceConfig.RunAtLoad = true; - serviceConfig.KeepAlive.SuccessfulExit = false; + serviceConfig = { + RunAtLoad = true; + KeepAlive.SuccessfulExit = false; + }; }; }) ]); From 4c315d9683e17b32cc5040cb214310508cee5150 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 14:21:49 -0700 Subject: [PATCH 08/12] Remove activation scripts --- modules/age.nix | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index 277d286..e3ca0b2 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -278,20 +278,6 @@ in { }; }) (optionalAttrs isDarwin { - system.activationScripts = { - # Secrets with root owner and group can be installed before users - # exist. This allows user password files to be encrypted. - preActivation.text = builtins.concatStringsSep "\n" [ - newGeneration - installSecrets - ]; - - # Other secrets need to wait for users and groups to exist. - users.text = lib.mkAfter '' - ${chownSecrets} - ''; - }; - launchd.daemons.activate-agenix = { script = '' set -e From 4b2b6fa11152e26a6c0d6626fc6c9f0eeaa037e0 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 14:37:15 -0700 Subject: [PATCH 09/12] Simplify removal of trailing spaces --- modules/age.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index e3ca0b2..701e101 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -23,9 +23,7 @@ with lib; let if isDarwin then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then - dev=$(diskutil image attach -mountPolicy=noMount ram://1MiB) - # Remove trailing tabs - dev=''${dev%%[[:space:]]*} + dev=$(diskutil image attach -mountPolicy=noMount ram://1MiB | sed 's/[[:space:]]*$//') newfs_hfs -v agenix "$dev" mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi From 9779a98f1e1920989c2f879edec79ec34e2cb77a Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 15:33:50 -0700 Subject: [PATCH 10/12] Testing for CI -- revert "Remove activation scripts" This reverts commit 4c315d9683e17b32cc5040cb214310508cee5150. --- modules/age.nix | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/modules/age.nix b/modules/age.nix index 701e101..ec8546f 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -276,6 +276,20 @@ in { }; }) (optionalAttrs isDarwin { + system.activationScripts = { + # Secrets with root owner and group can be installed before users + # exist. This allows user password files to be encrypted. + preActivation.text = builtins.concatStringsSep "\n" [ + newGeneration + installSecrets + ]; + + # Other secrets need to wait for users and groups to exist. + users.text = lib.mkAfter '' + ${chownSecrets} + ''; + }; + launchd.daemons.activate-agenix = { script = '' set -e From 6ec0b0f7c721a8beea4f708fc1ed4e8c311ac94b Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 15:51:52 -0700 Subject: [PATCH 11/12] Revert to hdiutil for older macos compatibility, be explicit about the weird number after ram:// --- modules/age.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/age.nix b/modules/age.nix index ec8546f..7f2fc0f 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -23,7 +23,8 @@ with lib; let if isDarwin then '' if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then - dev=$(diskutil image attach -mountPolicy=noMount ram://1MiB | sed 's/[[:space:]]*$//') + num_sectors=1048576 + dev=$(hdiutil attach -nomount ram://"$num_sectors" | sed 's/[[:space:]]*$//') newfs_hfs -v agenix "$dev" mount -t hfs -o nobrowse,nodev,nosuid,-m=0751 "$dev" "${cfg.secretsMountPoint}" fi From d7fd31756e1c5f1281981c48efbb2e188024ba47 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Mon, 30 Jan 2023 15:52:05 -0700 Subject: [PATCH 12/12] Remove activation scripts again --- modules/age.nix | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/modules/age.nix b/modules/age.nix index 7f2fc0f..e89125e 100644 --- a/modules/age.nix +++ b/modules/age.nix @@ -277,20 +277,6 @@ in { }; }) (optionalAttrs isDarwin { - system.activationScripts = { - # Secrets with root owner and group can be installed before users - # exist. This allows user password files to be encrypted. - preActivation.text = builtins.concatStringsSep "\n" [ - newGeneration - installSecrets - ]; - - # Other secrets need to wait for users and groups to exist. - users.text = lib.mkAfter '' - ${chownSecrets} - ''; - }; - launchd.daemons.activate-agenix = { script = '' set -e