password-store owner & group support

This commit is contained in:
lassulus
2024-11-14 23:42:15 +01:00
parent d9b1c59382
commit 8e1697a089
4 changed files with 143 additions and 35 deletions

View File

@@ -68,14 +68,24 @@ in
submodule (file: { submodule (file: {
imports = [ imports = [
config.settings.fileModule config.settings.fileModule
(lib.mkRenamedOptionModule [ "owner" ] [ (lib.mkRenamedOptionModule
[
"sops" "sops"
"owner" "owner"
]) ]
(lib.mkRenamedOptionModule [ "group" ] [ [
"owner"
]
)
(lib.mkRenamedOptionModule
[
"sops" "sops"
"group" "group"
]) ]
[
"group"
]
)
]; ];
options = { options = {
name = lib.mkOption { name = lib.mkOption {
@@ -129,16 +139,14 @@ in
type = str; type = str;
}; };
sops = {
owner = lib.mkOption { owner = lib.mkOption {
description = "The user name or id that will own the secret file. This option is currently only implemented for sops"; description = "The user name or id that will own the secret file.";
default = "root"; default = "root";
}; };
group = lib.mkOption { group = lib.mkOption {
description = "The group name or id that will own the secret file. This option is currently only implemented for sops"; description = "The group name or id that will own the secret file.";
default = "root"; default = "root";
}; };
};
value = value =
lib.mkOption { lib.mkOption {

View File

@@ -1,12 +1,94 @@
{ config, lib, ... }:
{ {
config.clan.core.vars.settings = 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") lib.mkIf (config.clan.core.vars.settings.secretStore == "password-store")
{ {
fileModule = file: { fileModule = file: {
path = lib.mkIf file.config.secret "${config.clan.core.vars.settings.secretUploadDirectory}/vars/${file.config.generatorName}/${file.config.name}"; path = "/run/secrets/vars/${file.config.generatorName}/${file.config.name}";
}; };
secretUploadDirectory = lib.mkDefault "/etc/secrets"; secretUploadDirectory = lib.mkDefault "/etc/secrets";
secretModule = "clan_cli.vars.secret_modules.password_store"; 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;
};
};
};
} }

View File

@@ -49,6 +49,8 @@ class Var:
secret: bool secret: bool
shared: bool shared: bool
deployed: bool deployed: bool
owner: str
group: str
@property @property
def value(self) -> bytes: def value(self) -> bytes:
@@ -184,6 +186,8 @@ class StoreBase(ABC):
secret=file["secret"], secret=file["secret"],
shared=generator["share"], shared=generator["share"],
deployed=file["deploy"], deployed=file["deploy"],
owner=file.get("owner", "root"),
group=file.get("group", "root"),
) )
) )
return all_vars return all_vars

View File

@@ -1,4 +1,8 @@
import io
import logging
import os import os
import subprocess
import tarfile
from itertools import chain from itertools import chain
from pathlib import Path from pathlib import Path
from typing import override from typing import override
@@ -9,6 +13,8 @@ from clan_cli.nix import nix_shell
from . import SecretStoreBase from . import SecretStoreBase
log = logging.getLogger(__name__)
class SecretStore(SecretStoreBase): class SecretStore(SecretStoreBase):
def __init__(self, machine: Machine) -> None: def __init__(self, machine: Machine) -> None:
@@ -130,6 +136,7 @@ class SecretStore(SecretStoreBase):
# TODO get the path to the secrets from the machine # TODO get the path to the secrets from the machine
["cat", f"{self.machine.secret_vars_upload_directory}/.pass_info"], ["cat", f"{self.machine.secret_vars_upload_directory}/.pass_info"],
check=False, check=False,
stdout=subprocess.PIPE,
).stdout.strip() ).stdout.strip()
if not remote_hash: if not remote_hash:
@@ -139,13 +146,20 @@ class SecretStore(SecretStoreBase):
return local_hash.decode() != remote_hash return local_hash.decode() != remote_hash
def upload(self, output_dir: Path) -> None: def upload(self, output_dir: Path) -> None:
for secret_var in self.get_all(): with tarfile.open(output_dir / "secrets.tar.gz", "w:gz") as tar:
if not secret_var.deployed: 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 continue
output_file = output_dir / "vars" / secret_var.generator / secret_var.name tar_file = tarfile.TarInfo(name=f"{gen_name}/{f_name}")
output_file.parent.mkdir(parents=True, exist_ok=True) content = self.get(gen_name, f_name, generator["share"])
with (output_file).open("wb") as f: tar_file.size = len(content)
f.write( tar_file.mode = 0o440
self.get(secret_var.generator, secret_var.name, secret_var.shared) 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()) (output_dir / ".pass_info").write_bytes(self.generate_hash())