From 57e9b27ff8388a2650dcc03d1f436ebe520c7494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 16 Feb 2024 17:03:14 +0100 Subject: [PATCH] add option to set defaultGroups for secrets --- nixosModules/clanCore/outputs.nix | 8 +++++++- nixosModules/clanCore/secrets/default.nix | 8 ++++++++ nixosModules/clanCore/secrets/sops.nix | 8 ++++++++ pkgs/clan-cli/clan_cli/machines/machines.py | 3 ++- pkgs/clan-cli/clan_cli/secrets/check.py | 8 ++++++-- pkgs/clan-cli/clan_cli/secrets/generate.py | 16 +++++++++++++--- .../clan_cli/secrets/modules/__init__.py | 4 +++- .../clan_cli/secrets/modules/password_store.py | 11 +++++++++-- pkgs/clan-cli/clan_cli/secrets/modules/sops.py | 5 ++++- pkgs/clan-cli/clan_cli/secrets/modules/vm.py | 4 +++- .../tests/test_flake_with_core/flake.nix | 1 + pkgs/clan-cli/tests/test_secrets_generate.py | 11 +++++++++++ pkgs/clan-cli/tests/test_vms_cli.py | 9 +++++++++ 13 files changed, 84 insertions(+), 12 deletions(-) diff --git a/nixosModules/clanCore/outputs.nix b/nixosModules/clanCore/outputs.nix index 19efb9b1..c7a77f1f 100644 --- a/nixosModules/clanCore/outputs.nix +++ b/nixosModules/clanCore/outputs.nix @@ -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; }) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 08b73448..99c3a94f 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -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 = '' diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index eefac02f..a5f627a5 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -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 + "-"; diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 8c1d7d55..5afc7c2d 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -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 @@ -101,7 +102,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()) diff --git a/pkgs/clan-cli/clan_cli/secrets/check.py b/pkgs/clan-cli/clan_cli/secrets/check.py index 83c92680..ea348548 100644 --- a/pkgs/clan-cli/clan_cli/secrets/check.py +++ b/pkgs/clan-cli/clan_cli/secrets/check.py @@ -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): diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index 449507f8..b38b39a7 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -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) diff --git a/pkgs/clan-cli/clan_cli/secrets/modules/__init__.py b/pkgs/clan-cli/clan_cli/secrets/modules/__init__.py index 6105bf86..591e4a5f 100644 --- a/pkgs/clan-cli/clan_cli/secrets/modules/__init__.py +++ b/pkgs/clan-cli/clan_cli/secrets/modules/__init__.py @@ -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 diff --git a/pkgs/clan-cli/clan_cli/secrets/modules/password_store.py b/pkgs/clan-cli/clan_cli/secrets/modules/password_store.py index e185be53..a1164a9f 100644 --- a/pkgs/clan-cli/clan_cli/secrets/modules/password_store.py +++ b/pkgs/clan-cli/clan_cli/secrets/modules/password_store.py @@ -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()) diff --git a/pkgs/clan-cli/clan_cli/secrets/modules/sops.py b/pkgs/clan-cli/clan_cli/secrets/modules/sops.py index 218b7887..08122313 100644 --- a/pkgs/clan-cli/clan_cli/secrets/modules/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/modules/sops.py @@ -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 diff --git a/pkgs/clan-cli/clan_cli/secrets/modules/vm.py b/pkgs/clan-cli/clan_cli/secrets/modules/vm.py index fe318669..6ae5acba 100644 --- a/pkgs/clan-cli/clan_cli/secrets/modules/vm.py +++ b/pkgs/clan-cli/clan_cli/secrets/modules/vm.py @@ -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) diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index 631b5ae6..3870cb6c 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -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; diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index d9605fe1..fc4b21ca 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -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") diff --git a/pkgs/clan-cli/tests/test_vms_cli.py b/pkgs/clan-cli/tests/test_vms_cli.py index 6e272098..0360a4d2 100644 --- a/pkgs/clan-cli/tests/test_vms_cli.py +++ b/pkgs/clan-cli/tests/test_vms_cli.py @@ -106,6 +106,15 @@ def test_run( age_keys[0].pubkey, ] ) + cli.run( + [ + "secrets", + "groups", + "add-user", + "admins", + "user1", + ] + ) cli.run(["vms", "run", "vm1"])