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 {
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)
);

View file

@ -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;

View file

@ -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 == "" {

View file

@ -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",

View file

@ -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 = {