diff --git a/flake.nix b/flake.nix index ad02cce..5ff7cd8 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,8 @@ }; }) // { - nixosModules.default = import ./modules/op-secrets.nix; + darwinModules.default = import ./modules/darwin.nix; + nixosModules.default = import ./modules/nixos.nix; # test is a hostname for our machine nixosConfigurations.test = nixpkgs.lib.nixosSystem { diff --git a/modules/common.nix b/modules/common.nix new file mode 100644 index 0000000..35efc8d --- /dev/null +++ b/modules/common.nix @@ -0,0 +1,56 @@ +{ lib, pkgs, config, ... }: +with lib; +let + inherit (import ./types.nix { + inherit lib; + inherit config; + }) + secretFileDeclaration; +in { + options.opnix = { + opBin = mkOption { + type = types.str; + default = "${pkgs._1password-cli}/bin/op"; + description = "The 1Password CLI `op` executable to use"; + }; + environmentFile = mkOption { + type = types.str; + description = '' + Path to a environment file which contains your service account token. Format should be `OP_SERVICE_ACCOUNT_TOKEN="{ your token here }"`. This is used to authorize the 1Password CLI.''; + }; + secretsDir = mkOption { + type = types.path; + default = "/run/opnix"; + description = '' + Directory where secrets are symlinked to + ''; + }; + secretsMountPoint = mkOption { + type = types.addCheck types.str (s: + (trim s) != "" # non-empty + && (builtins.match ".+/" s) == null) # without trailing slash + // { + description = + "${types.str.description} (with check: non-empty without trailing slash)"; + }; + default = "/run/opnix.d"; + }; + secrets = mkOption { + type = types.attrsOf secretFileDeclaration; + description = "The secrets you want to use in your NixOS deployment"; + default = { }; + example = { + my-secret = { + source = "{{ op://VaultName/ItemName/FieldName }}"; + mode = "0400"; + inherit (config.services.some_service) user; + inherit (config.services.some_service) group; + }; + another-secret.source = '' + [SomeTomlHeader] + SomeValue = "{{ op://AnotherVault/AnotherItem/AnotherField }}" + ''; + }; + }; + }; +} diff --git a/modules/darwin.nix b/modules/darwin.nix new file mode 100644 index 0000000..1071b58 --- /dev/null +++ b/modules/darwin.nix @@ -0,0 +1,53 @@ +toplevel @ { + config, + lib, + pkgs, + ... +}: let + inherit (lib) mkIf mkMerge; + + cfg = config.opnix; + scripts = import ./scripts.nix toplevel; +in { + imports = [./common.nix]; + + config = let + opnixScript = '' + ${scripts.installSecrets} + ${scripts.chownSecrets} + ''; + in + mkIf (cfg.secrets != {}) (mkMerge [ + { + launchd.daemons.activate-opnix = { + script = '' + set -euo pipefail + export PATH="${pkgs.gnugrep}/bin:${pkgs.coreutils}/bin:@out@/sw/bin:/usr/bin:/bin:/usr/sbin:/sbin" + source ${cfg.environmentFile} + export OP_SERVICE_ACCOUNT_TOKEN + ${opnixScript} + ''; + serviceConfig = { + RunAtLoad = true; + KeepAlive.SuccessfulExit = false; + }; + }; + } + { + system.activationScripts = { + # if no generation already exists, rely on the launchd startup job; + # otherwise, if there already is an existing generation, reprovision + # secrets because we did a darwin-rebuild + postActivation.text = lib.mkAfter '' + ${scripts.setOpnixGeneration} + (( _opnix_generation > 1 )) && { + # shellcheck disable=SC1091 + source ${cfg.environmentFile} + export OP_SERVICE_ACCOUNT_TOKEN + ${opnixScript} + } + ''; + }; + } + ]); +} diff --git a/modules/nixos.nix b/modules/nixos.nix new file mode 100644 index 0000000..e941a64 --- /dev/null +++ b/modules/nixos.nix @@ -0,0 +1,77 @@ +toplevel @ { + config, + lib, + ... +}: let + inherit + (lib) + mkIf + mkMerge + mkOption + types + ; + + cfg = config.opnix; + scripts = import ./scripts.nix toplevel; +in { + imports = [./common.nix]; + + options.opnix = { + systemdWantedBy = mkOption { + type = types.listOf types.str; + default = []; + description = '' + A list of `systemd` service names that depend on secrets from `opnix`. This option will set `after = [ "opnix.service" ]` and `wants = [ "opnix.service" ]` for each specified `systemd` unit.''; + example = ["homepage-dashboard" "wg-quick-vpn"]; + }; + }; + + config = let + opnixScript = '' + ${scripts.installSecrets} + ${scripts.chownSecrets} + ''; + in + mkIf (cfg.secrets != {}) (mkMerge [ + { + systemd.services.opnix = { + wants = ["network-online.target"]; + after = ["network.target" "network-online.target"]; + + serviceConfig = { + Type = "oneshot"; + EnvironmentFile = cfg.environmentFile; + RemainAfterExit = true; + }; + + script = opnixScript; + }; + } + { + system.activationScripts.opnix-on-rebuild = { + # if no generation already exists, rely on the systemd startup job; + # otherwise, if there already is an existing generation, reprovision + # secrets because we did a nixos-rebuild + text = '' + ${scripts.setOpnixGeneration} + (( _opnix_generation > 1 )) && { + source ${cfg.environmentFile} + export OP_SERVICE_ACCOUNT_TOKEN + ${opnixScript} + } + ''; + deps = ["usrbinenv"]; + }; + } + { + systemd.services = builtins.listToAttrs (builtins.map (systemdName: { + name = systemdName; + value = { + after = ["opnix.service"]; + wants = ["opnix.service"]; + }; + }) + cfg.systemdWantedBy); + } + ]); +} diff --git a/modules/op-secrets.nix b/modules/op-secrets.nix deleted file mode 100644 index 4ce3100..0000000 --- a/modules/op-secrets.nix +++ /dev/null @@ -1,112 +0,0 @@ -{ lib, pkgs, config, ... }: -with lib; -let - inherit (import ./types.nix { - inherit lib; - inherit config; - }) - secretFileDeclaration; - cfg = config.opnix; - scripts = import ./scripts.nix { - inherit lib; - inherit config pkgs; - }; -in { - options.opnix = { - opBin = mkOption { - type = types.str; - default = "${pkgs._1password-cli}/bin/op"; - description = "The 1Password CLI `op` executable to use"; - }; - environmentFile = mkOption { - type = types.str; - description = '' - Path to a environment file which contains your service account token. Format should be `OP_SERVICE_ACCOUNT_TOKEN="{ your token here }"`. This is used to authorize the 1Password CLI in the systemd job.''; - }; - secretsDir = mkOption { - type = types.path; - default = "/run/opnix"; - description = '' - Directory where secrets are symlinked to - ''; - }; - secretsMountPoint = mkOption { - type = types.addCheck types.str (s: - (trim s) != "" # non-empty - && (builtins.match ".+/" s) == null) # without trailing slash - // { - description = - "${types.str.description} (with check: non-empty without trailing slash)"; - }; - default = "/run/opnix.d"; - }; - secrets = mkOption { - type = types.attrsOf secretFileDeclaration; - description = "The secrets you want to use in your NixOS deployment"; - default = { }; - example = { - my-secret = { - source = "{{ op://VaultName/ItemName/FieldName }}"; - mode = "0400"; - inherit (config.services.some_service) user; - inherit (config.services.some_service) group; - }; - another-secret.source = '' - [SomeTomlHeader] - SomeValue = "{{ op://AnotherVault/AnotherItem/AnotherField }}" - ''; - }; - }; - systemdWantedBy = mkOption { - type = types.listOf types.str; - default = [ ]; - description = '' - A list of `systemd` service names that depend on secrets from `opnix`. This option will set `after = [ "opnix.service" ]` and `wants = [ "opnix.service" ]` for each specified `systemd` unit.''; - example = [ "homepage-dashboard" "wg-quick-vpn" ]; - }; - }; - config = let - opnixScript = '' - ${scripts.installSecrets} - ${scripts.chownSecrets} - ''; - in mkIf (cfg.secrets != { }) (mkMerge [ - { - systemd.services.opnix = { - wants = [ "network-online.target" ]; - after = [ "network.target" "network-online.target" ]; - - serviceConfig = { - Type = "oneshot"; - EnvironmentFile = cfg.environmentFile; - RemainAfterExit = true; - }; - - script = opnixScript; - }; - system.activationScripts.opnix-on-rebuild = { - # if no generation already exists, rely on the systemd startup job; - # otherwise, if there already is an existing generation, reprovision - # secrets because we did a nixos-rebuild - text = '' - ${scripts.setOpnixGeneration} - (( _opnix_generation > 1 )) && { - source ${cfg.environmentFile} - export OP_SERVICE_ACCOUNT_TOKEN - ${opnixScript} - } - ''; - deps = [ "usrbinenv" ]; - }; - } - { - systemd.services = builtins.listToAttrs (builtins.map (systemdName: { - name = systemdName; - value = { - after = [ "opnix.service" ]; - wants = [ "opnix.service" ]; - }; - }) cfg.systemdWantedBy); - } - ]); -} diff --git a/modules/scripts.nix b/modules/scripts.nix index 92a4e3d..5038aa0 100644 --- a/modules/scripts.nix +++ b/modules/scripts.nix @@ -1,9 +1,11 @@ -{ config, lib, pkgs, ... }: +{ config, lib, options, pkgs, ... }: let + isDarwin = lib.attrsets.hasAttrByPath ["environment" "darwinConfig"] options; + cfg = config.opnix; op = cfg.opBin; - op_tmp_dir = "/root/op_tmp"; - op_cfg_dir = "/root/.config/op"; + op_tmp_dir = "${lib.optionalString isDarwin "/var"}/root/op_tmp"; + op_cfg_dir = "${lib.optionalString isDarwin "/var"}/root/.config/op"; # fixes permissions issues with op session files createTmpDirShim = '' rm -rf ${op_tmp_dir} @@ -23,10 +25,20 @@ let fi chmod 600 ${op_cfg_dir}/config ''; - mountCommand = '' - grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || - ${pkgs.util-linux}/bin/mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751 - ''; + mountCommand = + if isDarwin + then '' + if ! diskutil info "${cfg.secretsMountPoint}" &> /dev/null; then + 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 + '' + else '' + grep -q "${cfg.secretsMountPoint} ramfs" /proc/mounts || + ${pkgs.util-linux}/bin/mount -t ramfs none "${cfg.secretsMountPoint}" -o nodev,nosuid,mode=0751 + ''; setOpnixGeneration = '' _opnix_generation="$(basename "$(readlink ${cfg.secretsDir})" || echo 0)" ''; @@ -40,7 +52,10 @@ let mkdir -p "${cfg.secretsMountPoint}/$_opnix_generation" chmod 0751 "${cfg.secretsMountPoint}/$_opnix_generation" ''; - chownGroup = "keys"; + 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 = '' @@ -76,6 +91,7 @@ let TMP_FILE="$_truePath.tmp" mkdir -p "$(dirname "$_truePath")" + # shellcheck disable=SC2050 [ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && mkdir -p "$(dirname "${secretType.path}")" ( umask u=r,g=,o= @@ -89,6 +105,7 @@ let mv -f "$TMP_FILE" "$_truePath" ${lib.optionalString secretType.symlink '' + # shellcheck disable=SC2050 [ "${secretType.path}" != "${cfg.secretsDir}/${secretType.name}" ] && ln -sfT "${cfg.secretsDir}/${secretType.name}" "${secretType.path}" ''} '';