Skip to content

Commit

Permalink
Allow to set uid and gid instead of owner and group. No checks will b…
Browse files Browse the repository at this point in the history
…e performed when uid and gid are set.

```
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 <Mic92@users.noreply.github.com>
  • Loading branch information
munnik and Mic92 committed Oct 23, 2024
1 parent 06535d0 commit 5c8e8eb
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 44 deletions.
32 changes: 26 additions & 6 deletions modules/sops/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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. Can only be set if uid is 0.
'';
};
uid = lib.mkOption {
type = with lib.types; nullOr int;
default = 0;
description = ''
User of the file.
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 {
Expand Down Expand Up @@ -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)
);

Expand Down
2 changes: 1 addition & 1 deletion modules/sops/secrets-for-users/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 29 additions & 18 deletions pkgs/sops-install-secrets/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 == "" {
Expand Down
43 changes: 27 additions & 16 deletions pkgs/sops-install-secrets/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
52 changes: 49 additions & 3 deletions pkgs/sops-install-secrets/nixos-test.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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' ]")
'';
};

Expand All @@ -142,6 +187,7 @@ in {
type = "ed25519";
path = ./test-assets/ssh-ed25519-key;
}];

sops = {
defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { };
Expand All @@ -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 = {
Expand Down

0 comments on commit 5c8e8eb

Please sign in to comment.