From 6944ee2ab9da4dd01bc9212bf010c7526afed093 Mon Sep 17 00:00:00 2001 From: Jesse Young Lin Date: Wed, 6 May 2026 16:09:29 -0500 Subject: [PATCH 1/5] style: run formatter --- modules/home-manager/sops.nix | 122 +++++++++++++++++----------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index be11f691..3bc29f22 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -3,14 +3,11 @@ lib, pkgs, ... -}: - -let +}: let cfg = config.sops; sops-install-secrets = cfg.package; secretType = lib.types.submodule ( - { name, ... }: - { + {name, ...}: { options = { name = lib.mkOption { type = lib.types.str; @@ -22,7 +19,10 @@ let key = lib.mkOption { type = lib.types.str; - default = if cfg.defaultSopsKey != null then cfg.defaultSopsKey else name; + default = + if cfg.defaultSopsKey != null + then cfg.defaultSopsKey + else name; description = '' Key used to lookup in the sops file. To access nested data structures, use / as a separator. @@ -85,8 +85,7 @@ let merge = lib.mergeEqualOption; }; - manifestFor = - suffix: secrets: templates: + manifestFor = suffix: secrets: templates: pkgs.writeTextFile { name = "manifest${suffix}.json"; text = builtins.toJSON { @@ -108,7 +107,9 @@ let }; checkPhase = '' ${sops-install-secrets}/bin/sops-install-secrets -check-mode=${ - if cfg.validateSopsFiles then "sopsfile" else "manifest" + if cfg.validateSopsFiles + then "sopsfile" + else "manifest" } "$out" ''; }; @@ -132,8 +133,7 @@ let '' ) ); -in -{ +in { imports = [ ./templates.nix ]; @@ -141,7 +141,7 @@ in options.sops = { secrets = lib.mkOption { type = lib.types.attrsOf secretType; - default = { }; + default = {}; description = '' Secrets to decrypt. ''; @@ -222,7 +222,7 @@ in 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. @@ -232,7 +232,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. @@ -251,7 +251,7 @@ in plugins = lib.mkOption { type = lib.types.listOf lib.types.package; - default = [ ]; + default = []; description = '' List of plugins to use for sops decryption. ''; @@ -269,7 +269,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. ''; @@ -310,7 +310,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. @@ -319,29 +319,32 @@ in }; }; - config = lib.mkIf (cfg.secrets != { }) { + config = lib.mkIf (cfg.secrets != {}) { assertions = [ { assertion = - cfg.gnupg.home != null - || cfg.gnupg.sshKeyPaths != [ ] + cfg.gnupg.home + != null + || cfg.gnupg.sshKeyPaths != [] || cfg.gnupg.qubes-split-gpg.enable == true || cfg.age.keyFile != null - || cfg.age.sshKeyPaths != [ ]; + || 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.sshKeyPaths != []) && !(cfg.gnupg.home != null && cfg.gnupg.qubes-split-gpg.enable == true) - && !(cfg.gnupg.sshKeyPaths != [ ] && 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 + == false || ( - cfg.gnupg.qubes-split-gpg.enable == true + cfg.gnupg.qubes-split-gpg.enable + == true && cfg.gnupg.qubes-split-gpg.domain != null && cfg.gnupg.qubes-split-gpg.domain != "" ); @@ -357,7 +360,7 @@ in sops.environment = { SOPS_GPG_EXEC = lib.mkMerge [ - (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != [ ]) ( + (lib.mkIf (cfg.gnupg.home != null || cfg.gnupg.sshKeyPaths != []) ( lib.mkDefault "${cfg.gnupg.package}/bin/gpg" )) (lib.mkIf cfg.gnupg.qubes-split-gpg.enable ( @@ -365,12 +368,11 @@ in )) ]; - PATH = - let - pluginPaths = lib.makeBinPath cfg.age.plugins; - systemPaths = lib.optionalString pkgs.stdenv.isDarwin "/usr/bin:/bin:/usr/sbin:/sbin"; - in - lib.concatStringsSep ":" (lib.filter (p: p != "") [ pluginPaths systemPaths ]); + PATH = let + pluginPaths = lib.makeBinPath cfg.age.plugins; + systemPaths = lib.optionalString pkgs.stdenv.isDarwin "/usr/bin:/bin:/usr/sbin:/sbin"; + in + lib.concatStringsSep ":" (lib.filter (p: p != "") [pluginPaths systemPaths]); QUBES_GPG_DOMAIN = lib.mkIf cfg.gnupg.qubes-split-gpg.enable ( lib.mkDefault cfg.gnupg.qubes-split-gpg.domain @@ -389,7 +391,9 @@ in ExecStart = script; }; Install.WantedBy = - if cfg.gnupg.home != null then [ "graphical-session-pre.target" ] else [ "default.target" ]; + if cfg.gnupg.home != null + then ["graphical-session-pre.target"] + else ["default.target"]; }; # Darwin: load secrets once on login @@ -406,36 +410,32 @@ 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 - ''; - - linux = - let - systemctl = config.systemd.user.systemctlPath; - in - '' - systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) + 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 + ''; - 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 + linux = let + systemctl = config.systemd.user.systemctlPath; + in '' + systemdStatus=$(${systemctl} --user is-system-running 2>&1 || true) - unset systemdStatus - ''; + 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 - in - { - sops-nix = if pkgs.stdenv.isLinux then linux else darwin; - }; + unset systemdStatus + ''; + in { + sops-nix = + if pkgs.stdenv.isLinux + then linux + else darwin; + }; }; } From 2aa784f5383ebc74d99f63c2ce6e431dc9eec8a6 Mon Sep 17 00:00:00 2001 From: Jesse Young Lin Date: Wed, 6 May 2026 15:59:42 -0500 Subject: [PATCH 2/5] feat: add materializePath --- modules/home-manager/sops.nix | 51 ++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 3bc29f22..e152d856 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -73,6 +73,18 @@ Sops file the secret is loaded from. ''; }; + + materializePath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Optional absolute path where this secret should be copied as a real + file during home-manager activation. + + If the destination already exists (regular file or symlink), + activation verifies checksums match and fails if they differ. + ''; + }; }; } ); @@ -431,11 +443,38 @@ in { unset systemdStatus ''; - in { - sops-nix = - if pkgs.stdenv.isLinux - then linux - else darwin; - }; + + secretsToMaterialize = lib.filter (secret: secret.materializePath != null) (builtins.attrValues cfg.secrets); + materializeSecret = secret: let + src = lib.escapeShellArg secret.path; + dst = lib.escapeShellArg secret.materializePath; + dstDir = lib.escapeShellArg (builtins.dirOf secret.materializePath); + in '' + mkdir -p ${dstDir} + + if [ -e ${dst} ] || [ -L ${dst} ]; then + src_sha="$(${pkgs.coreutils}/bin/sha256sum ${src} | cut -d ' ' -f1)" + dst_sha="$(${pkgs.coreutils}/bin/sha256sum ${dst} | cut -d ' ' -f1)" + + if [ "$src_sha" != "$dst_sha" ]; then + echo "ERROR: materialized secret exists but checksum differs: ${secret.materializePath}" >&2 + exit 1 + fi + else + ${pkgs.coreutils}/bin/cp --no-clobber --no-preserve=mode,ownership ${src} ${dst} + ${pkgs.coreutils}/bin/chmod 600 ${dst} + fi + ''; + materializeScript = lib.concatMapStringsSep "\n\n" materializeSecret secretsToMaterialize; + in + { + sops-nix = + if pkgs.stdenv.isLinux + then linux + else darwin; + } + // lib.optionalAttrs (secretsToMaterialize != []) { + sops-materialize-secrets = lib.hm.dag.entryAfter ["sops-nix" "writeBoundary"] materializeScript; + }; }; } From 8d6929c145287f8041d13c8e9de2c14ebea3fa71 Mon Sep 17 00:00:00 2001 From: Jesse Young Lin Date: Thu, 7 May 2026 10:27:06 -0500 Subject: [PATCH 3/5] refactor: use a bash function --- modules/home-manager/sops.nix | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index e152d856..f5e12fa0 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -445,27 +445,33 @@ in { ''; secretsToMaterialize = lib.filter (secret: secret.materializePath != null) (builtins.attrValues cfg.secrets); + materializeSecretFn = '' + materialize_secret() { + local src="$1" + local dst="$2" + local dstDir="$(dirname "$2")" + + mkdir -p "$dstDir" + + if [ -e "$dst" ] || [ -L "$dst" ]; then + src_sha="$(${pkgs.coreutils}/bin/sha256sum "$src" | cut -d ' ' -f1)" + dst_sha="$(${pkgs.coreutils}/bin/sha256sum "$dst" | cut -d ' ' -f1)" + + if [ "$src_sha" != "$dst_sha" ]; then + echo "ERROR: materialized secret exists but checksum differs: $dst" >&2 + exit 1 + fi + else + ${pkgs.coreutils}/bin/cp --no-clobber --no-preserve=mode,ownership "$src" "$dst" + ${pkgs.coreutils}/bin/chmod 600 "$dst" + fi + } + ''; materializeSecret = secret: let src = lib.escapeShellArg secret.path; dst = lib.escapeShellArg secret.materializePath; - dstDir = lib.escapeShellArg (builtins.dirOf secret.materializePath); - in '' - mkdir -p ${dstDir} - - if [ -e ${dst} ] || [ -L ${dst} ]; then - src_sha="$(${pkgs.coreutils}/bin/sha256sum ${src} | cut -d ' ' -f1)" - dst_sha="$(${pkgs.coreutils}/bin/sha256sum ${dst} | cut -d ' ' -f1)" - - if [ "$src_sha" != "$dst_sha" ]; then - echo "ERROR: materialized secret exists but checksum differs: ${secret.materializePath}" >&2 - exit 1 - fi - else - ${pkgs.coreutils}/bin/cp --no-clobber --no-preserve=mode,ownership ${src} ${dst} - ${pkgs.coreutils}/bin/chmod 600 ${dst} - fi - ''; - materializeScript = lib.concatMapStringsSep "\n\n" materializeSecret secretsToMaterialize; + in "materialize_secret ${src} ${dst}"; + materializeScript = materializeSecretFn + lib.concatMapStringsSep "\n\n" materializeSecret secretsToMaterialize; in { sops-nix = From 6fded1feb196c77cb0c6cc30391aba72e9676258 Mon Sep 17 00:00:00 2001 From: Jesse Young Lin Date: Thu, 7 May 2026 10:31:12 -0500 Subject: [PATCH 4/5] refactor: remove dep on writeBoundary --- 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 f5e12fa0..9f5f5ec9 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -480,7 +480,7 @@ in { else darwin; } // lib.optionalAttrs (secretsToMaterialize != []) { - sops-materialize-secrets = lib.hm.dag.entryAfter ["sops-nix" "writeBoundary"] materializeScript; + sops-materialize-secrets = lib.hm.dag.entryAfter ["sops-nix"] materializeScript; }; }; } From 4fcc104dddb9977bb4bc77da44c14de0f45fbb10 Mon Sep 17 00:00:00 2001 From: Jesse Young Lin Date: Thu, 7 May 2026 11:34:01 -0500 Subject: [PATCH 5/5] docs: simplify description --- modules/home-manager/sops.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/home-manager/sops.nix b/modules/home-manager/sops.nix index 9f5f5ec9..d5dd0a89 100644 --- a/modules/home-manager/sops.nix +++ b/modules/home-manager/sops.nix @@ -80,9 +80,6 @@ description = '' Optional absolute path where this secret should be copied as a real file during home-manager activation. - - If the destination already exists (regular file or symlink), - activation verifies checksums match and fails if they differ. ''; }; };