Allow setting user passwords

This commit is contained in:
Janne Heß 2021-10-19 18:26:43 +02:00
parent 79706f6748
commit bac08f6919
No known key found for this signature in database
GPG key ID: 69165158F05265DF
4 changed files with 142 additions and 51 deletions

View file

@ -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 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.<name>.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 ## Different file formats
At the moment we support the following file formats: YAML, JSON, binary At the moment we support the following file formats: YAML, JSON, binary

View file

@ -5,6 +5,9 @@ with lib;
let let
cfg = config.sops; cfg = config.sops;
users = config.users.users; 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, ... }: { secretType = types.submodule ({ config, ... }: {
config = { config = {
sopsFile = lib.mkOptionDefault cfg.defaultSopsFile; sopsFile = lib.mkOptionDefault cfg.defaultSopsFile;
@ -29,7 +32,8 @@ let
}; };
path = mkOption { path = mkOption {
type = types.str; 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 = '' description = ''
Path where secrets are symlinked to. Path where secrets are symlinked to.
If the default is kept no symlink is created. If the default is kept no symlink is created.
@ -87,31 +91,45 @@ let
This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />. This works the same way as <xref linkend="opt-systemd.services._name_.restartTriggers" />.
''; '';
}; };
}; neededForUsers = mkOption {
}); type = types.bool;
manifest = pkgs.writeText "manifest.json" (builtins.toJSON { default = false;
secrets = builtins.attrValues cfg.secrets; description = ''
# Does this need to be configurable? Enabling this option causes the secret to be decrypted before users and groups are created.
secretsMountPoint = "/run/secrets.d"; This can be used to retrieve user's passwords from sops-nix.
symlinkPath = "/run/secrets"; Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
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;
}; };
}); });
checkedManifest = let manifestFor = suffix: secrets: extraJson: pkgs.writeTextFile {
sops-install-secrets = (pkgs.buildPackages.callPackage ../.. {}).sops-install-secrets; name = "manifest${suffix}.json";
in pkgs.runCommand "checked-manifest.json" { text = builtins.toJSON ({
nativeBuildInputs = [ sops-install-secrets ]; secrets = builtins.attrValues secrets;
} '' # Does this need to be configurable?
sops-install-secrets -check-mode=${if cfg.validateSopsFiles then "sopsfile" else "manifest"} ${manifest} secretsMountPoint = "/run/secrets.d";
cp ${manifest} $out 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 { in {
options.sops = { options.sops = {
secrets = mkOption { secrets = mkOption {
@ -214,6 +232,9 @@ in {
} { } {
assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []); assertion = !(cfg.gnupg.home != null && cfg.gnupg.sshKeyPaths != []);
message = "Exactly one of sops.gnupg.home and sops.gnupg.sshKeyPaths must be set"; 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 ( }] ++ optionals cfg.validateSopsFiles (
concatLists (mapAttrsToList (name: secret: [{ concatLists (mapAttrsToList (name: secret: [{
assertion = builtins.pathExists secret.sopsFile; assertion = builtins.pathExists secret.sopsFile;
@ -226,22 +247,33 @@ in {
}]) cfg.secrets) }]) cfg.secrets)
); );
system.activationScripts.setup-secrets = let system.activationScripts = {
sops-install-secrets = (pkgs.callPackage ../.. {}).sops-install-secrets; setupSecretsForUsers = mkIf (secretsForUsers != {}) (stringAfter ([ "specialfs" ] ++ optional cfg.age.generateKey "generate-age-key") ''
in (stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") '' [ -e /run/current-system ] || echo setting up secrets for users...
[ -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 -ignore-passwd ${manifestForUsers}
${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) {
'') // lib.optionalAttrs (config.system ? dryActivationScript) { supportsDryActivation = true;
supportsDryActivation = true; });
};
system.activationScripts.generate-age-key = (mkIf cfg.age.generateKey) (stringAfter [] '' users = mkIf (secretsForUsers != {}) {
if [[ ! -f '${cfg.age.keyFile}' ]]; then deps = [ "setupSecretsForUsers" ];
echo generating machine-specific age key... };
mkdir -p $(dirname ${cfg.age.keyFile})
# age-keygen sets 0600 by default, no need to chmod. setupSecrets = mkIf (regularSecrets != {}) (stringAfter ([ "specialfs" "users" "groups" ] ++ optional cfg.age.generateKey "generate-age-key") ''
${pkgs.age}/bin/age-keygen -o ${cfg.age.keyFile} [ -e /run/current-system ] || echo setting up secrets...
fi ${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
'');
};
}; };
} }

View file

@ -102,14 +102,16 @@ const (
) )
type options struct { type options struct {
checkMode CheckMode checkMode CheckMode
manifest string manifest string
ignorePasswd bool
} }
type appContext struct { type appContext struct {
manifest manifest manifest manifest
secretFiles map[string]secretFile secretFiles map[string]secretFile
checkMode CheckMode checkMode CheckMode
ignorePasswd bool
} }
func secureSymlinkChown(symlinkToCheck, expectedTarget string, owner, group int) error { 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) 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 // we only access to the user/group during deployment
owner, err := user.Lookup(secret.Owner) owner, err := user.Lookup(secret.Owner)
if err != nil { if err != nil {
@ -785,6 +790,7 @@ func parseFlags(args []string) (*options, error) {
} }
var checkMode string var checkMode string
fs.StringVar(&checkMode, "check-mode", "off", `Validate configuration without installing it (possible values: "manifest","sopsfile","off")`) 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 { if err := fs.Parse(args[1:]); err != nil {
return nil, err return nil, err
} }
@ -816,9 +822,10 @@ func installSecrets(args []string) error {
} }
app := appContext{ app := appContext{
manifest: *manifest, manifest: *manifest,
checkMode: opts.checkMode, checkMode: opts.checkMode,
secretFiles: make(map[string]secretFile), ignorePasswd: opts.ignorePasswd,
secretFiles: make(map[string]secretFile),
} }
if err := app.validateManifest(); err != nil { if err := app.validateManifest(); err != nil {
@ -829,9 +836,14 @@ func installSecrets(args []string) error {
return nil return nil
} }
keysGid, err := lookupKeysGroup() var keysGid int
if err != nil { if opts.ignorePasswd {
return err keysGid = 0
} else {
keysGid, err = lookupKeysGroup()
if err != nil {
return err
}
} }
isDry := os.Getenv("NIXOS_ACTION") == "dry-activate" isDry := os.Getenv("NIXOS_ACTION") == "dry-activate"

View file

@ -23,6 +23,34 @@
inherit (pkgs) system; 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 { age-keys = makeTest {
name = "sops-age-keys"; name = "sops-age-keys";
machine = { machine = {