From d89ee46d7f5ddd0ab4bee79fcef16ad7f9b53b75 Mon Sep 17 00:00:00 2001 From: lassulus Date: Tue, 3 Dec 2024 22:28:39 +0100 Subject: [PATCH] vars password-store: add neededForUsers option --- nixosModules/clanCore/vars/default.nix | 7 +- nixosModules/clanCore/vars/interface.nix | 9 ++ .../clanCore/vars/secret/password-store.nix | 138 +++++++++++------- .../vars/secret_modules/password_store.py | 11 +- pkgs/clan-cli/clan_cli/vars/var.py | 2 + 5 files changed, 114 insertions(+), 53 deletions(-) diff --git a/nixosModules/clanCore/vars/default.nix b/nixosModules/clanCore/vars/default.nix index 264b85ea3..453aa740b 100644 --- a/nixosModules/clanCore/vars/default.nix +++ b/nixosModules/clanCore/vars/default.nix @@ -49,7 +49,12 @@ in ; files = lib.flip lib.mapAttrs generator.files ( _name: file: { - inherit (file) name deploy secret; + inherit (file) + name + deploy + secret + neededForUsers + ; } ); } diff --git a/nixosModules/clanCore/vars/interface.nix b/nixosModules/clanCore/vars/interface.nix index d5f4f62ad..4067254f4 100644 --- a/nixosModules/clanCore/vars/interface.nix +++ b/nixosModules/clanCore/vars/interface.nix @@ -196,6 +196,15 @@ in ''; type = str; }; + neededForUsers = lib.mkOption { + description = '' + Enabling this option causes the secret to be decrypted/installed before users and groups are created. + This can be used to retrieve user's passwords. + Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root. + ''; + type = bool; + default = false; + }; owner = lib.mkOption { description = "The user name or id that will own the secret file."; default = "root"; diff --git a/nixosModules/clanCore/vars/secret/password-store.nix b/nixosModules/clanCore/vars/secret/password-store.nix index 4af30efcd..a992d2511 100644 --- a/nixosModules/clanCore/vars/secret/password-store.nix +++ b/nixosModules/clanCore/vars/secret/password-store.nix @@ -17,26 +17,37 @@ let set -efu -o pipefail src=$1 - mkdir -p /run/secrets.tmp /run/secrets - if mountpoint -q /run/secrets; then - mount -t tmpfs -o noswap -o private tmpfs /run/secrets.tmp - chmod 511 /run/secrets.tmp - mount --bind --make-private /run/secrets.tmp /run/secrets.tmp - mount --bind --make-private /run/secrets /run/secrets - tar -xf "$src" -C /run/secrets.tmp - move-mount --beneath --move /run/secrets.tmp /run/secrets >/dev/null - umount -R /run/secrets.tmp - rmdir /run/secrets.tmp - umount --lazy /run/secrets + target=$2 + + echo "installing secrets from $src to $target" >&2 + + mkdir -p "$target".tmp "$target" + if mountpoint -q "$target"; then + mount -t tmpfs -o noswap -o private tmpfs "$target".tmp + chmod 511 "$target".tmp + mount --bind --make-private "$target".tmp "$target".tmp + mount --bind --make-private "$target" "$target" + tar -xf "$src" -C "$target".tmp + move-mount --beneath --move "$target".tmp "$target" 2>/dev/null + umount -R "$target".tmp + rmdir "$target".tmp + umount --lazy "$target" else - mount -t tmpfs -o noswap tmpfs /run/secrets - tar -xf "$src" -C /run/secrets + mount -t tmpfs -o noswap tmpfs "$target" + tar -xf "$src" -C "$target" fi ''; }; useSystemdActivation = (options.systemd ? sysusers && config.systemd.sysusers.enable) || (options.services ? userborn && config.services.userborn.enable); + normalSecrets = lib.any (gen: lib.any (file: !file.neededForUsers) (lib.attrValues gen.files)) ( + lib.attrValues config.clan.core.vars.generators + ); + userSecrets = lib.any (gen: lib.any (file: file.neededForUsers) (lib.attrValues gen.files)) ( + lib.attrValues config.clan.core.vars.generators + ); + in { options.clan.vars.password-store = { @@ -57,48 +68,75 @@ in fileModule = file: lib.mkIf file.config.secret { - path = "/run/secrets/${file.config.generatorName}/${file.config.name}"; + path = + if file.config.neededForUsers then + "/run/user-secrets/${file.config.generatorName}/${file.config.name}" + else + "/run/secrets/${file.config.generatorName}/${file.config.name}"; }; secretModule = "clan_cli.vars.secret_modules.password_store"; }; - system.activationScripts.setupSecrets = - lib.mkIf - ( - (config.clan.core.vars.settings.secretStore == "password-store") - && (config.clan.core.vars.generators != { } && !useSystemdActivation) - ) - ( - lib.stringAfter - [ - "specialfs" - "users" - "groups" - ] - '' - [ -e /run/current-system ] || echo setting up secrets... - ${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz - '' - // lib.optionalAttrs (config.system ? dryActivationScript) { - supportsDryActivation = true; - } - ); - systemd.services.pass-install-secrets = - lib.mkIf - ( - (config.clan.core.vars.settings.secretStore == "password-store") - && (config.clan.core.vars.generators != { } && useSystemdActivation) - ) + system.activationScripts = + lib.mkIf ((config.clan.core.vars.settings.secretStore == "password-store") && !useSystemdActivation) { - wantedBy = [ "sysinit.target" ]; - after = [ "systemd-sysusers.service" ]; - unitConfig.DefaultDependencies = "no"; + setupUserSecrets = lib.mkIf userSecrets ( + lib.stringAfter + [ + "specialfs" + ] + '' + [ -e /run/current-system ] || echo setting up secrets... + ${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets + '' + // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + } + ); + users.deps = lib.mkIf userSecrets [ "setupUserSecrets" ]; + setupSecrets = lib.mkIf normalSecrets ( + lib.stringAfter + [ + "specialfs" + "users" + "groups" + ] + '' + [ -e /run/current-system ] || echo setting up secrets... + ${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets + '' + // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + } + ); + }; + systemd.services = + lib.mkIf ((config.clan.core.vars.settings.secretStore == "password-store") && useSystemdActivation) + { + pass-install-user-secrets = lib.mkIf userSecrets { + wantedBy = [ "systemd-sysusers.service" ]; + before = [ "systemd-sysusers.service" ]; + unitConfig.DefaultDependencies = "no"; - serviceConfig = { - Type = "oneshot"; - ExecStart = [ - "${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz" - ]; - RemainAfterExit = true; + serviceConfig = { + Type = "oneshot"; + ExecStart = [ + "${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets_for_users.tar.gz /run/user-secrets" + ]; + RemainAfterExit = true; + }; + }; + pass-install-secrets = lib.mkIf normalSecrets { + wantedBy = [ "sysinit.target" ]; + after = [ "systemd-sysusers.service" ]; + unitConfig.DefaultDependencies = "no"; + + serviceConfig = { + Type = "oneshot"; + ExecStart = [ + "${installSecretTarball}/bin/install-secret-tarball ${config.clan.vars.password-store.secretLocation}/secrets.tar.gz /run/secrets" + ]; + RemainAfterExit = true; + }; }; }; }; diff --git a/pkgs/clan-cli/clan_cli/vars/secret_modules/password_store.py b/pkgs/clan-cli/clan_cli/vars/secret_modules/password_store.py index 5ca3374f2..7053f8d50 100644 --- a/pkgs/clan-cli/clan_cli/vars/secret_modules/password_store.py +++ b/pkgs/clan-cli/clan_cli/vars/secret_modules/password_store.py @@ -150,7 +150,10 @@ class SecretStore(SecretStoreBase): return local_hash.decode() != remote_hash def populate_dir(self, output_dir: Path) -> None: - with tarfile.open(output_dir / "secrets.tar.gz", "w:gz") as tar: + with ( + tarfile.open(output_dir / "secrets.tar.gz", "w:gz") as tar, + tarfile.open(output_dir / "secrets_for_users.tar.gz", "w:gz") as user_tar, + ): for generator in self.machine.vars_generators: dir_exists = False for file in generator.files: @@ -170,7 +173,10 @@ class SecretStore(SecretStoreBase): tar_file.mode = 0o440 tar_file.uname = file.owner tar_file.gname = file.group - tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) + if file.needed_for_users: + user_tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) + else: + tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) (output_dir / ".pass_info").write_bytes(self.generate_hash()) def upload(self) -> None: @@ -179,6 +185,7 @@ class SecretStore(SecretStoreBase): return with TemporaryDirectory(prefix="vars-upload-") as tempdir: pass_dir = Path(tempdir) + self.populate_dir(pass_dir) upload_dir = Path( self.machine.deployment["password-store"]["secretLocation"] ) diff --git a/pkgs/clan-cli/clan_cli/vars/var.py b/pkgs/clan-cli/clan_cli/vars/var.py index 1b584c70f..d562623d6 100644 --- a/pkgs/clan-cli/clan_cli/vars/var.py +++ b/pkgs/clan-cli/clan_cli/vars/var.py @@ -15,6 +15,7 @@ class Var: deploy: bool = False owner: str = "root" group: str = "root" + needed_for_users: bool = False # TODO: those shouldn't be set here _store: "StoreBase | None" = None @@ -74,4 +75,5 @@ class Var: deploy=data["deploy"], owner=data.get("owner", "root"), group=data.get("group", "root"), + needed_for_users=data.get("neededForUsers", False), )