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]; +}