diff --git a/README.md b/README.md index 6202d33..5da95fc 100644 --- a/README.md +++ b/README.md @@ -589,6 +589,25 @@ $ ls -la /var/lib/hass/secrets.yaml lrwxrwxrwx 1 root root 40 Jul 19 22:36 /var/lib/hass/secrets.yaml -> /run/secrets/home-assistant-secrets.yaml ``` +## Setting a user's password + +sops-nix has to run after users were created by NixOS. +This means that it's not possible to set `users.users..passwordFile` to any secrets managed by sops-nix. +To work around this issue, it's possible to set `neededForUsers = true` in a secret. +This will cause the secret to be decrypted to `/run/secrets-for-users` instead of `/run/secrets` before NixOS creates the users. +As users are not created yet, it's not possible to set an owner for these secrets. + +```nix +{ config, ... }: { + sops.secrets.my-password.neededForUsers = true; + + users.users.mic92 = { + isNormalUser = true; + passwordFile = config.sops.secrets.my-password.path; + }; +} +``` + ## Different file formats At the moment we support the following file formats: YAML, JSON, binary diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 889cb03..f0d0fd3 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -5,6 +5,9 @@ with lib; let cfg = config.sops; users = config.users.users; + sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; + regularSecrets = lib.filterAttrs (_: v: !v.neededForUsers) cfg.secrets; + secretsForUsers = lib.filterAttrs (_: v: v.neededForUsers) cfg.secrets; secretType = types.submodule ({ config, ... }: { config = { sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; @@ -29,7 +32,8 @@ let }; path = mkOption { type = types.str; - default = "/run/secrets/${config.name}"; + 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. @@ -87,31 +91,45 @@ let This works the same way as . ''; }; - }; - }); - manifest = pkgs.writeText "manifest.json" (builtins.toJSON { - secrets = builtins.attrValues cfg.secrets; - # Does this need to be configurable? - secretsMountPoint = "/run/secrets.d"; - symlinkPath = "/run/secrets"; - gnupgHome = cfg.gnupg.home; - sshKeyPaths = cfg.gnupg.sshKeyPaths; - ageKeyFile = cfg.age.keyFile; - ageSshKeyPaths = cfg.age.sshKeyPaths; - logging = { - keyImport = builtins.elem "keyImport" cfg.log; - secretChanges = builtins.elem "secretChanges" cfg.log; + neededForUsers = mkOption { + type = 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. + ''; + }; }; }); - checkedManifest = let - sops-install-secrets = (pkgs.buildPackages.callPackage ../.. {}).sops-install-secrets; - in pkgs.runCommand "checked-manifest.json" { - nativeBuildInputs = [ sops-install-secrets ]; - } '' - sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} ${manifest} - cp ${manifest} $out - ''; + 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"; + gnupgHome = cfg.gnupg.home; + sshKeyPaths = cfg.gnupg.sshKeyPaths; + ageKeyFile = cfg.age.keyFile; + ageSshKeyPaths = cfg.age.sshKeyPaths; + logging = { + keyImport = builtins.elem "keyImport" cfg.log; + secretChanges = builtins.elem "secretChanges" cfg.log; + }; + } // extraJson); + checkPhase = '' + ${sops-install-secrets}/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.d/users"; + symlinkPath = "/run/secrets-for-users"; + }; + in { options.sops = { secrets = mkOption { @@ -214,6 +232,9 @@ in { } { 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: [{ assertion = builtins.pathExists secret.sopsFile; @@ -226,22 +247,33 @@ in { }]) cfg.secrets) ); - system.activationScripts.setup-secrets = let - sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; - in (stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") '' - [ -e /run/current-system ] || echo setting up secrets... - ${optionalString (cfg.gnupg.home != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${checkedManifest} - '') // lib.optionalAttrs (config.system ? dryActivationScript) { - supportsDryActivation = true; - }; + system.activationScripts = { + setupSecretsForUsers = mkIf (secretsForUsers != {}) (stringAfter ([ "specialfs" ] ++ optional cfg.age.generateKey "generate-age-key") '' + [ -e /run/current-system ] || echo setting up secrets for users... + ${optionalString (cfg.gnupg.home != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifestForUsers} + '' // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + }); - system.activationScripts.generate-age-key = (mkIf cfg.age.generateKey) (stringAfter [] '' - if [[ ! -f '${cfg.age.keyFile}' ]]; then - echo generating machine-specific age key... - mkdir -p $(dirname ${cfg.age.keyFile}) - # age-keygen sets 0600 by default, no need to chmod. - ${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile} - fi - ''); + users = mkIf (secretsForUsers != {}) { + deps = [ "setupSecretsForUsers" ]; + }; + + setupSecrets = mkIf (regularSecrets != {}) (stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") '' + [ -e /run/current-system ] || echo setting up secrets... + ${optionalString (cfg.gnupg.home != null) "SOPS_GPG_EXEC=${pkgs.gnupg}/bin/gpg"} ${sops-install-secrets}/bin/sops-install-secrets ${manifest} + '' // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + }); + + generate-age-key = mkIf (cfg.age.generateKey) (stringAfter [] '' + if [[ ! -f '${cfg.age.keyFile}' ]]; then + echo generating machine-specific age key... + mkdir -p $(dirname ${cfg.age.keyFile}) + # age-keygen sets 0600 by default, no need to chmod. + ${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile} + fi + ''); + }; }; } diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 7c46ffa..2949c5e 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -102,14 +102,16 @@ const ( ) type options struct { - checkMode CheckMode - manifest string + checkMode CheckMode + manifest string + ignorePasswd bool } type appContext struct { - manifest manifest - secretFiles map[string]secretFile - checkMode CheckMode + manifest manifest + secretFiles map[string]secretFile + checkMode CheckMode + ignorePasswd bool } func secureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) error { @@ -451,7 +453,10 @@ func (app *appContext) validateSecret(secret *secret) error { } secret.mode = os.FileMode(mode) - if app.checkMode == Off { + if app.ignorePasswd { + secret.owner = 0 + secret.group = 0 + } else if app.checkMode == Off { // we only access to the user/group during deployment owner, err := user.Lookup(secret.Owner) if err != nil { @@ -785,6 +790,7 @@ func parseFlags(args []string) (*options, error) { } var checkMode string fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`) + fs.BoolVar(&opts.ignorePasswd, "ignore-passwd", false, `Don't look up anything in /etc/passwd. Causes everything to be owned by root:root`) if err := fs.Parse(args[1:]); err != nil { return nil, err } @@ -816,9 +822,10 @@ func installSecrets(args []string) error { } app := appContext{ - manifest: *manifest, - checkMode: opts.checkMode, - secretFiles: make(map[string]secretFile), + manifest: *manifest, + checkMode: opts.checkMode, + ignorePasswd: opts.ignorePasswd, + secretFiles: make(map[string]secretFile), } if err := app.validateManifest(); err != nil { @@ -829,9 +836,14 @@ func installSecrets(args []string) error { return nil } - keysGid, err := lookupKeysGroup() - if err != nil { - return err + var keysGid int + if opts.ignorePasswd { + keysGid = 0 + } else { + keysGid, err = lookupKeysGroup() + if err != nil { + return err + } } isDry := os.Getenv("NIXOS_ACTION") == "dry-activate" diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 5f60dd9..228dafe 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -23,6 +23,34 @@ inherit (pkgs) system; }; + user-passwords = makeTest { + name = "sops-user-passwords"; + machine = { + imports = [ ../../modules/sops ]; + sops = { + age.keyFile = ./test-assets/age-keys.txt; + defaultSopsFile = ./test-assets/secrets.yaml; + secrets.test_key.neededForUsers = true; + secrets."nested/test/file".owner = "example-user"; + }; + + users.users.example-user = { + isNormalUser = true; + passwordFile = "/run/secrets-for-users/test_key"; + }; + }; + + testScript = '' + start_all() + 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... + machine.succeed("[ $(stat -c%U /run/secrets/nested/test/file) = example-user ]") # ...and are owned + ''; + } { + inherit pkgs; + inherit (pkgs) system; + }; + age-keys = makeTest { name = "sops-age-keys"; machine = {