1
0
Fork 0
mirror of https://github.com/Mic92/sops-nix.git synced 2024-12-14 11:57:52 +00:00

Allow to set uid and gid instead of owner and group. No checks will be 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>
This commit is contained in:
Martijn de Munnik 2024-10-16 01:30:11 +02:00 committed by mergify[bot]
parent 26642e8f19
commit a4c33bfecb
5 changed files with 132 additions and 44 deletions

View file

@ -73,18 +73,32 @@ let
''; '';
}; };
owner = lib.mkOption { owner = lib.mkOption {
type = lib.types.str; type = with lib.types; nullOr str;
default = "root"; default = null;
description = '' 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 { group = lib.mkOption {
type = lib.types.str; type = with lib.types; nullOr str;
default = users.${config.owner}.group; default = if config.owner != null then users.${config.owner}.group else null;
defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`"; defaultText = lib.literalMD "{option}`config.users.users.\${owner}.group`";
description = '' 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 { sopsFile = lib.mkOption {
@ -318,6 +332,12 @@ in {
builtins.isPath secret.sopsFile || builtins.isPath secret.sopsFile ||
(builtins.isString secret.sopsFile && lib.hasPrefix builtins.storeDir 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"; 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) }]) cfg.secrets)
); );

View file

@ -43,7 +43,7 @@ in
}; };
assertions = [{ 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"; message = "neededForUsers cannot be used for secrets that are not root-owned";
} { } {
assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers; assertion = secretsForUsers != { } && sysusersEnabled -> config.users.mutableUsers;

View file

@ -29,8 +29,10 @@ type secret struct {
Name string `json:"name"` Name string `json:"name"`
Key string `json:"key"` Key string `json:"key"`
Path string `json:"path"` Path string `json:"path"`
Owner string `json:"owner"` Owner *string `json:"owner,omitempty"`
Group string `json:"group"` UID int `json:"uid"`
Group *string `json:"group,omitempty"`
GID int `json:"gid"`
SopsFile string `json:"sopsFile"` SopsFile string `json:"sopsFile"`
Format FormatType `json:"format"` Format FormatType `json:"format"`
Mode string `json:"mode"` Mode string `json:"mode"`
@ -475,25 +477,34 @@ func (app *appContext) validateSecret(secret *secret) error {
secret.group = 0 secret.group = 0
} else if app.checkMode == Off || app.ignorePasswd { } else if app.checkMode == Off || app.ignorePasswd {
// we only access to the user/group during deployment // 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 secret.Owner == nil {
if err != nil { secret.owner = secret.UID
return fmt.Errorf("failed to lookup group '%s': %w", secret.Group, err) } 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 { if secret.Group == nil {
return fmt.Errorf("cannot parse gid %s: %w", group.Gid, err) 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 == "" { if secret.Format == "" {

View file

@ -98,12 +98,14 @@ func testGPG(t *testing.T) {
} }
}() }()
nobody := "nobody"
nogroup := "nogroup"
// should create a symlink // should create a symlink
yamlSecret := secret{ yamlSecret := secret{
Name: "test", Name: "test",
Key: "test_key", Key: "test_key",
Owner: "nobody", Owner: &nobody,
Group: "nogroup", Group: &nogroup,
SopsFile: path.Join(assets, "secrets.yaml"), SopsFile: path.Join(assets, "secrets.yaml"),
Path: path.Join(testdir.path, "test-target"), Path: path.Join(testdir.path, "test-target"),
Mode: "0400", Mode: "0400",
@ -112,12 +114,13 @@ func testGPG(t *testing.T) {
} }
var jsonSecret, binarySecret, dotenvSecret, iniSecret secret var jsonSecret, binarySecret, dotenvSecret, iniSecret secret
root := "root"
// should not create a symlink // should not create a symlink
jsonSecret = yamlSecret jsonSecret = yamlSecret
jsonSecret.Name = "test2" jsonSecret.Name = "test2"
jsonSecret.Owner = "root" jsonSecret.Owner = &root
jsonSecret.Format = "json" jsonSecret.Format = "json"
jsonSecret.Group = "root" jsonSecret.Group = &root
jsonSecret.SopsFile = path.Join(assets, "secrets.json") jsonSecret.SopsFile = path.Join(assets, "secrets.json")
jsonSecret.Path = path.Join(testdir.secretsPath, "test2") jsonSecret.Path = path.Join(testdir.secretsPath, "test2")
jsonSecret.Mode = "0700" jsonSecret.Mode = "0700"
@ -130,16 +133,16 @@ func testGPG(t *testing.T) {
dotenvSecret = yamlSecret dotenvSecret = yamlSecret
dotenvSecret.Name = "test4" dotenvSecret.Name = "test4"
dotenvSecret.Owner = "root" dotenvSecret.Owner = &root
dotenvSecret.Group = "root" dotenvSecret.Group = &root
dotenvSecret.Format = "dotenv" dotenvSecret.Format = "dotenv"
dotenvSecret.SopsFile = path.Join(assets, "secrets.env") dotenvSecret.SopsFile = path.Join(assets, "secrets.env")
dotenvSecret.Path = path.Join(testdir.secretsPath, "test4") dotenvSecret.Path = path.Join(testdir.secretsPath, "test4")
iniSecret = yamlSecret iniSecret = yamlSecret
iniSecret.Name = "test5" iniSecret.Name = "test5"
iniSecret.Owner = "root" iniSecret.Owner = &root
iniSecret.Group = "root" iniSecret.Group = &root
iniSecret.Format = "ini" iniSecret.Format = "ini"
iniSecret.SopsFile = path.Join(assets, "secrets.ini") iniSecret.SopsFile = path.Join(assets, "secrets.ini")
iniSecret.Path = path.Join(testdir.secretsPath, "test5") iniSecret.Path = path.Join(testdir.secretsPath, "test5")
@ -214,11 +217,13 @@ func testSSHKey(t *testing.T) {
ok(t, err) ok(t, err)
file.Close() file.Close()
nobody := "nobody"
nogroup := "nogroup"
s := secret{ s := secret{
Name: "test", Name: "test",
Key: "test_key", Key: "test_key",
Owner: "nobody", Owner: &nobody,
Group: "nogroup", Group: &nogroup,
SopsFile: path.Join(assets, "secrets.yaml"), SopsFile: path.Join(assets, "secrets.yaml"),
Path: target, Path: target,
Mode: "0400", Mode: "0400",
@ -247,11 +252,13 @@ func TestAge(t *testing.T) {
ok(t, err) ok(t, err)
file.Close() file.Close()
nobody := "nobody"
nogroup := "nogroup"
s := secret{ s := secret{
Name: "test", Name: "test",
Key: "test_key", Key: "test_key",
Owner: "nobody", Owner: &nobody,
Group: "nogroup", Group: &nogroup,
SopsFile: path.Join(assets, "secrets.yaml"), SopsFile: path.Join(assets, "secrets.yaml"),
Path: target, Path: target,
Mode: "0400", Mode: "0400",
@ -280,11 +287,13 @@ func TestAgeWithSSH(t *testing.T) {
ok(t, err) ok(t, err)
file.Close() file.Close()
nobody := "nobody"
nogroup := "nogroup"
s := secret{ s := secret{
Name: "test", Name: "test",
Key: "test_key", Key: "test_key",
Owner: "nobody", Owner: &nobody,
Group: "nogroup", Group: &nogroup,
SopsFile: path.Join(assets, "secrets.yaml"), SopsFile: path.Join(assets, "secrets.yaml"),
Path: target, Path: target,
Mode: "0400", Mode: "0400",
@ -314,11 +323,13 @@ func TestValidateManifest(t *testing.T) {
testdir := newTestDir(t) testdir := newTestDir(t)
defer testdir.Remove() defer testdir.Remove()
nobody := "nobody"
nogroup := "nogroup"
s := secret{ s := secret{
Name: "test", Name: "test",
Key: "test_key", Key: "test_key",
Owner: "nobody", Owner: &nobody,
Group: "nogroup", Group: &nogroup,
SopsFile: path.Join(assets, "secrets.yaml"), SopsFile: path.Join(assets, "secrets.yaml"),
Path: path.Join(testdir.path, "test-target"), Path: path.Join(testdir.path, "test-target"),
Mode: "0400", Mode: "0400",

View file

@ -112,12 +112,41 @@ in {
age-keys = testers.runNixOSTest { age-keys = testers.runNixOSTest {
name = "sops-age-keys"; name = "sops-age-keys";
nodes.machine = { lib, ... }: { nodes.machine = { config, ... }: {
imports = [ ../../modules/sops ]; imports = [ ../../modules/sops ];
sops = { sops = {
age.keyFile = "/run/age-keys.txt"; age.keyFile = "/run/age-keys.txt";
defaultSopsFile = ./test-assets/secrets.yaml; 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 # must run before sops sets up keys
@ -130,6 +159,22 @@ in {
testScript = '' testScript = ''
start_all() start_all()
machine.succeed("cat /run/secrets/test_key | grep -q test_value") 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"; type = "ed25519";
path = ./test-assets/ssh-ed25519-key; path = ./test-assets/ssh-ed25519-key;
}]; }];
sops = { sops = {
defaultSopsFile = ./test-assets/secrets.yaml; defaultSopsFile = ./test-assets/secrets.yaml;
secrets.test_key = { }; secrets.test_key = { };
@ -161,7 +207,7 @@ in {
pgp-keys = testers.runNixOSTest { pgp-keys = testers.runNixOSTest {
name = "sops-pgp-keys"; name = "sops-pgp-keys";
nodes.server = { pkgs, lib, config, ... }: { nodes.server = { lib, config, ... }: {
imports = [ ../../modules/sops ]; imports = [ ../../modules/sops ];
users.users.someuser = { users.users.someuser = {