vars: allow setting files as needed for activation

This commit is contained in:
lassulus
2024-12-18 15:09:20 +01:00
parent 37dc74d0f7
commit d91f653a65
9 changed files with 50 additions and 16 deletions

View File

@@ -23,12 +23,12 @@ clan secrets list
A NixOS machine will automatically import all secrets that are encrypted for the A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into current machine. At runtime it will use the host key to decrypt all secrets into
an in-memory, non-persistent filesystem using [sops-nix](https://github.com/Mic92/sops-nix). an in-memory, non-persistent filesystem using [sops-nix](https://github.com/Mic92/sops-nix).
In your nixos configuration you can get a path to secrets like this `config.sops.secrets.<name>.path`. For example: In your nixos configuration you can get a path to secrets like this `config.sops.secrets.<name>.path`. For example:
```nix ```nix
{ config, ...}: { { config, ...}: {
sops.secrets.my-password.neededForUsers = true; sops.secrets.my-password.neededFor = "users";
users.users.mic92 = { users.users.mic92 = {
isNormalUser = true; isNormalUser = true;

View File

@@ -55,7 +55,7 @@ in
mode mode
deploy deploy
secret secret
neededForUsers neededFor
; ;
} }
); );

View File

@@ -196,14 +196,18 @@ in
''; '';
type = str; type = str;
}; };
neededForUsers = lib.mkOption { neededFor = lib.mkOption {
description = '' description = ''
Enabling this option causes the secret to be decrypted/installed before users and groups are created. 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. 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. Setting this option moves the secret to /run/secrets-for-users and disallows setting owner and group to anything else than root.
''; '';
type = bool; type = lib.types.enum [
default = false; "activation"
"users"
"services"
];
default = "services";
}; };
owner = lib.mkOption { owner = lib.mkOption {
description = "The user name or id that will own the file."; description = "The user name or id that will own the file.";

View File

@@ -69,10 +69,15 @@ in
file: file:
lib.mkIf file.config.secret { lib.mkIf file.config.secret {
path = path =
if file.config.neededForUsers then if file.config.neededFor == "users" then
"/run/user-secrets/${file.config.generatorName}/${file.config.name}" "/run/user-secrets/${file.config.generatorName}/${file.config.name}"
else if file.config.neededFor == "services" then
"/run/secrets/${file.config.generatorName}/${file.config.name}"
else if file.config.neededFor == "activation" then
"${config.clan.password-store.secretLocation}/${file.config.generatorName}/${file.config.name}"
else else
"/run/secrets/${file.config.generatorName}/${file.config.name}"; throw "unknown neededFor ${file.config.neededFor}";
}; };
secretModule = "clan_cli.vars.secret_modules.password_store"; secretModule = "clan_cli.vars.secret_modules.password_store";
}; };

View File

@@ -27,8 +27,11 @@ in
# Before we generate a secret we cannot know the path yet, so we need to set it to an empty string # Before we generate a secret we cannot know the path yet, so we need to set it to an empty string
fileModule = file: { fileModule = file: {
path = lib.mkIf file.config.secret ( path = lib.mkIf file.config.secret (
config.sops.secrets.${"vars/${file.config.generatorName}/${file.config.name}"}.path if file.config.neededFor == "activation" then
or "/no-such-path" "/var/lib/sops-nix/${file.config.generatorName}/${file.config.name}"
else
config.sops.secrets.${"vars/${file.config.generatorName}/${file.config.name}"}.path
or "/no-such-path"
); );
}; };
secretModule = "clan_cli.vars.secret_modules.sops"; secretModule = "clan_cli.vars.secret_modules.sops";

View File

@@ -16,7 +16,9 @@ in
collectFiles = collectFiles =
vars: vars:
let let
relevantFiles = generator: flip filterAttrs generator.files (_name: f: f.secret && f.deploy); relevantFiles =
generator:
flip filterAttrs generator.files (_name: f: f.secret && f.deploy && (f.neededFor != "activation"));
allFiles = flatten ( allFiles = flatten (
flip mapAttrsToList vars.generators ( flip mapAttrsToList vars.generators (
gen_name: generator: gen_name: generator:
@@ -24,8 +26,9 @@ in
fname: file: { fname: file: {
name = fname; name = fname;
generator = gen_name; generator = gen_name;
neededForUsers = file.neededFor == "users";
inherit (generator) share; inherit (generator) share;
inherit (file) owner group neededForUsers; inherit (file) owner group;
} }
) )
) )

View File

@@ -160,11 +160,20 @@ class SecretStore(StoreBase):
for generator in self.machine.vars_generators: for generator in self.machine.vars_generators:
dir_exists = False dir_exists = False
for file in generator.files: for file in generator.files:
if file.needed_for == "activation":
(output_dir / generator.name / file.name).parent.mkdir(
parents=True,
exist_ok=True,
)
(output_dir / generator.name / file.name).write_bytes(
self.get(generator, file.name)
)
continue
if not file.deploy: if not file.deploy:
continue continue
if not file.secret: if not file.secret:
continue continue
if not dir_exists and not file.needed_for_users: if not dir_exists and file.needed_for == "services":
tar_dir = tarfile.TarInfo(name=generator.name) tar_dir = tarfile.TarInfo(name=generator.name)
tar_dir.type = tarfile.DIRTYPE tar_dir.type = tarfile.DIRTYPE
tar_dir.mode = 0o511 tar_dir.mode = 0o511
@@ -176,7 +185,7 @@ class SecretStore(StoreBase):
tar_file.mode = file.mode tar_file.mode = file.mode
tar_file.uname = file.owner tar_file.uname = file.owner
tar_file.gname = file.group tar_file.gname = file.group
if file.needed_for_users: if file.needed_for == "users":
user_tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) user_tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content))
else: else:
tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content)) tar.addfile(tarinfo=tar_file, fileobj=io.BytesIO(content))

View File

@@ -173,6 +173,16 @@ class SecretStore(StoreBase):
sops_secrets_folder(self.machine.flake_dir) / key_name, sops_secrets_folder(self.machine.flake_dir) / key_name,
) )
(output_dir / "key.txt").write_text(key) (output_dir / "key.txt").write_text(key)
for generator in self.machine.vars_generators:
for file in generator.files:
if file.needed_for == "activation":
(output_dir / generator.name / file.name).parent.mkdir(
parents=True,
exist_ok=True,
)
(output_dir / generator.name / file.name).write_bytes(
self.get(generator, file.name)
)
def upload(self) -> None: def upload(self) -> None:
with TemporaryDirectory(prefix="sops-upload-") as tempdir: with TemporaryDirectory(prefix="sops-upload-") as tempdir:

View File

@@ -17,7 +17,7 @@ class Var:
owner: str = "root" owner: str = "root"
group: str = "root" group: str = "root"
mode: int = 0o400 mode: int = 0o400
needed_for_users: bool = False needed_for: str = "services"
# TODO: those shouldn't be set here # TODO: those shouldn't be set here
_store: "StoreBase | None" = None _store: "StoreBase | None" = None
@@ -78,5 +78,5 @@ class Var:
owner=data.get("owner", "root"), owner=data.get("owner", "root"),
group=data.get("group", "root"), group=data.get("group", "root"),
mode=int(data.get("mode", "400"), 8), mode=int(data.get("mode", "400"), 8),
needed_for_users=data.get("neededForUsers", False), needed_for=data.get("neededFor", "services"),
) )