From a89d01b2731db5445278706411cce8c01246e103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 8 Feb 2024 13:11:04 +0100 Subject: [PATCH 1/2] move secrets-fo-users to it's own module This preparation to support sysusers. No behavior change. --- modules/sops/default.nix | 214 ++++++++------------- modules/sops/manifest-for.nix | 27 +++ modules/sops/secrets-for-users/default.nix | 37 ++++ modules/sops/with-environment.nix | 12 ++ 4 files changed, 160 insertions(+), 130 deletions(-) create mode 100644 modules/sops/manifest-for.nix create mode 100644 modules/sops/secrets-for-users/default.nix create mode 100644 modules/sops/with-environment.nix diff --git a/modules/sops/default.nix b/modules/sops/default.nix index a3f1c965..37fa8c6e 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -1,29 +1,35 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.sops; users = config.users.users; sops-install-secrets = cfg.package; - sops-install-secrets-check = cfg.validationPackage; + manifestFor = pkgs.callPackage ./manifest-for.nix { + inherit cfg; + inherit (pkgs) writeTextFile; + }; + manifest = manifestFor "" regularSecrets {}; + regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets; - secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; - secretType = types.submodule ({ config, ... }: { + + withEnvironment = import ./with-environment.nix { + inherit cfg lib; + }; + secretType = lib.types.submodule ({ config, ... }: { config = { sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; - sopsFileHash = mkOptionDefault (optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); + sopsFileHash = lib.mkOptionDefault (lib.optionalString cfg.validateSopsFiles "${builtins.hashFile "sha256" config.sopsFile}"); }; options = { - name = mkOption { - type = types.str; + name = lib.mkOption { + type = lib.types.str; default = config._module.args.name; description = '' Name of the file used in /run/secrets ''; }; - key = mkOption { - type = types.str; + key = lib.mkOption { + type = lib.types.str; default = config._module.args.name; description = '' Key used to lookup in the sops file. @@ -31,8 +37,8 @@ let This option is ignored if format is binary. ''; }; - path = mkOption { - type = types.str; + 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 = '' @@ -40,52 +46,52 @@ let If the default is kept no symlink is created. ''; }; - format = mkOption { - type = types.enum ["yaml" "json" "binary" "dotenv" "ini"]; + 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 = mkOption { - type = types.str; + mode = lib.mkOption { + type = lib.types.str; default = "0400"; description = '' Permissions mode of the in octal. ''; }; - owner = mkOption { - type = types.str; + owner = lib.mkOption { + type = lib.types.str; default = "root"; description = '' User of the file. ''; }; - group = mkOption { - type = types.str; + group = lib.mkOption { + type = lib.types.str; default = users.${config.owner}.group; - defaultText = literalMD "{option}`config.users.users.\${owner}.group`"; + defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`"; description = '' Group of the file. ''; }; - sopsFile = mkOption { - type = types.path; + sopsFile = lib.mkOption { + type = lib.types.path; defaultText = "\${config.sops.defaultSopsFile}"; description = '' Sops file the secret is loaded from. ''; }; - sopsFileHash = mkOption { - type = types.str; + sopsFileHash = lib.mkOption { + type = lib.types.str; readOnly = true; description = '' Hash of the sops file, useful in . ''; }; - restartUnits = mkOption { - type = types.listOf types.str; + restartUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; default = [ ]; example = [ "sshd.service" ]; description = '' @@ -93,8 +99,8 @@ let This works the same way as . ''; }; - reloadUnits = mkOption { - type = types.listOf types.str; + reloadUnits = lib.mkOption { + type = lib.types.listOf lib.types.str; default = [ ]; example = [ "sshd.service" ]; description = '' @@ -102,8 +108,8 @@ let This works the same way as . ''; }; - neededForUsers = mkOption { - type = types.bool; + 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. @@ -114,42 +120,6 @@ let }; }); - manifestFor = suffix: secrets: extraJson: pkgs.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 = cfg.useTmpfs; - userMode = false; - logging = { - keyImport = builtins.elem "keyImport" cfg.log; - secretChanges = builtins.elem "secretChanges" cfg.log; - }; - } // extraJson); - checkPhase = '' - ${sops-install-secrets-check}/bin/sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} "$out" - ''; - }; - - manifest = manifestFor "" regularSecrets {}; - manifestForUsers = manifestFor "-for-users" secretsForUsers { - secretsMountPoint = "/run/secrets-for-users.d"; - symlinkPath = "/run/secrets-for-users"; - }; - - withEnvironment = sopsCall: if cfg.environment == {} then sopsCall else '' - ( - ${concatStringsSep "\n" (mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} - ${sopsCall} - ) - ''; # Skip ssh keys deployed with sops to avoid a catch 22 defaultImportKeys = algo: if config.services.openssh.enable then @@ -158,31 +128,31 @@ let []; in { options.sops = { - secrets = mkOption { - type = types.attrsOf secretType; + secrets = lib.mkOption { + type = lib.types.attrsOf secretType; default = {}; description = '' Path where the latest secrets are mounted to. ''; }; - defaultSopsFile = mkOption { - type = types.path; + defaultSopsFile = lib.mkOption { + type = lib.types.path; description = '' Default sops file used for all secrets. ''; }; - defaultSopsFormat = mkOption { - type = types.str; + defaultSopsFormat = lib.mkOption { + type = lib.types.str; default = "yaml"; description = '' Default sops format used for all secrets. ''; }; - validateSopsFiles = mkOption { - type = types.bool; + validateSopsFiles = lib.mkOption { + type = lib.types.bool; default = true; description = '' Check all sops files at evaluation time. @@ -190,22 +160,22 @@ in { ''; }; - keepGenerations = mkOption { - type = types.ints.unsigned; + keepGenerations = lib.mkOption { + type = lib.types.ints.unsigned; default = 1; description = '' Number of secrets generations to keep. Setting this to 0 disables pruning. ''; }; - log = mkOption { - type = types.listOf (types.enum [ "keyImport" "secretChanges" ]); + log = lib.mkOption { + type = lib.types.listOf (lib.types.enum [ "keyImport" "secretChanges" ]); default = [ "keyImport" "secretChanges" ]; description = "What to log"; }; - environment = mkOption { - type = types.attrsOf (types.either types.str types.path); + environment = lib.mkOption { + type = lib.types.attrsOf (lib.types.either lib.types.str lib.types.path); default = {}; description = '' Environment variables to set before calling sops-install-secrets. @@ -219,22 +189,22 @@ in { ''; }; - package = mkOption { - type = types.package; + package = lib.mkOption { + type = lib.types.package; default = (pkgs.callPackage ../.. {}).sops-install-secrets; - defaultText = literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; + defaultText = lib.literalExpression "(pkgs.callPackage ../.. {}).sops-install-secrets"; description = '' sops-install-secrets package to use. ''; }; - validationPackage = mkOption { - type = types.package; + 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; - defaultText = literalExpression "config.sops.package"; + defaultText = lib.literalExpression "config.sops.package"; description = '' sops-install-secrets package to use when validating configuration. @@ -243,8 +213,8 @@ in { ''; }; - useTmpfs = mkOption { - type = types.bool; + useTmpfs = lib.mkOption { + type = lib.types.bool; default = false; description = lib.mdDoc '' Use tmpfs in place of ramfs for secrets storage. @@ -264,8 +234,8 @@ in { }; age = { - keyFile = mkOption { - type = types.nullOr types.path; + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; default = null; example = "/var/lib/sops-nix/key.txt"; description = '' @@ -273,8 +243,8 @@ in { ''; }; - generateKey = mkOption { - type = types.bool; + generateKey = lib.mkOption { + type = lib.types.bool; default = false; description = '' Whether or not to generate the age key. If this @@ -283,10 +253,10 @@ in { ''; }; - sshKeyPaths = mkOption { - type = types.listOf types.path; + sshKeyPaths = lib.mkOption { + type = lib.types.listOf lib.types.path; default = defaultImportKeys "ed25519"; - defaultText = literalMD "The ed25519 keys from {option}`config.services.openssh.hostKeys`"; + defaultText = lib.literalMD "The ed25519 keys from {option}`config.services.openssh.hostKeys`"; description = '' Paths to ssh keys added as age keys during sops description. ''; @@ -294,8 +264,8 @@ in { }; gnupg = { - home = mkOption { - type = types.nullOr types.str; + home = lib.mkOption { + type = lib.types.nullOr lib.types.str; default = null; example = "/root/.gnupg"; description = '' @@ -303,10 +273,10 @@ in { ''; }; - sshKeyPaths = mkOption { - type = types.listOf types.path; + sshKeyPaths = lib.mkOption { + type = lib.types.listOf lib.types.path; default = defaultImportKeys "rsa"; - defaultText = literalMD "The rsa keys from {option}`config.services.openssh.hostKeys`"; + defaultText = lib.literalMD "The rsa keys from {option}`config.services.openssh.hostKeys`"; 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. @@ -316,54 +286,41 @@ in { }; imports = [ ./templates - (mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) - (mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) + ./secrets-for-users + (lib.mkRenamedOptionModule [ "sops" "gnupgHome" ] [ "sops" "gnupg" "home" ]) + (lib.mkRenamedOptionModule [ "sops" "sshKeyPaths" ] [ "sops" "gnupg" "sshKeyPaths" ]) ]; - config = mkMerge [ - (mkIf (cfg.secrets != {}) { + 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"; - } { - assertion = (filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == {}; - message = "neededForUsers cannot be used for secrets that are not root-owned"; - }] ++ optionals cfg.validateSopsFiles ( - concatLists (mapAttrsToList (name: secret: [{ + }] ++ 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.${strings.escapeNixIdentifier name}.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 && hasPrefix builtins.storeDir 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) ); - sops.environment.SOPS_GPG_EXEC = mkIf (cfg.gnupg.home != null) (mkDefault "${pkgs.gnupg}/bin/gpg"); + sops.environment.SOPS_GPG_EXEC = lib.mkIf (cfg.gnupg.home != null) (lib.mkDefault "${pkgs.gnupg}/bin/gpg"); system.activationScripts = { - setupSecretsForUsers = mkIf (secretsForUsers != {}) (stringAfter ([ "specialfs" ] ++ optional cfg.age.generateKey "generate-age-key") '' - [ -e /run/current-system ] || echo setting up secrets for users... - ${withEnvironment "${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers}"} - '' // lib.optionalAttrs (config.system ? dryActivationScript) { - supportsDryActivation = true; - }); - - users = mkIf (secretsForUsers != {}) { - deps = [ "setupSecretsForUsers" ]; - }; - - setupSecrets = mkIf (regularSecrets != {}) (stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") '' + setupSecrets = lib.mkIf (regularSecrets != {}) (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 = mkIf (cfg.age.generateKey) (stringAfter [] '' + generate-age-key = lib.mkIf (cfg.age.generateKey) (lib.stringAfter [] '' if [[ ! -f '${cfg.age.keyFile}' ]]; then echo generating machine-specific age key... mkdir -p $(dirname ${cfg.age.keyFile}) @@ -374,10 +331,7 @@ in { }; }) { - system.build = { - sops-nix-users-manifest = manifestForUsers; - sops-nix-manifest = manifest; - }; + system.build.sops-nix-manifest = manifest; } ]; } diff --git a/modules/sops/manifest-for.nix b/modules/sops/manifest-for.nix new file mode 100644 index 00000000..0752909c --- /dev/null +++ b/modules/sops/manifest-for.nix @@ -0,0 +1,27 @@ +{ writeTextFile, cfg }: + +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 = cfg.useTmpfs; + 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" + ''; +} diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix new file mode 100644 index 00000000..d009c804 --- /dev/null +++ b/modules/sops/secrets-for-users/default.nix @@ -0,0 +1,37 @@ +{ lib, config, pkgs, ... }: +let + cfg = config.sops; + secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; + manifestFor = pkgs.callPackage ../manifest-for.nix { + inherit cfg; + inherit (pkgs) writeTextFile; + }; + withEnvironment = import ../with-environment.nix { + inherit cfg lib; + }; + manifestForUsers = manifestFor "-for-users" secretsForUsers { + secretsMountPoint = "/run/secrets-for-users.d"; + symlinkPath = "/run/secrets-for-users"; + }; +in +{ + system.activationScripts = lib.mkIf (secretsForUsers != {}) { + setupSecretsForUsers = lib.mkIf (secretsForUsers != {}) (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 = lib.mkIf (secretsForUsers != {}) { + deps = [ "setupSecretsForUsers" ]; + }; + }; + + assertions = [{ + assertion = (lib.filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == {}; + message = "neededForUsers cannot be used for secrets that are not root-owned"; + }]; + + system.build.sops-nix-users-manifest = manifestForUsers; +} diff --git a/modules/sops/with-environment.nix b/modules/sops/with-environment.nix new file mode 100644 index 00000000..d19d5fd5 --- /dev/null +++ b/modules/sops/with-environment.nix @@ -0,0 +1,12 @@ +{ cfg, lib }: + +sopsCall: + +if cfg.environment == {} then + sopsCall +else '' + ( + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: " export ${n}='${v}'") cfg.environment)} + ${sopsCall} + ) +'' From 69fe41fb420579c679488d347bc4399dc12f7745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 8 Feb 2024 13:18:48 +0100 Subject: [PATCH 2/2] bump nixos-stable release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/79a13f1437e149dc7be2d1290c74d378dad60814' (2024-02-03) → 'github:NixOS/nixpkgs/f8e2ebd66d097614d51a56a755450d4ae1632df1' (2024-02-07) • Updated input 'nixpkgs-stable': 'github:NixOS/nixpkgs/9a333eaa80901efe01df07eade2c16d183761fa3' (2024-01-22) → 'github:NixOS/nixpkgs/bc6cb3d59b7aab88e967264254f8c1aa4c0284e9' (2024-02-08) --- flake.lock | 14 +++++++------- flake.nix | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flake.lock b/flake.lock index 08ccfe00..1cf95632 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1706925685, - "narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=", + "lastModified": 1707268954, + "narHash": "sha256-2en1kvde3cJVc3ZnTy8QeD2oKcseLFjYPLKhIGDanQ0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "79a13f1437e149dc7be2d1290c74d378dad60814", + "rev": "f8e2ebd66d097614d51a56a755450d4ae1632df1", "type": "github" }, "original": { @@ -18,16 +18,16 @@ }, "nixpkgs-stable": { "locked": { - "lastModified": 1705957679, - "narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=", + "lastModified": 1707391491, + "narHash": "sha256-TyDXcq8Z3slMNeyeF+ke0BzISWuM6NrBklr7XyiRbZA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9a333eaa80901efe01df07eade2c16d183761fa3", + "rev": "bc6cb3d59b7aab88e967264254f8c1aa4c0284e9", "type": "github" }, "original": { "owner": "NixOS", - "ref": "release-23.05", + "ref": "release-23.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index b038c2dd..67ed3878 100644 --- a/flake.nix +++ b/flake.nix @@ -1,9 +1,9 @@ { description = "Integrates sops into nixos"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-23.05"; - nixConfig.extra-substituters = ["https://cache.garnix.io"]; - nixConfig.extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="]; + inputs.nixpkgs-stable.url = "github:NixOS/nixpkgs/release-23.11"; + nixConfig.extra-substituters = ["https://cache.thalheim.io"]; + nixConfig.extra-trusted-public-keys = ["cache.thalheim.io-1:R7msbosLEZKrxk/lKxf9BTjOOH7Ax3H0Qj0/6wiHOgc="]; outputs = { self, nixpkgs, @@ -17,7 +17,7 @@ ]; 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 "-23_05"; + suffix-stable = suffix-version "-23_11"; in { overlays.default = final: prev: let localPkgs = import ./default.nix {pkgs = final;};