forked from clan/clan-core
Merge pull request 'add option to set defaultGroups for secrets' (#858) from Mic92-target_host into main
This commit is contained in:
commit
36b20f18d4
@ -64,7 +64,13 @@
|
||||
'';
|
||||
default = pkgs.writers.writeJSON "secrets.json" (lib.mapAttrs
|
||||
(_name: secret: {
|
||||
secrets = builtins.attrNames secret.secrets;
|
||||
secrets = lib.mapAttrsToList
|
||||
(name: secret: {
|
||||
inherit name;
|
||||
} // lib.optionalAttrs (secret ? groups) {
|
||||
inherit (secret) groups;
|
||||
})
|
||||
secret.secrets;
|
||||
facts = lib.mapAttrs (_: secret: secret.path) secret.facts;
|
||||
generator = secret.generator.finalScript;
|
||||
})
|
||||
|
@ -108,6 +108,14 @@
|
||||
'';
|
||||
default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}";
|
||||
};
|
||||
} // lib.optionalAttrs (config'.clanCore.secretStore == "sops") {
|
||||
groups = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = config'.clanCore.sops.defaultGroups;
|
||||
description = ''
|
||||
Groups to decrypt the secret for. By default we always use the user's key.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}));
|
||||
description = ''
|
||||
|
@ -22,6 +22,14 @@ let
|
||||
secrets = filterDir containsMachineOrGroups secretsDir;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
clanCore.sops.defaultGroups = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "admins" ];
|
||||
description = "The default groups to for encryption use when no groups are specified.";
|
||||
};
|
||||
};
|
||||
config = lib.mkIf (config.clanCore.secretStore == "sops") {
|
||||
clanCore.secretsDirectory = "/run/secrets";
|
||||
clanCore.secretsPrefix = config.clanCore.machineName + "-";
|
||||
|
@ -4,6 +4,7 @@ from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.dirs import vm_state_dir
|
||||
from qemu.qmp import QEMUMonitorProtocol
|
||||
@ -107,7 +108,7 @@ class Machine:
|
||||
return self.deployment_info["factsModule"]
|
||||
|
||||
@property
|
||||
def secrets_data(self) -> dict:
|
||||
def secrets_data(self) -> dict[str, dict[str, Any]]:
|
||||
if self.deployment_info["secretsData"]:
|
||||
try:
|
||||
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
|
||||
|
@ -17,9 +17,13 @@ def check_secrets(machine: Machine) -> bool:
|
||||
missing_facts = []
|
||||
for service in machine.secrets_data:
|
||||
for secret in machine.secrets_data[service]["secrets"]:
|
||||
if not secret_store.exists(service, secret):
|
||||
if isinstance(secret, str):
|
||||
secret_name = secret
|
||||
else:
|
||||
secret_name = secret["name"]
|
||||
if not secret_store.exists(service, secret_name):
|
||||
log.info(f"Secret {secret} for service {service} is missing")
|
||||
missing_secrets.append((service, secret))
|
||||
missing_secrets.append((service, secret_name))
|
||||
|
||||
for fact in machine.secrets_data[service]["facts"]:
|
||||
if not fact_store.exists(service, fact):
|
||||
|
@ -69,12 +69,22 @@ def generate_service_secrets(
|
||||
files_to_commit = []
|
||||
# store secrets
|
||||
for secret in machine.secrets_data[service]["secrets"]:
|
||||
secret_file = secrets_dir / secret
|
||||
if isinstance(secret, str):
|
||||
# TODO: This is the old NixOS module, can be dropped everyone has updated.
|
||||
secret_name = secret
|
||||
groups = []
|
||||
else:
|
||||
secret_name = secret["name"]
|
||||
groups = secret.get("groups", [])
|
||||
|
||||
secret_file = secrets_dir / secret_name
|
||||
if not secret_file.is_file():
|
||||
msg = f"did not generate a file for '{secret}' when running the following command:\n"
|
||||
msg = f"did not generate a file for '{secret_name}' when running the following command:\n"
|
||||
msg += machine.secrets_data[service]["generator"]
|
||||
raise ClanError(msg)
|
||||
secret_path = secret_store.set(service, secret, secret_file.read_bytes())
|
||||
secret_path = secret_store.set(
|
||||
service, secret_name, secret_file.read_bytes(), groups
|
||||
)
|
||||
if secret_path:
|
||||
files_to_commit.append(secret_path)
|
||||
|
||||
|
@ -10,7 +10,9 @@ class SecretStoreBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||
def set(
|
||||
self, service: str, name: str, value: bytes, groups: list[str]
|
||||
) -> Path | None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
@ -12,7 +12,9 @@ class SecretStore(SecretStoreBase):
|
||||
def __init__(self, machine: Machine) -> None:
|
||||
self.machine = machine
|
||||
|
||||
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||
def set(
|
||||
self, service: str, name: str, value: bytes, groups: list[str]
|
||||
) -> Path | None:
|
||||
subprocess.run(
|
||||
nix_shell(
|
||||
["nixpkgs#pass"],
|
||||
@ -104,5 +106,10 @@ class SecretStore(SecretStoreBase):
|
||||
def upload(self, output_dir: Path) -> None:
|
||||
for service in self.machine.secrets_data:
|
||||
for secret in self.machine.secrets_data[service]["secrets"]:
|
||||
(output_dir / secret).write_bytes(self.get(service, secret))
|
||||
if isinstance(secret, dict):
|
||||
secret_name = secret["name"]
|
||||
else:
|
||||
# TODO: drop old format soon
|
||||
secret_name = secret
|
||||
(output_dir / secret_name).write_bytes(self.get(service, secret_name))
|
||||
(output_dir / ".pass_info").write_bytes(self.generate_hash())
|
||||
|
@ -28,7 +28,9 @@ class SecretStore:
|
||||
)
|
||||
add_machine(self.machine.flake_dir, self.machine.name, pub_key, False)
|
||||
|
||||
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||
def set(
|
||||
self, service: str, name: str, value: bytes, groups: list[str]
|
||||
) -> Path | None:
|
||||
path = (
|
||||
sops_secrets_folder(self.machine.flake_dir) / f"{self.machine.name}-{name}"
|
||||
)
|
||||
@ -37,6 +39,7 @@ class SecretStore:
|
||||
path,
|
||||
value.decode(),
|
||||
add_machines=[self.machine.name],
|
||||
add_groups=groups,
|
||||
)
|
||||
return path
|
||||
|
||||
|
@ -14,7 +14,9 @@ class SecretStore(SecretStoreBase):
|
||||
self.dir = vm_state_dir(str(machine.flake), machine.name) / "secrets"
|
||||
self.dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||
def set(
|
||||
self, service: str, name: str, value: bytes, groups: list[str]
|
||||
) -> Path | None:
|
||||
secret_file = self.dir / service / name
|
||||
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
secret_file.write_bytes(value)
|
||||
|
@ -142,7 +142,7 @@ def allow_member(
|
||||
) -> None:
|
||||
source = source_folder / name
|
||||
if not source.exists():
|
||||
msg = f"{name} does not exist in {source_folder}: "
|
||||
msg = f"Cannot encrypt {group_folder.parent.name} for '{name}' group. '{name}' group does not exist in {source_folder}: "
|
||||
msg += list_directory(source_folder)
|
||||
raise ClanError(msg)
|
||||
group_folder.mkdir(parents=True, exist_ok=True)
|
||||
@ -150,7 +150,7 @@ def allow_member(
|
||||
if user_target.exists():
|
||||
if not user_target.is_symlink():
|
||||
raise ClanError(
|
||||
f"Cannot add user {name}. {user_target} exists but is not a symlink"
|
||||
f"Cannot add user '{name}' to {group_folder.parent.name} secret. {user_target} exists but is not a symlink"
|
||||
)
|
||||
os.remove(user_target)
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
system.stateVersion = lib.version;
|
||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
||||
clanCore.sops.defaultGroups = [ "admins" ];
|
||||
clan.virtualisation.graphics = false;
|
||||
|
||||
clan.networking.zerotier.controller.enable = true;
|
||||
|
@ -33,6 +33,17 @@ def test_generate_secret(
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"admins",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cmd = ["--flake", str(test_flake_with_core.path), "secrets", "generate", "vm1"]
|
||||
cli.run(cmd)
|
||||
has_secret(test_flake_with_core.path, "vm1-age.key")
|
||||
|
@ -110,6 +110,15 @@ def test_run(
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"groups",
|
||||
"add-user",
|
||||
"admins",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
cli.run(["vms", "run", "vm1"])
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user