From 8f0c57542515e7f1cda4d2c2afa8495ffe8266d4 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Nov 2024 23:42:15 +0100 Subject: [PATCH] password-store owner & group support --- nixosModules/clanCore/vars/interface.nix | 42 +++++--- .../clanCore/vars/secret/password-store.nix | 100 ++++++++++++++++-- pkgs/clan-cli/clan_cli/vars/_types.py | 4 + .../vars/secret_modules/password_store.py | 32 ++++-- 4 files changed, 143 insertions(+), 35 deletions(-) diff --git a/nixosModules/clanCore/vars/interface.nix b/nixosModules/clanCore/vars/interface.nix index ca17f6c31..67cca84f0 100644 --- a/nixosModules/clanCore/vars/interface.nix +++ b/nixosModules/clanCore/vars/interface.nix @@ -68,14 +68,24 @@ in submodule (file: { imports = [ config.settings.fileModule - (lib.mkRenamedOptionModule [ "owner" ] [ - "sops" - "owner" - ]) - (lib.mkRenamedOptionModule [ "group" ] [ - "sops" - "group" - ]) + (lib.mkRenamedOptionModule + [ + "sops" + "owner" + ] + [ + "owner" + ] + ) + (lib.mkRenamedOptionModule + [ + "sops" + "group" + ] + [ + "group" + ] + ) ]; options = { name = lib.mkOption { @@ -129,15 +139,13 @@ in type = str; }; - sops = { - owner = lib.mkOption { - description = "The user name or id that will own the secret file. This option is currently only implemented for sops"; - default = "root"; - }; - group = lib.mkOption { - description = "The group name or id that will own the secret file. This option is currently only implemented for sops"; - default = "root"; - }; + owner = lib.mkOption { + description = "The user name or id that will own the secret file."; + default = "root"; + }; + group = lib.mkOption { + description = "The group name or id that will own the secret file."; + default = "root"; }; value = diff --git a/nixosModules/clanCore/vars/secret/password-store.nix b/nixosModules/clanCore/vars/secret/password-store.nix index afd191778..ccfee34ed 100644 --- a/nixosModules/clanCore/vars/secret/password-store.nix +++ b/nixosModules/clanCore/vars/secret/password-store.nix @@ -1,12 +1,94 @@ -{ config, lib, ... }: { - config.clan.core.vars.settings = - lib.mkIf (config.clan.core.vars.settings.secretStore == "password-store") - { - fileModule = file: { - path = lib.mkIf file.config.secret "${config.clan.core.vars.settings.secretUploadDirectory}/vars/${file.config.generatorName}/${file.config.name}"; + config, + options, + lib, + pkgs, + ... +}: +let + installSecretTarball = pkgs.writeShellApplication { + name = "install-secret-tarball"; + runtimeInputs = [ + pkgs.gnutar + pkgs.gzip + pkgs.move-mount-beneath + ]; + text = '' + 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 + umount -R /run/secrets.tmp + rmdir /run/secrets.tmp + umount --lazy /run/secrets + else + mount -t tmpfs -o noswap tmpfs /run/secrets + tar -xf "$src" -C /run/secrets + fi + ''; + }; + useSystemdActivation = + (options.systemd ? sysusers && config.systemd.sysusers.enable) + || (options.services ? userborn && config.services.userborn.enable); +in +{ + config = { + clan.core.vars.settings = + lib.mkIf (config.clan.core.vars.settings.secretStore == "password-store") + { + fileModule = file: { + path = "/run/secrets/vars/${file.config.generatorName}/${file.config.name}"; + }; + secretUploadDirectory = lib.mkDefault "/etc/secrets"; + secretModule = "clan_cli.vars.secret_modules.password_store"; }; - secretUploadDirectory = lib.mkDefault "/etc/secrets"; - 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.core.vars.settings.secretUploadDirectory}/secrets.tar.gz + '' + // lib.optionalAttrs (config.system ? dryActivationScript) { + supportsDryActivation = true; + } + ); + systemd.services.sops-install-secrets = + lib.mkIf + ( + (config.clan.core.vars.settings.secretStore == "password-store") + && (config.clan.core.vars.generators != { } && useSystemdActivation) + ) + { + wantedBy = [ "sysinit.target" ]; + after = [ "systemd-sysusers.service" ]; + unitConfig.DefaultDependencies = "no"; + + serviceConfig = { + Type = "oneshot"; + ExecStart = [ + "${installSecretTarball}/bin/install-secret-tarball ${config.clan.core.vars.settings.secretUploadDirectory}/secrets.tar.gz" + ]; + RemainAfterExit = true; + }; + }; + }; + } diff --git a/pkgs/clan-cli/clan_cli/vars/_types.py b/pkgs/clan-cli/clan_cli/vars/_types.py index ea4d20999..58c05c519 100644 --- a/pkgs/clan-cli/clan_cli/vars/_types.py +++ b/pkgs/clan-cli/clan_cli/vars/_types.py @@ -49,6 +49,8 @@ class Var: secret: bool shared: bool deployed: bool + owner: str + group: str @property def value(self) -> bytes: @@ -184,6 +186,8 @@ class StoreBase(ABC): secret=file["secret"], shared=generator["share"], deployed=file["deploy"], + owner=file.get("owner", "root"), + group=file.get("group", "root"), ) ) return all_vars 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 f60120550..974315078 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 @@ -1,4 +1,8 @@ +import io +import logging import os +import subprocess +import tarfile from itertools import chain from pathlib import Path from typing import override @@ -9,6 +13,8 @@ from clan_cli.nix import nix_shell from . import SecretStoreBase +log = logging.getLogger(__name__) + class SecretStore(SecretStoreBase): def __init__(self, machine: Machine) -> None: @@ -130,6 +136,7 @@ class SecretStore(SecretStoreBase): # TODO get the path to the secrets from the machine ["cat", f"{self.machine.secret_vars_upload_directory}/.pass_info"], check=False, + stdout=subprocess.PIPE, ).stdout.strip() if not remote_hash: @@ -139,13 +146,20 @@ class SecretStore(SecretStoreBase): return local_hash.decode() != remote_hash def upload(self, output_dir: Path) -> None: - for secret_var in self.get_all(): - if not secret_var.deployed: - continue - output_file = output_dir / "vars" / secret_var.generator / secret_var.name - output_file.parent.mkdir(parents=True, exist_ok=True) - with (output_file).open("wb") as f: - f.write( - self.get(secret_var.generator, secret_var.name, secret_var.shared) - ) + with tarfile.open(output_dir / "secrets.tar.gz", "w:gz") as tar: + for gen_name, generator in self.machine.vars_generators.items(): + tar_dir = tarfile.TarInfo(name=gen_name) + tar_dir.type = tarfile.DIRTYPE + tar_dir.mode = 0o511 + tar.addfile(tarinfo=tar_dir) + for f_name, file in generator["files"].items(): + if not file["deploy"]: + continue + tar_file = tarfile.TarInfo(name=f"{gen_name}/{f_name}") + content = self.get(gen_name, f_name, generator["share"]) + tar_file.size = len(content) + tar_file.mode = 0o440 + tar_file.uname = file.get("owner", "root") + tar_file.gname = file.get("group", "root") + tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) (output_dir / ".pass_info").write_bytes(self.generate_hash())