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:
parent
26642e8f19
commit
a4c33bfecb
5 changed files with 132 additions and 44 deletions
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 == "" {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in a new issue