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