From a4c33bfecb93458d90f9eb26f1cf695b47285243 Mon Sep 17 00:00:00 2001 From: Martijn de Munnik Date: Wed, 16 Oct 2024 01:30:11 +0200 Subject: [PATCH] Allow to set uid and gid instead of owner and group. No checks will be performed when uid and gid are set. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` sops.secrets = { sslCertificate = { sopsFile = ./secrets.yaml; owner = ""; group = ""; uid = config.containers."nginx".config.users.users."nginx".uid; gid = config.containers."nginx".config.users.groups."nginx".gid; }; sslCertificateKey = { sopsFile = ./secrets.yaml; owner = ""; group = ""; uid = config.containers."nginx".config.users.users."nginx".uid; gid = config.containers."nginx".config.users.groups."nginx".gid; }; }; ``` Co-authored-by: Jörg Thalheim --- modules/sops/default.nix | 32 ++++++++++--- modules/sops/secrets-for-users/default.nix | 2 +- pkgs/sops-install-secrets/main.go | 47 +++++++++++-------- pkgs/sops-install-secrets/main_test.go | 43 +++++++++++------- pkgs/sops-install-secrets/nixos-test.nix | 52 ++++++++++++++++++++-- 5 files changed, 132 insertions(+), 44 deletions(-) diff --git a/modules/sops/default.nix b/modules/sops/default.nix index 1a2d793..b348070 100644 --- a/modules/sops/default.nix +++ b/modules/sops/default.nix @@ -73,18 +73,32 @@ let ''; }; owner = lib.mkOption { - type = lib.types.str; - default = "root"; + type = with lib.types; nullOr str; + default = null; description = '' - User of the file. + 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 = lib.types.str; - default = users.${config.owner}.group; + 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. + 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 { @@ -318,6 +332,12 @@ in { 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) ); diff --git a/modules/sops/secrets-for-users/default.nix b/modules/sops/secrets-for-users/default.nix index bb65532..7c316c4 100644 --- a/modules/sops/secrets-for-users/default.nix +++ b/modules/sops/secrets-for-users/default.nix @@ -43,7 +43,7 @@ in }; assertions = [{ - assertion = (lib.filterAttrs (_: v: v.owner != "root" || v.group != "root") secretsForUsers) == { }; + 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; diff --git a/pkgs/sops-install-secrets/main.go b/pkgs/sops-install-secrets/main.go index 2597fc5..6f51665 100644 --- a/pkgs/sops-install-secrets/main.go +++ b/pkgs/sops-install-secrets/main.go @@ -29,8 +29,10 @@ type secret struct { Name string `json:"name"` Key string `json:"key"` Path string `json:"path"` - Owner string `json:"owner"` - Group string `json:"group"` + Owner *string `json:"owner,omitempty"` + UID int `json:"uid"` + Group *string `json:"group,omitempty"` + GID int `json:"gid"` SopsFile string `json:"sopsFile"` Format FormatType `json:"format"` Mode string `json:"mode"` @@ -475,25 +477,34 @@ func (app *appContext) validateSecret(secret *secret) error { secret.group = 0 } else if app.checkMode == Off || app.ignorePasswd { // we only access to the user/group during deployment - owner, err := user.Lookup(secret.Owner) - if err != nil { - return fmt.Errorf("failed to lookup user '%s': %w", secret.Owner, err) - } - ownerNr, err := strconv.ParseUint(owner.Uid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err) - } - secret.owner = int(ownerNr) - group, err := user.LookupGroup(secret.Group) - if err != nil { - return fmt.Errorf("failed to lookup group '%s': %w", secret.Group, err) + if secret.Owner == nil { + secret.owner = secret.UID + } else { + owner, err := user.Lookup(*secret.Owner) + if err != nil { + return fmt.Errorf("failed to lookup user '%s': %w", *secret.Owner, err) + } + uid, err := strconv.ParseUint(owner.Uid, 10, 64) + if err != nil { + return fmt.Errorf("cannot parse uid %s: %w", owner.Uid, err) + } + secret.owner = int(uid) } - groupNr, err := strconv.ParseUint(group.Gid, 10, 64) - if err != nil { - return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) + + if secret.Group == nil { + secret.group = secret.GID + } else { + group, err := user.LookupGroup(*secret.Group) + if err != nil { + return fmt.Errorf("failed to lookup group '%s': %w", *secret.Group, err) + } + gid, err := strconv.ParseUint(group.Gid, 10, 64) + if err != nil { + return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) + } + secret.group = int(gid) } - secret.group = int(groupNr) } if secret.Format == "" { diff --git a/pkgs/sops-install-secrets/main_test.go b/pkgs/sops-install-secrets/main_test.go index e001c65..8c09e68 100644 --- a/pkgs/sops-install-secrets/main_test.go +++ b/pkgs/sops-install-secrets/main_test.go @@ -98,12 +98,14 @@ func testGPG(t *testing.T) { } }() + nobody := "nobody" + nogroup := "nogroup" // should create a symlink yamlSecret := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: path.Join(testdir.path, "test-target"), Mode: "0400", @@ -112,12 +114,13 @@ func testGPG(t *testing.T) { } var jsonSecret, binarySecret, dotenvSecret, iniSecret secret + root := "root" // should not create a symlink jsonSecret = yamlSecret jsonSecret.Name = "test2" - jsonSecret.Owner = "root" + jsonSecret.Owner = &root jsonSecret.Format = "json" - jsonSecret.Group = "root" + jsonSecret.Group = &root jsonSecret.SopsFile = path.Join(assets, "secrets.json") jsonSecret.Path = path.Join(testdir.secretsPath, "test2") jsonSecret.Mode = "0700" @@ -130,16 +133,16 @@ func testGPG(t *testing.T) { dotenvSecret = yamlSecret dotenvSecret.Name = "test4" - dotenvSecret.Owner = "root" - dotenvSecret.Group = "root" + dotenvSecret.Owner = &root + dotenvSecret.Group = &root dotenvSecret.Format = "dotenv" dotenvSecret.SopsFile = path.Join(assets, "secrets.env") dotenvSecret.Path = path.Join(testdir.secretsPath, "test4") iniSecret = yamlSecret iniSecret.Name = "test5" - iniSecret.Owner = "root" - iniSecret.Group = "root" + iniSecret.Owner = &root + iniSecret.Group = &root iniSecret.Format = "ini" iniSecret.SopsFile = path.Join(assets, "secrets.ini") iniSecret.Path = path.Join(testdir.secretsPath, "test5") @@ -214,11 +217,13 @@ func testSSHKey(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -247,11 +252,13 @@ func TestAge(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -280,11 +287,13 @@ func TestAgeWithSSH(t *testing.T) { ok(t, err) file.Close() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: target, Mode: "0400", @@ -314,11 +323,13 @@ func TestValidateManifest(t *testing.T) { testdir := newTestDir(t) defer testdir.Remove() + nobody := "nobody" + nogroup := "nogroup" s := secret{ Name: "test", Key: "test_key", - Owner: "nobody", - Group: "nogroup", + Owner: &nobody, + Group: &nogroup, SopsFile: path.Join(assets, "secrets.yaml"), Path: path.Join(testdir.path, "test-target"), Mode: "0400", diff --git a/pkgs/sops-install-secrets/nixos-test.nix b/pkgs/sops-install-secrets/nixos-test.nix index 488b8a3..4f3a760 100644 --- a/pkgs/sops-install-secrets/nixos-test.nix +++ b/pkgs/sops-install-secrets/nixos-test.nix @@ -112,12 +112,41 @@ in { age-keys = testers.runNixOSTest { name = "sops-age-keys"; - nodes.machine = { lib, ... }: { + nodes.machine = { config, ... }: { imports = [ ../../modules/sops ]; sops = { age.keyFile = "/run/age-keys.txt"; defaultSopsFile = ./test-assets/secrets.yaml; - secrets.test_key = { }; + 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"; + }; + }; + }; + + users.users."someuser" = { + uid = 1000; + group = "somegroup"; + isNormalUser = true; + }; + users.groups."somegroup" = { + gid = 1000; }; # must run before sops sets up keys @@ -130,6 +159,22 @@ in { testScript = '' start_all() machine.succeed("cat /run/secrets/test_key | grep -q test_value") + + with subtest("test ownership"): + machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_somegroup) = '1000' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_somegroup) = '1000' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_somegroup) = 'someuser' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_somegroup) = 'somegroup' ]") + + machine.succeed("[ $(stat -c%u /run/secrets/test_key_someuser_root) = '1000' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_someuser_root) = '0' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_someuser_root) = 'someuser' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_someuser_root) = 'root' ]") + + machine.succeed("[ $(stat -c%u /run/secrets/test_key_1001_1001) = '1001' ]") + machine.succeed("[ $(stat -c%g /run/secrets/test_key_1001_1001) = '1001' ]") + machine.succeed("[ $(stat -c%U /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]") + machine.succeed("[ $(stat -c%G /run/secrets/test_key_1001_1001) = 'UNKNOWN' ]") ''; }; @@ -142,6 +187,7 @@ in { type = "ed25519"; path = ./test-assets/ssh-ed25519-key; }]; + sops = { defaultSopsFile = ./test-assets/secrets.yaml; secrets.test_key = { }; @@ -161,7 +207,7 @@ in { pgp-keys = testers.runNixOSTest { name = "sops-pgp-keys"; - nodes.server = { pkgs, lib, config, ... }: { + nodes.server = { lib, config, ... }: { imports = [ ../../modules/sops ]; users.users.someuser = {