mirror of
https://github.com/Mic92/sops-nix.git
synced 2025-04-18 02:06:50 +00:00
Allow setting user passwords
This commit is contained in:
parent
79706f6748
commit
bac08f6919
4 changed files with 142 additions and 51 deletions
19
README.md
19
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.<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
|
||||
|
||||
At the moment we support the following file formats: YAML, JSON, binary
|
||||
|
|
|
@ -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 <xref linkend="opt-systemd.services._name_.restartTriggers" />.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
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
|
||||
'');
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Reference in a new issue