add option to set defaultGroups for secrets
This commit is contained in:
parent
714f3b0378
commit
57e9b27ff8
|
@ -64,7 +64,13 @@
|
||||||
'';
|
'';
|
||||||
default = pkgs.writers.writeJSON "secrets.json" (lib.mapAttrs
|
default = pkgs.writers.writeJSON "secrets.json" (lib.mapAttrs
|
||||||
(_name: secret: {
|
(_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;
|
facts = lib.mapAttrs (_: secret: secret.path) secret.facts;
|
||||||
generator = secret.generator.finalScript;
|
generator = secret.generator.finalScript;
|
||||||
})
|
})
|
||||||
|
|
|
@ -108,6 +108,14 @@
|
||||||
'';
|
'';
|
||||||
default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}";
|
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 = ''
|
description = ''
|
||||||
|
|
|
@ -22,6 +22,14 @@ let
|
||||||
secrets = filterDir containsMachineOrGroups secretsDir;
|
secrets = filterDir containsMachineOrGroups secretsDir;
|
||||||
in
|
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") {
|
config = lib.mkIf (config.clanCore.secretStore == "sops") {
|
||||||
clanCore.secretsDirectory = "/run/secrets";
|
clanCore.secretsDirectory = "/run/secrets";
|
||||||
clanCore.secretsPrefix = config.clanCore.machineName + "-";
|
clanCore.secretsPrefix = config.clanCore.machineName + "-";
|
||||||
|
|
|
@ -4,6 +4,7 @@ from collections.abc import Generator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from clan_cli.dirs import vm_state_dir
|
from clan_cli.dirs import vm_state_dir
|
||||||
from qemu.qmp import QEMUMonitorProtocol
|
from qemu.qmp import QEMUMonitorProtocol
|
||||||
|
@ -101,7 +102,7 @@ class Machine:
|
||||||
return self.deployment_info["factsModule"]
|
return self.deployment_info["factsModule"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def secrets_data(self) -> dict:
|
def secrets_data(self) -> dict[str, dict[str, Any]]:
|
||||||
if self.deployment_info["secretsData"]:
|
if self.deployment_info["secretsData"]:
|
||||||
try:
|
try:
|
||||||
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
|
return json.loads(Path(self.deployment_info["secretsData"]).read_text())
|
||||||
|
|
|
@ -17,9 +17,13 @@ def check_secrets(machine: Machine) -> bool:
|
||||||
missing_facts = []
|
missing_facts = []
|
||||||
for service in machine.secrets_data:
|
for service in machine.secrets_data:
|
||||||
for secret in machine.secrets_data[service]["secrets"]:
|
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")
|
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"]:
|
for fact in machine.secrets_data[service]["facts"]:
|
||||||
if not fact_store.exists(service, fact):
|
if not fact_store.exists(service, fact):
|
||||||
|
|
|
@ -69,12 +69,22 @@ def generate_service_secrets(
|
||||||
files_to_commit = []
|
files_to_commit = []
|
||||||
# store secrets
|
# store secrets
|
||||||
for secret in machine.secrets_data[service]["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():
|
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"]
|
msg += machine.secrets_data[service]["generator"]
|
||||||
raise ClanError(msg)
|
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:
|
if secret_path:
|
||||||
files_to_commit.append(secret_path)
|
files_to_commit.append(secret_path)
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,9 @@ class SecretStoreBase(ABC):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
@ -12,7 +12,9 @@ class SecretStore(SecretStoreBase):
|
||||||
def __init__(self, machine: Machine) -> None:
|
def __init__(self, machine: Machine) -> None:
|
||||||
self.machine = machine
|
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(
|
subprocess.run(
|
||||||
nix_shell(
|
nix_shell(
|
||||||
["nixpkgs#pass"],
|
["nixpkgs#pass"],
|
||||||
|
@ -104,5 +106,10 @@ class SecretStore(SecretStoreBase):
|
||||||
def upload(self, output_dir: Path) -> None:
|
def upload(self, output_dir: Path) -> None:
|
||||||
for service in self.machine.secrets_data:
|
for service in self.machine.secrets_data:
|
||||||
for secret in self.machine.secrets_data[service]["secrets"]:
|
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())
|
(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)
|
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 = (
|
path = (
|
||||||
sops_secrets_folder(self.machine.flake_dir) / f"{self.machine.name}-{name}"
|
sops_secrets_folder(self.machine.flake_dir) / f"{self.machine.name}-{name}"
|
||||||
)
|
)
|
||||||
|
@ -37,6 +39,7 @@ class SecretStore:
|
||||||
path,
|
path,
|
||||||
value.decode(),
|
value.decode(),
|
||||||
add_machines=[self.machine.name],
|
add_machines=[self.machine.name],
|
||||||
|
add_groups=groups,
|
||||||
)
|
)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,9 @@ class SecretStore(SecretStoreBase):
|
||||||
self.dir = vm_state_dir(str(machine.flake), machine.name) / "secrets"
|
self.dir = vm_state_dir(str(machine.flake), machine.name) / "secrets"
|
||||||
self.dir.mkdir(parents=True, exist_ok=True)
|
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 = self.dir / service / name
|
||||||
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
secret_file.write_bytes(value)
|
secret_file.write_bytes(value)
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
system.stateVersion = lib.version;
|
system.stateVersion = lib.version;
|
||||||
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__";
|
||||||
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__";
|
||||||
|
clanCore.sops.defaultGroups = [ "admins" ];
|
||||||
clan.virtualisation.graphics = false;
|
clan.virtualisation.graphics = false;
|
||||||
|
|
||||||
clan.networking.zerotier.controller.enable = true;
|
clan.networking.zerotier.controller.enable = true;
|
||||||
|
|
|
@ -33,6 +33,17 @@ def test_generate_secret(
|
||||||
age_keys[0].pubkey,
|
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"]
|
cmd = ["--flake", str(test_flake_with_core.path), "secrets", "generate", "vm1"]
|
||||||
cli.run(cmd)
|
cli.run(cmd)
|
||||||
has_secret(test_flake_with_core.path, "vm1-age.key")
|
has_secret(test_flake_with_core.path, "vm1-age.key")
|
||||||
|
|
|
@ -106,6 +106,15 @@ def test_run(
|
||||||
age_keys[0].pubkey,
|
age_keys[0].pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"secrets",
|
||||||
|
"groups",
|
||||||
|
"add-user",
|
||||||
|
"admins",
|
||||||
|
"user1",
|
||||||
|
]
|
||||||
|
)
|
||||||
cli.run(["vms", "run", "vm1"])
|
cli.run(["vms", "run", "vm1"])
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user