Merge pull request 'age plugin support' (#3322) from feat/age-plugin-support into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3322
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
|
||||
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
|
||||
|
||||
By default Clan utilizes the [sops](https://github.com/getsops/sops) format and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||
By default, Clan uses the [sops](https://github.com/getsops/sops) format
|
||||
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
|
||||
Clan can also be configured to be used with other secret store [backends](https://docs.clan.lol/reference/clan-core/vars/#clan.core.vars.settings.secretStore).
|
||||
|
||||
This guide will walk you through:
|
||||
|
||||
- **Creating a Keypair for Your User**: Learn how to generate a keypair for $USER to securely control all secrets.
|
||||
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
|
||||
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
|
||||
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
|
||||
|
||||
## Create Your Admin Keypair
|
||||
|
||||
To get started, you'll need to create **Your admin keypair**.
|
||||
To get started, you'll need to create **your admin keypair**.
|
||||
|
||||
!!! info
|
||||
Don't worry — if you've already made one before, this step won't change or overwrite it.
|
||||
@@ -34,9 +35,78 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
|
||||
Make sure to keep a safe backup of the private key you've just created.
|
||||
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
|
||||
|
||||
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
|
||||
```
|
||||
|
||||
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
|
||||
using `SOPS_AGE_KEY_FILE`.
|
||||
For more information see the [SOPS] guide on [encrypting with age].
|
||||
|
||||
!!! note
|
||||
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
|
||||
|
||||
### Using Age Plugins
|
||||
|
||||
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
|
||||
|
||||
You must **precede your secret key with a comment that contains its corresponding recipient**.
|
||||
|
||||
This is usually output as part of the generation process
|
||||
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```title="~/.config/sops/age/keys.txt"
|
||||
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
|
||||
```
|
||||
|
||||
!!! note
|
||||
The comment that precedes the plugin secret key need only contain the recipient.
|
||||
Any other text is ignored.
|
||||
|
||||
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
|
||||
just `# age1zdy....`
|
||||
|
||||
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
|
||||
are loaded when using Clan:
|
||||
|
||||
```nix title="flake.nix"
|
||||
{
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||
|
||||
outputs =
|
||||
{ self, clan-core, ... }:
|
||||
let
|
||||
clan = clan-core.clanLib.buildClan {
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
|
||||
# Add Yubikey and FIDO2 HMAC plugins
|
||||
# Note: the plugins listed here must be available in nixpkgs.
|
||||
secrets.age.plugins = [
|
||||
"age-plugin-yubikey"
|
||||
"age-plugin-fido2-hmac"
|
||||
];
|
||||
|
||||
machines = {
|
||||
# elided for brevity
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan) nixosConfigurations clanInternals;
|
||||
|
||||
# elided for brevity
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Add Your Public Key(s)
|
||||
|
||||
```console
|
||||
@@ -70,7 +140,7 @@ If you followed the quickstart tutorial all necessary secrets are initialized at
|
||||
You can list keys for your user with `clan secrets users get $USER`:
|
||||
|
||||
```console
|
||||
❯ bin/clan secrets users get alice
|
||||
clan secrets users get alice
|
||||
|
||||
[
|
||||
{
|
||||
@@ -97,3 +167,8 @@ To remove a key from your user:
|
||||
```console
|
||||
clan secrets users remove-key $USER --age-key <your_public_key>
|
||||
```
|
||||
|
||||
[age]: https://github.com/FiloSottile/age
|
||||
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
|
||||
[sops]: https://github.com/getsops/sops
|
||||
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
|
||||
|
||||
@@ -108,6 +108,14 @@ in
|
||||
default = { };
|
||||
};
|
||||
|
||||
secrets = lib.mkOption {
|
||||
type = types.submodule { imports = [ ./secrets/interface.nix ]; };
|
||||
description = ''
|
||||
Secrets related options such as AGE plugins required to encrypt/decrypt secrets using the CLI.
|
||||
'';
|
||||
default = { };
|
||||
};
|
||||
|
||||
pkgsForSystem = lib.mkOption {
|
||||
type = types.functionTo (types.nullOr types.attrs);
|
||||
default = _system: null;
|
||||
@@ -165,6 +173,7 @@ in
|
||||
clanModules = lib.mkOption { type = lib.types.raw; };
|
||||
source = lib.mkOption { type = lib.types.raw; };
|
||||
meta = lib.mkOption { type = lib.types.raw; };
|
||||
secrets = lib.mkOption { type = lib.types.raw; };
|
||||
clanLib = lib.mkOption { type = lib.types.raw; };
|
||||
all-machines-json = lib.mkOption { type = lib.types.raw; };
|
||||
machines = lib.mkOption { type = lib.types.raw; };
|
||||
|
||||
@@ -219,6 +219,7 @@ in
|
||||
templates = config.templates;
|
||||
inventory = config.inventory;
|
||||
meta = config.inventory.meta;
|
||||
secrets = config.secrets;
|
||||
|
||||
source = "${clan-core}";
|
||||
|
||||
|
||||
18
lib/build-clan/secrets/interface.nix
Normal file
18
lib/build-clan/secrets/interface.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib) types;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
age.plugins = lib.mkOption {
|
||||
type = types.listOf (types.strMatching "age-plugin-.*");
|
||||
default = [ ];
|
||||
description = ''
|
||||
A list of age plugins which must be available in the shell when encrypting and decrypting secrets.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
[
|
||||
"age",
|
||||
"age-plugin-fido2-hmac",
|
||||
"age-plugin-ledger",
|
||||
"age-plugin-se",
|
||||
"age-plugin-sss",
|
||||
"age-plugin-tpm",
|
||||
"age-plugin-yubikey",
|
||||
"avahi",
|
||||
"bash",
|
||||
"bubblewrap",
|
||||
|
||||
@@ -240,6 +240,7 @@ def add_group_argument(parser: argparse.ArgumentParser) -> None:
|
||||
|
||||
def add_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||
secrets.allow_member(
|
||||
flake_dir,
|
||||
secrets.groups_folder(sops_secrets_folder(flake_dir) / name),
|
||||
sops_groups_folder(flake_dir),
|
||||
group,
|
||||
@@ -267,7 +268,7 @@ def add_secret_command(args: argparse.Namespace) -> None:
|
||||
|
||||
def remove_secret(flake_dir: Path, group: str, name: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.groups_folder(sops_secrets_folder(flake_dir) / name), group
|
||||
flake_dir, secrets.groups_folder(sops_secrets_folder(flake_dir) / name), group
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
|
||||
@@ -74,6 +74,7 @@ def list_sops_machines(flake_dir: Path) -> list[str]:
|
||||
|
||||
def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
|
||||
paths = secrets.allow_member(
|
||||
flake_dir,
|
||||
secrets.machines_folder(secret_path),
|
||||
sops_machines_folder(flake_dir),
|
||||
machine,
|
||||
@@ -87,7 +88,9 @@ def add_secret(flake_dir: Path, machine: str, secret_path: Path) -> None:
|
||||
|
||||
def remove_secret(flake_dir: Path, machine: str, secret: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.machines_folder(sops_secrets_folder(flake_dir) / secret), machine
|
||||
flake_dir,
|
||||
secrets.machines_folder(sops_secrets_folder(flake_dir) / secret),
|
||||
machine,
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
|
||||
@@ -86,6 +86,7 @@ def update_secrets(
|
||||
changed_files.extend(cleanup_dangling_symlinks(path / "machines"))
|
||||
changed_files.extend(
|
||||
update_keys(
|
||||
flake_dir,
|
||||
path,
|
||||
collect_keys_for_path(path),
|
||||
)
|
||||
@@ -155,16 +156,14 @@ def encrypt_secret(
|
||||
if add_users is None:
|
||||
add_users = []
|
||||
|
||||
keys = sops.ensure_admin_public_key(flake_dir)
|
||||
admin_keys = sops.ensure_admin_public_keys(flake_dir)
|
||||
|
||||
if not keys:
|
||||
if not admin_keys:
|
||||
# todo double check the correct command to run
|
||||
msg = "No keys found. Please run 'clan secrets add-key' to add a key."
|
||||
raise ClanError(msg)
|
||||
|
||||
username = next(iter(keys)).username
|
||||
|
||||
recipient_keys = set()
|
||||
username = next(iter(admin_keys)).username
|
||||
|
||||
# encrypt_secret can be called before the secret has been created
|
||||
# so don't try to call sops.update_keys on a non-existent file:
|
||||
@@ -174,6 +173,7 @@ def encrypt_secret(
|
||||
for user in add_users:
|
||||
files_to_commit.extend(
|
||||
allow_member(
|
||||
flake_dir,
|
||||
users_folder(secret_path),
|
||||
sops_users_folder(flake_dir),
|
||||
user,
|
||||
@@ -184,6 +184,7 @@ def encrypt_secret(
|
||||
for machine in add_machines:
|
||||
files_to_commit.extend(
|
||||
allow_member(
|
||||
flake_dir,
|
||||
machines_folder(secret_path),
|
||||
sops_machines_folder(flake_dir),
|
||||
machine,
|
||||
@@ -194,6 +195,7 @@ def encrypt_secret(
|
||||
for group in add_groups:
|
||||
files_to_commit.extend(
|
||||
allow_member(
|
||||
flake_dir,
|
||||
groups_folder(secret_path),
|
||||
sops_groups_folder(flake_dir),
|
||||
group,
|
||||
@@ -203,11 +205,12 @@ def encrypt_secret(
|
||||
|
||||
recipient_keys = collect_keys_for_path(secret_path)
|
||||
|
||||
if not keys.intersection(recipient_keys):
|
||||
recipient_keys.update(keys)
|
||||
if admin_keys not in recipient_keys:
|
||||
recipient_keys.update(admin_keys)
|
||||
|
||||
files_to_commit.extend(
|
||||
allow_member(
|
||||
flake_dir,
|
||||
users_folder(secret_path),
|
||||
sops_users_folder(flake_dir),
|
||||
username,
|
||||
@@ -216,7 +219,7 @@ def encrypt_secret(
|
||||
)
|
||||
|
||||
secret_path = secret_path / "secret"
|
||||
encrypt_file(secret_path, value, sorted(recipient_keys))
|
||||
encrypt_file(flake_dir, secret_path, value, sorted(recipient_keys))
|
||||
files_to_commit.append(secret_path)
|
||||
if git_commit:
|
||||
commit_files(
|
||||
@@ -276,7 +279,11 @@ def list_directory(directory: Path) -> str:
|
||||
|
||||
|
||||
def allow_member(
|
||||
group_folder: Path, source_folder: Path, name: str, do_update_keys: bool = True
|
||||
flake_dir: str | Path,
|
||||
group_folder: Path,
|
||||
source_folder: Path,
|
||||
name: str,
|
||||
do_update_keys: bool = True,
|
||||
) -> list[Path]:
|
||||
source = source_folder / name
|
||||
if not source.exists():
|
||||
@@ -299,6 +306,7 @@ def allow_member(
|
||||
if do_update_keys:
|
||||
changed.extend(
|
||||
update_keys(
|
||||
flake_dir,
|
||||
group_folder.parent,
|
||||
collect_keys_for_path(group_folder.parent),
|
||||
)
|
||||
@@ -306,7 +314,7 @@ def allow_member(
|
||||
return changed
|
||||
|
||||
|
||||
def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
||||
def disallow_member(flake_dir: str | Path, group_folder: Path, name: str) -> list[Path]:
|
||||
target = group_folder / name
|
||||
if not target.exists():
|
||||
msg = f"{name} does not exist in group in {group_folder}: "
|
||||
@@ -326,7 +334,9 @@ def disallow_member(group_folder: Path, name: str) -> list[Path]:
|
||||
if next(group_folder.parent.iterdir(), None) is None:
|
||||
group_folder.parent.rmdir()
|
||||
|
||||
return update_keys(target.parent.parent, collect_keys_for_path(group_folder.parent))
|
||||
return update_keys(
|
||||
flake_dir, target.parent.parent, collect_keys_for_path(group_folder.parent)
|
||||
)
|
||||
|
||||
|
||||
def has_secret(secret_path: Path) -> bool:
|
||||
@@ -366,7 +376,7 @@ def decrypt_secret(flake_dir: Path, secret_path: Path) -> str:
|
||||
if not path.exists():
|
||||
msg = f"Secret '{secret_path!s}' does not exist"
|
||||
raise ClanError(msg)
|
||||
return decrypt_file(path)
|
||||
return decrypt_file(flake_dir, path)
|
||||
|
||||
|
||||
def get_command(args: argparse.Namespace) -> None:
|
||||
|
||||
@@ -4,6 +4,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections.abc import Iterable, Sequence
|
||||
@@ -17,9 +18,11 @@ from clan_lib.api import API
|
||||
from clan_cli.cmd import Log, RunOpts, run
|
||||
from clan_cli.dirs import user_config_dir
|
||||
from clan_cli.errors import ClanError
|
||||
from clan_cli.nix import nix_shell
|
||||
from clan_cli.nix import nix_eval, nix_shell
|
||||
|
||||
from .folders import sops_machines_folder, sops_users_folder
|
||||
from .folders import sops_users_folder
|
||||
|
||||
AGE_RECIPIENT_REGEX = re.compile(r"^.*((age1|ssh-(rsa|ed25519) ).*?)(\s|$)")
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,14 +58,20 @@ class KeyType(enum.Enum):
|
||||
def maybe_read_from_path(key_path: Path) -> None:
|
||||
try:
|
||||
# as in parse.go in age:
|
||||
lines = Path(key_path).read_text().strip().splitlines()
|
||||
for private_key in filter(lambda ln: not ln.startswith("#"), lines):
|
||||
public_key = get_public_age_key(private_key)
|
||||
content = Path(key_path).read_text().strip()
|
||||
|
||||
try:
|
||||
for public_key in get_public_age_keys(content):
|
||||
log.info(
|
||||
f"Found age public key from a private key "
|
||||
f"in {key_path}: {public_key}"
|
||||
)
|
||||
|
||||
keyring.append(public_key)
|
||||
except ClanError as e:
|
||||
error_msg = f"Failed to read age keys from {key_path}"
|
||||
raise ClanError(error_msg) from e
|
||||
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except Exception as ex:
|
||||
@@ -72,13 +81,19 @@ class KeyType(enum.Enum):
|
||||
# SOPS_AGE_KEY is fed into age.ParseIdentities by Sops, and
|
||||
# reads identities line by line. See age/keysource.go in
|
||||
# Sops, and age/parse.go in Age.
|
||||
for private_key in keys.strip().splitlines():
|
||||
public_key = get_public_age_key(private_key)
|
||||
content = keys.strip()
|
||||
|
||||
try:
|
||||
for public_key in get_public_age_keys(content):
|
||||
log.info(
|
||||
f"Found age public key from a private key "
|
||||
f"in the environment (SOPS_AGE_KEY): {public_key}"
|
||||
)
|
||||
|
||||
keyring.append(public_key)
|
||||
except ClanError as e:
|
||||
error_msg = "Failed to read age keys from SOPS_AGE_KEY"
|
||||
raise ClanError(error_msg) from e
|
||||
|
||||
# Sops will try every location, see age/keysource.go
|
||||
elif key_path := os.environ.get("SOPS_AGE_KEY_FILE"):
|
||||
@@ -176,7 +191,41 @@ class Operation(enum.StrEnum):
|
||||
UPDATE_KEYS = "updatekeys"
|
||||
|
||||
|
||||
def load_age_plugins(flake_dir: str | Path) -> list[str]:
|
||||
if not flake_dir:
|
||||
msg = "Missing flake directory"
|
||||
raise ClanError(msg)
|
||||
|
||||
cmd = nix_eval(
|
||||
[
|
||||
f"{flake_dir}#clanInternals.secrets.age.plugins",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
|
||||
try:
|
||||
result = run(cmd)
|
||||
except Exception as e:
|
||||
msg = f"Failed to load age plugins {flake_dir}"
|
||||
raise ClanError(msg) from e
|
||||
|
||||
json_str = result.stdout.strip()
|
||||
|
||||
try:
|
||||
plugins = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
msg = f"Failed to decode '{json_str}': {e}"
|
||||
raise ClanError(msg) from e
|
||||
|
||||
if isinstance(plugins, list):
|
||||
return plugins
|
||||
|
||||
msg = f"Expected a list of age plugins but {type(plugins)!r} was provided"
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
def sops_run(
|
||||
flake_dir: str | Path,
|
||||
call: Operation,
|
||||
secret_path: Path,
|
||||
public_keys: Iterable[SopsKey],
|
||||
@@ -234,7 +283,9 @@ def sops_run(
|
||||
raise ClanError(msg)
|
||||
sops_cmd.append(str(secret_path))
|
||||
|
||||
cmd = nix_shell(["sops", "gnupg"], sops_cmd)
|
||||
age_plugins = load_age_plugins(flake_dir)
|
||||
|
||||
cmd = nix_shell(["sops", "gnupg", *age_plugins], sops_cmd)
|
||||
opts = (
|
||||
dataclasses.replace(run_opts, env=environ)
|
||||
if run_opts
|
||||
@@ -249,7 +300,44 @@ def sops_run(
|
||||
return p.returncode, p.stdout
|
||||
|
||||
|
||||
def get_public_age_key(privkey: str) -> str:
|
||||
def get_public_age_keys(contents: str) -> set[str]:
|
||||
# we use a set as it's possible we may detect the same key twice, once in a `# comment` and once by recovering it
|
||||
# from AGE-SECRET-KEY
|
||||
keys: set[str] = set()
|
||||
recipient: str | None = None
|
||||
|
||||
for line_number, line in enumerate(contents.splitlines()):
|
||||
match = AGE_RECIPIENT_REGEX.match(line)
|
||||
|
||||
if match:
|
||||
recipient = match[1]
|
||||
keys.add(recipient)
|
||||
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("AGE-PLUGIN-"):
|
||||
if not recipient:
|
||||
msg = f"Did you forget to precede line {line_number} with it's corresponding `# recipient: age1xxxxxxxx` entry?"
|
||||
raise ClanError(msg)
|
||||
|
||||
# reset recipient
|
||||
recipient = None
|
||||
|
||||
if line.startswith("AGE-SECRET-KEY-"):
|
||||
try:
|
||||
keys.add(get_public_age_key_from_private_key(line))
|
||||
except Exception as e:
|
||||
msg = "Failed to get public key for age private key. Is the key malformed?"
|
||||
raise ClanError(msg) from e
|
||||
|
||||
# reset recipient
|
||||
recipient = None
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
def get_public_age_key_from_private_key(privkey: str) -> str:
|
||||
cmd = nix_shell(["age"], ["age-keygen", "-y"])
|
||||
|
||||
error_msg = "Failed to get public key for age private key. Is the key malformed?"
|
||||
@@ -298,23 +386,7 @@ def get_user_name(flake_dir: Path, user: str) -> str:
|
||||
print(f"{flake_dir / user} already exists")
|
||||
|
||||
|
||||
def maybe_get_user_or_machine(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
||||
folders = [sops_users_folder(flake_dir), sops_machines_folder(flake_dir)]
|
||||
|
||||
for folder in folders:
|
||||
if folder.exists():
|
||||
for user in folder.iterdir():
|
||||
if not (user / "key.json").exists():
|
||||
continue
|
||||
|
||||
keys = read_keys(user)
|
||||
if key in keys:
|
||||
return {SopsKey(key.pubkey, user.name, key.key_type)}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
||||
def maybe_get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
||||
folder = sops_users_folder(flake_dir)
|
||||
|
||||
if folder.exists():
|
||||
@@ -324,20 +396,11 @@ def get_user(flake_dir: Path, key: SopsKey) -> set[SopsKey] | None:
|
||||
|
||||
keys = read_keys(user)
|
||||
if key in keys:
|
||||
return {SopsKey(key.pubkey, user.name, key.key_type)}
|
||||
return {SopsKey(key.pubkey, user.name, key.key_type) for key in keys}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@API.register
|
||||
def ensure_user_or_machine(flake_dir: Path, key: SopsKey) -> set[SopsKey]:
|
||||
maybe_keys = maybe_get_user_or_machine(flake_dir, key)
|
||||
if maybe_keys:
|
||||
return maybe_keys
|
||||
msg = f"A sops key is not yet added to the repository. Please add it with 'clan secrets users add youruser {key.pubkey}' (replace youruser with your user name)"
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
def default_admin_private_key_path() -> Path:
|
||||
raw_path = os.environ.get("SOPS_AGE_KEY_FILE")
|
||||
if raw_path:
|
||||
@@ -346,8 +409,9 @@ def default_admin_private_key_path() -> Path:
|
||||
|
||||
|
||||
@API.register
|
||||
def maybe_get_admin_public_key() -> None | SopsKey:
|
||||
def maybe_get_admin_public_key() -> SopsKey | None:
|
||||
keyring = SopsKey.collect_public_keys()
|
||||
|
||||
if len(keyring) == 0:
|
||||
return None
|
||||
|
||||
@@ -366,19 +430,31 @@ def maybe_get_admin_public_key() -> None | SopsKey:
|
||||
return keyring[0]
|
||||
|
||||
|
||||
def ensure_admin_public_key(flake_dir: Path) -> set[SopsKey]:
|
||||
def ensure_admin_public_keys(flake_dir: Path) -> set[SopsKey]:
|
||||
key = maybe_get_admin_public_key()
|
||||
if key:
|
||||
return ensure_user_or_machine(flake_dir, key)
|
||||
msg = "No sops key found. Please generate one with 'clan secrets key generate'."
|
||||
|
||||
if not key:
|
||||
msg = "No SOPS key found. Please generate one with `clan secrets key generate`."
|
||||
raise ClanError(msg)
|
||||
|
||||
user_keys = maybe_get_user(flake_dir, key)
|
||||
|
||||
def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
|
||||
if not user_keys:
|
||||
# todo improve error message
|
||||
msg = f"We could not figure out which Clan secrets user you are with the SOPS key we found: {key.pubkey}"
|
||||
raise ClanError(msg)
|
||||
|
||||
return user_keys
|
||||
|
||||
|
||||
def update_keys(
|
||||
flake_dir: str | Path, secret_path: Path, keys: Iterable[SopsKey]
|
||||
) -> list[Path]:
|
||||
secret_path = secret_path / "secret"
|
||||
error_msg = f"Could not update keys for {secret_path}"
|
||||
|
||||
rc, _ = sops_run(
|
||||
flake_dir,
|
||||
Operation.UPDATE_KEYS,
|
||||
secret_path,
|
||||
keys,
|
||||
@@ -389,6 +465,7 @@ def update_keys(secret_path: Path, keys: Iterable[SopsKey]) -> list[Path]:
|
||||
|
||||
|
||||
def encrypt_file(
|
||||
flake_dir: str | Path,
|
||||
secret_path: Path,
|
||||
content: str | IO[bytes] | bytes | None,
|
||||
pubkeys: list[SopsKey],
|
||||
@@ -399,6 +476,7 @@ def encrypt_file(
|
||||
if not content:
|
||||
# This will spawn an editor to edit the file.
|
||||
rc, _ = sops_run(
|
||||
flake_dir,
|
||||
Operation.EDIT,
|
||||
secret_path,
|
||||
pubkeys,
|
||||
@@ -437,6 +515,7 @@ def encrypt_file(
|
||||
msg = f"Invalid content type: {type(content)}"
|
||||
raise ClanError(msg)
|
||||
sops_run(
|
||||
flake_dir,
|
||||
Operation.ENCRYPT,
|
||||
Path(source.name),
|
||||
pubkeys,
|
||||
@@ -451,11 +530,12 @@ def encrypt_file(
|
||||
Path(source.name).unlink()
|
||||
|
||||
|
||||
def decrypt_file(secret_path: Path) -> str:
|
||||
def decrypt_file(flake_dir: str | Path, secret_path: Path) -> str:
|
||||
# decryption uses private keys from the environment or default paths:
|
||||
no_public_keys_needed: list[SopsKey] = []
|
||||
|
||||
_, stdout = sops_run(
|
||||
flake_dir,
|
||||
Operation.DECRYPT,
|
||||
secret_path,
|
||||
no_public_keys_needed,
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
from clan_cli.errors import ClanError
|
||||
|
||||
from .sops import get_public_age_key
|
||||
from .sops import get_public_age_keys
|
||||
|
||||
VALID_SECRET_NAME = re.compile(r"^[a-zA-Z0-9._-]+$")
|
||||
VALID_USER_NAME = re.compile(r"^[a-z_]([a-z0-9_-]{0,31})?$")
|
||||
@@ -21,14 +21,19 @@ def secret_name_type(arg_value: str) -> str:
|
||||
def public_or_private_age_key_type(arg_value: str) -> str:
|
||||
if Path(arg_value).is_file():
|
||||
arg_value = Path(arg_value).read_text().strip()
|
||||
for line in arg_value.splitlines():
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
if line.startswith("age1"):
|
||||
return line.strip()
|
||||
if line.startswith("AGE-SECRET-KEY-"):
|
||||
return get_public_age_key(line)
|
||||
msg = f"Please provide an age public key starting with age1 or an age private key AGE-SECRET-KEY-, got: '{arg_value}'"
|
||||
|
||||
public_keys = get_public_age_keys(arg_value)
|
||||
|
||||
match len(public_keys):
|
||||
case 0:
|
||||
msg = f"Please provide an age public key starting with age1 or an age private key starting with AGE-SECRET-KEY- or AGE-PLUGIN-, got: '{arg_value}'"
|
||||
raise ClanError(msg)
|
||||
|
||||
case 1:
|
||||
return next(iter(public_keys))
|
||||
|
||||
case n:
|
||||
msg = f"{n} age keys were provided, please provide only 1: '{arg_value}'"
|
||||
raise ClanError(msg)
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ def list_users(flake_dir: Path) -> list[str]:
|
||||
|
||||
def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||
updated_paths = secrets.allow_member(
|
||||
flake_dir,
|
||||
secrets.users_folder(sops_secrets_folder(flake_dir) / secret),
|
||||
sops_users_folder(flake_dir),
|
||||
user,
|
||||
@@ -106,7 +107,7 @@ def add_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||
|
||||
def remove_secret(flake_dir: Path, user: str, secret: str) -> None:
|
||||
updated_paths = secrets.disallow_member(
|
||||
secrets.users_folder(sops_secrets_folder(flake_dir) / secret), user
|
||||
flake_dir, secrets.users_folder(sops_secrets_folder(flake_dir) / secret), user
|
||||
)
|
||||
commit_files(
|
||||
updated_paths,
|
||||
|
||||
@@ -62,6 +62,10 @@ KEYS = [
|
||||
"age1eyyhln9g3cdwtrwpckugvqgtf5p8ugt0426sw38ra3wkc0t4rfhslq7txv",
|
||||
"AGE-SECRET-KEY-1567QKA63Y9P62SHF5TCHVCT5GZX2LZ8NS0E9RKA2QHDA662SF5LQ2VJJYX",
|
||||
),
|
||||
KeyPair(
|
||||
"age1e9ufa6wrsr5danka50qp0np0832uz7jca7s00wyeg2nt3aqnvaks7p4jfr",
|
||||
"AGE-SECRET-KEY-1Z89SHU9KAF709TTAZDARUWKC7H9TPZW4L8A2PVYSYAF7QVLCNQZQZ07U5J",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ if TYPE_CHECKING:
|
||||
from .age_keys import KeyPair
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_import_sops(
|
||||
test_root: Path,
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
age_keys: list["KeyPair"],
|
||||
@@ -24,7 +25,7 @@ def test_import_sops(
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
@@ -35,7 +36,7 @@ def test_import_sops(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
@@ -46,7 +47,7 @@ def test_import_sops(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user2",
|
||||
age_keys[2].pubkey,
|
||||
]
|
||||
@@ -57,7 +58,7 @@ def test_import_sops(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
@@ -68,7 +69,7 @@ def test_import_sops(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"user2",
|
||||
]
|
||||
@@ -80,7 +81,7 @@ def test_import_sops(
|
||||
"secrets",
|
||||
"import-sops",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"--group",
|
||||
"group1",
|
||||
"--machine",
|
||||
@@ -90,10 +91,13 @@ def test_import_sops(
|
||||
|
||||
cli.run(cmd)
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake_with_core.path)])
|
||||
|
||||
users = sorted(output.out.rstrip().split())
|
||||
assert users == ["user1", "user2"]
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-key"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "secret-key"]
|
||||
)
|
||||
assert output.out == "secret-value"
|
||||
|
||||
@@ -40,6 +40,8 @@ def test_add_module_to_inventory(
|
||||
age_keys: list["KeyPair"],
|
||||
) -> None:
|
||||
base_path = test_flake_with_core.path
|
||||
|
||||
with monkeypatch.context():
|
||||
monkeypatch.chdir(test_flake_with_core.path)
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||
|
||||
@@ -60,7 +62,9 @@ def test_add_module_to_inventory(
|
||||
)
|
||||
|
||||
create_machine(opts)
|
||||
(test_flake_with_core.path / "machines" / "machine1" / "facter.json").write_text(
|
||||
(
|
||||
test_flake_with_core.path / "machines" / "machine1" / "facter.json"
|
||||
).write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
@@ -87,7 +91,13 @@ def test_add_module_to_inventory(
|
||||
set_inventory(inventory, base_path, "Add borgbackup service")
|
||||
|
||||
# cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||
cmd = ["vars", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
|
||||
cmd = [
|
||||
"vars",
|
||||
"generate",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
]
|
||||
|
||||
cli.run(cmd)
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -20,14 +22,15 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def _test_identities(
|
||||
what: str,
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
age_keys: list["KeyPair"],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
sops_folder = test_flake.path / "sops"
|
||||
sops_folder = test_flake_with_core.path / "sops"
|
||||
|
||||
what_singular = what[:-1]
|
||||
test_secret_name = f"{what_singular}_secret"
|
||||
@@ -43,7 +46,7 @@ def _test_identities(
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"foo",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
@@ -56,7 +59,7 @@ def _test_identities(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin",
|
||||
admin_age_key.pubkey,
|
||||
]
|
||||
@@ -69,28 +72,30 @@ def _test_identities(
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"foo",
|
||||
age_keys[0].pubkey,
|
||||
]
|
||||
)
|
||||
|
||||
with monkeypatch.context():
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "deadfeed")
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("SOPS_NIX_SECRET", "deadfeed")
|
||||
m.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
f"--{what_singular}",
|
||||
"foo",
|
||||
test_secret_name,
|
||||
]
|
||||
)
|
||||
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
test_secret_name,
|
||||
expected_age_recipients_keypairs=[age_keys[0], admin_age_key],
|
||||
)
|
||||
@@ -103,14 +108,14 @@ def _test_identities(
|
||||
what,
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"-f",
|
||||
"foo",
|
||||
age_keys[1].privkey,
|
||||
]
|
||||
)
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
test_secret_name,
|
||||
expected_age_recipients_keypairs=[age_keys[1], admin_age_key],
|
||||
)
|
||||
@@ -122,24 +127,35 @@ def _test_identities(
|
||||
what,
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"foo",
|
||||
]
|
||||
)
|
||||
assert age_keys[1].pubkey in output.out
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake_with_core.path)])
|
||||
assert "foo" in output.out
|
||||
|
||||
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
|
||||
cli.run(
|
||||
["secrets", what, "remove", "--flake", str(test_flake_with_core.path), "foo"]
|
||||
)
|
||||
assert not (sops_folder / what / "foo" / "key.json").exists()
|
||||
|
||||
with pytest.raises(ClanError): # already removed
|
||||
cli.run(["secrets", what, "remove", "--flake", str(test_flake.path), "foo"])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
what,
|
||||
"remove",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"foo",
|
||||
]
|
||||
)
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", what, "list", "--flake", str(test_flake_with_core.path)])
|
||||
assert "foo" not in output.out
|
||||
|
||||
user_or_machine_symlink = sops_folder / "secrets" / test_secret_name / what / "foo"
|
||||
@@ -151,112 +167,186 @@ def _test_identities(
|
||||
assert not user_or_machine_symlink.exists(follow_symlinks=False), err_msg
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_users(
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
age_keys: list["KeyPair"],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_test_identities("users", test_flake, capture_output, age_keys, monkeypatch)
|
||||
with monkeypatch.context():
|
||||
_test_identities(
|
||||
"users", test_flake_with_core, capture_output, age_keys, monkeypatch
|
||||
)
|
||||
|
||||
# some additional user-specific tests
|
||||
|
||||
admin_key = age_keys[2]
|
||||
sops_folder = test_flake.path / "sops"
|
||||
@pytest.mark.with_core
|
||||
def test_multiple_user_keys(
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
age_keys: list["KeyPair"],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
sops_folder = test_flake_with_core.path / "sops"
|
||||
|
||||
user_keys = {
|
||||
"bob": [age_keys[0], age_keys[1]],
|
||||
"alice": [age_keys[2]],
|
||||
"charlie": [age_keys[3], age_keys[4]],
|
||||
users_keys = {
|
||||
"bob": {age_keys[0], age_keys[1]},
|
||||
"alice": {age_keys[2]},
|
||||
"charlie": {age_keys[3], age_keys[4], age_keys[5]},
|
||||
}
|
||||
|
||||
for user, keys in user_keys.items():
|
||||
key_args = [f"--age-key={key.pubkey}" for key in keys]
|
||||
|
||||
# add the user keys
|
||||
for user, user_keys in users_keys.items():
|
||||
# add the user
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
user,
|
||||
*key_args,
|
||||
*[f"--age-key={key.pubkey}" for key in user_keys],
|
||||
]
|
||||
)
|
||||
assert (sops_folder / "users" / user / "key.json").exists()
|
||||
|
||||
# check they are returned in get
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "users", "get", "--flake", str(test_flake.path), user])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"users",
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
user,
|
||||
]
|
||||
)
|
||||
|
||||
for key in keys:
|
||||
assert key.pubkey in output.out
|
||||
for user_key in user_keys:
|
||||
assert user_key.pubkey in output.out
|
||||
|
||||
# let's do some setting and getting of secrets
|
||||
|
||||
def random_str() -> str:
|
||||
return "".join(random.choices(string.ascii_letters, k=10))
|
||||
|
||||
for user_key in user_keys:
|
||||
# set a secret using each of the user's private keys
|
||||
with monkeypatch.context():
|
||||
secret_name = f"{user}_secret_{random_str()}"
|
||||
secret_value = random_str()
|
||||
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", user_key.privkey)
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", secret_value)
|
||||
|
||||
# set a secret
|
||||
secret_name = f"{user}_secret"
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
"--user",
|
||||
user,
|
||||
str(test_flake_with_core.path),
|
||||
secret_name,
|
||||
]
|
||||
)
|
||||
|
||||
# check the secret has each of our user's keys as a recipient
|
||||
# in addition the admin key should be there
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[admin_key, *keys],
|
||||
expected_age_recipients_keypairs=[*user_keys],
|
||||
)
|
||||
|
||||
if len(keys) == 1:
|
||||
# check we can get the secret
|
||||
with capture_output as output:
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
secret_name,
|
||||
]
|
||||
)
|
||||
|
||||
assert secret_value in output.out
|
||||
|
||||
if len(user_keys) == 1:
|
||||
continue
|
||||
|
||||
# remove one of the keys
|
||||
# remove one of the user keys,
|
||||
user_keys_iter = iter(user_keys)
|
||||
|
||||
key_to_remove = next(user_keys_iter)
|
||||
key_to_encrypt_with = next(user_keys_iter)
|
||||
|
||||
with monkeypatch.context():
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", key_to_encrypt_with.privkey)
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "deafbeef")
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"users",
|
||||
"remove-key",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
user,
|
||||
keys[0].pubkey,
|
||||
key_to_remove.pubkey,
|
||||
]
|
||||
)
|
||||
|
||||
# check the secret has been updated
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[admin_key, *keys[1:]],
|
||||
expected_age_recipients_keypairs=list({*user_keys} - {key_to_remove}),
|
||||
)
|
||||
|
||||
# add the key back
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"users",
|
||||
"add-key",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
user,
|
||||
key_to_remove.pubkey,
|
||||
]
|
||||
)
|
||||
|
||||
# check the secret has been updated
|
||||
assert_secrets_file_recipients(
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=user_keys,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_machines(
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
age_keys: list["KeyPair"],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
_test_identities("machines", test_flake, capture_output, age_keys, monkeypatch)
|
||||
_test_identities(
|
||||
"machines", test_flake_with_core, capture_output, age_keys, monkeypatch
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_groups(
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
age_keys: list["KeyPair"],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(
|
||||
["secrets", "groups", "list", "--flake", str(test_flake_with_core.path)]
|
||||
)
|
||||
assert output.out == ""
|
||||
|
||||
machine1_age_key = age_keys[0]
|
||||
@@ -270,7 +360,7 @@ def test_groups(
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@@ -282,7 +372,7 @@ def test_groups(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"groupb1",
|
||||
"user1",
|
||||
]
|
||||
@@ -293,7 +383,7 @@ def test_groups(
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
machine1_age_key.pubkey,
|
||||
]
|
||||
@@ -304,7 +394,7 @@ def test_groups(
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@@ -317,7 +407,7 @@ def test_groups(
|
||||
"groups",
|
||||
"add-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
@@ -329,7 +419,7 @@ def test_groups(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
user1_age_key.pubkey,
|
||||
]
|
||||
@@ -340,7 +430,7 @@ def test_groups(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin",
|
||||
admin_age_key.pubkey,
|
||||
]
|
||||
@@ -351,14 +441,16 @@ def test_groups(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "groups", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(
|
||||
["secrets", "groups", "list", "--flake", str(test_flake_with_core.path)]
|
||||
)
|
||||
out = output.out
|
||||
assert "user1" in out
|
||||
assert "machine1" in out
|
||||
@@ -368,12 +460,13 @@ def test_groups(
|
||||
with monkeypatch.context():
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "deafbeef")
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", admin_age_key.privkey)
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"--group",
|
||||
"group1",
|
||||
secret_name,
|
||||
@@ -381,7 +474,7 @@ def test_groups(
|
||||
)
|
||||
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[
|
||||
machine1_age_key,
|
||||
@@ -400,13 +493,13 @@ def test_groups(
|
||||
"groups",
|
||||
"remove-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
||||
err_msg=(
|
||||
@@ -422,13 +515,13 @@ def test_groups(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[
|
||||
machine1_age_key,
|
||||
@@ -444,12 +537,12 @@ def test_groups(
|
||||
"users",
|
||||
"remove",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
]
|
||||
)
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[machine1_age_key, admin_age_key],
|
||||
err_msg=(
|
||||
@@ -464,13 +557,13 @@ def test_groups(
|
||||
"groups",
|
||||
"remove-machine",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"group1",
|
||||
"machine1",
|
||||
]
|
||||
)
|
||||
assert_secrets_file_recipients(
|
||||
test_flake.path,
|
||||
test_flake_with_core.path,
|
||||
secret_name,
|
||||
expected_age_recipients_keypairs=[admin_age_key],
|
||||
err_msg=(
|
||||
@@ -479,11 +572,11 @@ def test_groups(
|
||||
),
|
||||
)
|
||||
|
||||
first_group = next((test_flake.path / "sops" / "groups").iterdir(), None)
|
||||
first_group = next((test_flake_with_core.path / "sops" / "groups").iterdir(), None)
|
||||
assert first_group is None
|
||||
|
||||
# Check if the symlink to the group was removed from our foo test secret:
|
||||
group_symlink = test_flake.path / "sops/secrets/foo/groups/group1"
|
||||
group_symlink = test_flake_with_core.path / "sops/secrets/foo/groups/group1"
|
||||
err_msg = (
|
||||
"Symlink to group1's key in foo secret "
|
||||
"was not cleaned up after group1 was removed"
|
||||
@@ -520,61 +613,105 @@ def use_gpg_key(key: GpgKey, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", old_key)
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_secrets(
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
gpg_key: GpgKey,
|
||||
age_keys: list["KeyPair"],
|
||||
) -> None:
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
|
||||
assert output.out == ""
|
||||
|
||||
# Generate a new key for the clan
|
||||
monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key"))
|
||||
monkeypatch.setenv(
|
||||
"SOPS_AGE_KEY_FILE", str(test_flake_with_core.path / ".." / "age.key")
|
||||
)
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "key", "generate", "--flake", str(test_flake.path)])
|
||||
cli.run(
|
||||
["secrets", "key", "generate", "--flake", str(test_flake_with_core.path)]
|
||||
)
|
||||
assert "age private key" in output.out
|
||||
# Read the key that was generated
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
|
||||
key = json.loads(output.out)["publickey"]
|
||||
assert key.startswith("age1")
|
||||
cli.run(["secrets", "key", "show", "--flake", str(test_flake_with_core.path)])
|
||||
|
||||
key = json.loads(output.out)
|
||||
assert key["publickey"].startswith("age1")
|
||||
# Add testuser with the key that was generated for the clan
|
||||
cli.run(
|
||||
["secrets", "users", "add", "--flake", str(test_flake.path), "testuser", key]
|
||||
[
|
||||
"secrets",
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"testuser",
|
||||
key["publickey"],
|
||||
]
|
||||
)
|
||||
|
||||
with pytest.raises(ClanError): # does not exist yet
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "nonexisting"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "nonexisting"]
|
||||
)
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "foo")
|
||||
cli.run(["secrets", "set", "--flake", str(test_flake.path), "initialkey"])
|
||||
cli.run(["secrets", "set", "--flake", str(test_flake_with_core.path), "initialkey"])
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "initialkey"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "initialkey"]
|
||||
)
|
||||
assert output.out == "foo"
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", "users", "list", "--flake", str(test_flake_with_core.path)])
|
||||
users = output.out.rstrip().split("\n")
|
||||
assert len(users) == 1, f"users: {users}"
|
||||
owner = users[0]
|
||||
|
||||
monkeypatch.setenv("EDITOR", "cat")
|
||||
cli.run(["secrets", "set", "--edit", "--flake", str(test_flake.path), "initialkey"])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"set",
|
||||
"--edit",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"initialkey",
|
||||
]
|
||||
)
|
||||
monkeypatch.delenv("EDITOR")
|
||||
|
||||
cli.run(["secrets", "rename", "--flake", str(test_flake.path), "initialkey", "key"])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"rename",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"initialkey",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
|
||||
assert output.out == "key\n"
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path), "nonexisting"])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"list",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"nonexisting",
|
||||
]
|
||||
)
|
||||
assert output.out == ""
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path), "key"])
|
||||
assert output.out == "key\n"
|
||||
|
||||
# using the `age_keys` KeyPair, add a machine and rotate its key
|
||||
@@ -585,7 +722,7 @@ def test_secrets(
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
@@ -596,18 +733,22 @@ def test_secrets(
|
||||
"machines",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "machines", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(
|
||||
["secrets", "machines", "list", "--flake", str(test_flake_with_core.path)]
|
||||
)
|
||||
assert output.out == "machine1\n"
|
||||
|
||||
with use_age_key(age_keys[1].privkey, monkeypatch):
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
|
||||
)
|
||||
assert output.out == "foo"
|
||||
|
||||
# rotate machines key
|
||||
@@ -617,7 +758,7 @@ def test_secrets(
|
||||
"machines",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"-f",
|
||||
"machine1",
|
||||
age_keys[0].privkey,
|
||||
@@ -627,7 +768,9 @@ def test_secrets(
|
||||
# should also rotate the encrypted secret
|
||||
with use_age_key(age_keys[0].privkey, monkeypatch):
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
|
||||
)
|
||||
assert output.out == "foo"
|
||||
|
||||
cli.run(
|
||||
@@ -636,7 +779,7 @@ def test_secrets(
|
||||
"machines",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"machine1",
|
||||
"key",
|
||||
]
|
||||
@@ -648,7 +791,7 @@ def test_secrets(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
@@ -659,13 +802,13 @@ def test_secrets(
|
||||
"users",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
with capture_output as output, use_age_key(age_keys[1].privkey, monkeypatch):
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake_with_core.path), "key"])
|
||||
assert output.out == "foo"
|
||||
cli.run(
|
||||
[
|
||||
@@ -673,7 +816,7 @@ def test_secrets(
|
||||
"users",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"user1",
|
||||
"key",
|
||||
]
|
||||
@@ -686,7 +829,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
@@ -697,7 +840,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"user1",
|
||||
]
|
||||
@@ -708,7 +851,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
owner,
|
||||
]
|
||||
@@ -719,7 +862,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"add-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
@@ -730,7 +873,7 @@ def test_secrets(
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"--group",
|
||||
"admin-group",
|
||||
"key2",
|
||||
@@ -739,7 +882,9 @@ def test_secrets(
|
||||
|
||||
with use_age_key(age_keys[1].privkey, monkeypatch):
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
|
||||
)
|
||||
assert output.out == "foo"
|
||||
|
||||
# Add an user with a GPG key
|
||||
@@ -749,7 +894,7 @@ def test_secrets(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"--pgp-key",
|
||||
gpg_key.fingerprint,
|
||||
"user2",
|
||||
@@ -763,7 +908,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"add-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"user2",
|
||||
]
|
||||
@@ -771,7 +916,9 @@ def test_secrets(
|
||||
|
||||
with use_gpg_key(gpg_key, monkeypatch): # user2
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(
|
||||
["secrets", "get", "--flake", str(test_flake_with_core.path), "key"]
|
||||
)
|
||||
assert output.out == "foo"
|
||||
|
||||
cli.run(
|
||||
@@ -780,7 +927,7 @@ def test_secrets(
|
||||
"groups",
|
||||
"remove-user",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"user2",
|
||||
]
|
||||
@@ -791,7 +938,7 @@ def test_secrets(
|
||||
capture_output as output,
|
||||
):
|
||||
# user2 is not in the group anymore
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake_with_core.path), "key"])
|
||||
print(output.out)
|
||||
|
||||
cli.run(
|
||||
@@ -800,22 +947,23 @@ def test_secrets(
|
||||
"groups",
|
||||
"remove-secret",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"admin-group",
|
||||
"key",
|
||||
]
|
||||
)
|
||||
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key"])
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake.path), "key2"])
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key"])
|
||||
cli.run(["secrets", "remove", "--flake", str(test_flake_with_core.path), "key2"])
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake.path)])
|
||||
cli.run(["secrets", "list", "--flake", str(test_flake_with_core.path)])
|
||||
assert output.out == ""
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_secrets_key_generate_gpg(
|
||||
test_flake: FlakeForTest,
|
||||
test_flake_with_core: FlakeForTest,
|
||||
capture_output: CaptureOutput,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
gpg_key: GpgKey,
|
||||
@@ -830,14 +978,16 @@ def test_secrets_key_generate_gpg(
|
||||
"key",
|
||||
"generate",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
]
|
||||
)
|
||||
assert "age private key" not in output.out
|
||||
assert re.match(r"PGP key.+is already set", output.err) is not None
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
|
||||
cli.run(
|
||||
["secrets", "key", "show", "--flake", str(test_flake_with_core.path)]
|
||||
)
|
||||
key = json.loads(output.out)
|
||||
assert key["type"] == "pgp"
|
||||
assert key["publickey"] == gpg_key.fingerprint
|
||||
@@ -849,12 +999,13 @@ def test_secrets_key_generate_gpg(
|
||||
"users",
|
||||
"add",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"--pgp-key",
|
||||
gpg_key.fingerprint,
|
||||
"testuser",
|
||||
]
|
||||
)
|
||||
|
||||
with capture_output as output:
|
||||
cli.run(
|
||||
[
|
||||
@@ -862,7 +1013,7 @@ def test_secrets_key_generate_gpg(
|
||||
"users",
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake.path),
|
||||
str(test_flake_with_core.path),
|
||||
"testuser",
|
||||
]
|
||||
)
|
||||
@@ -873,8 +1024,26 @@ def test_secrets_key_generate_gpg(
|
||||
assert key["type"] == "pgp"
|
||||
assert key["publickey"] == gpg_key.fingerprint
|
||||
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", "secret-value")
|
||||
cli.run(["secrets", "set", "--flake", str(test_flake.path), "secret-name"])
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("SOPS_NIX_SECRET", "secret-value")
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"set",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secret-name",
|
||||
]
|
||||
)
|
||||
with capture_output as output:
|
||||
cli.run(["secrets", "get", "--flake", str(test_flake.path), "secret-name"])
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
"get",
|
||||
"--flake",
|
||||
str(test_flake_with_core.path),
|
||||
"secret-name",
|
||||
]
|
||||
)
|
||||
assert output.out == "secret-value"
|
||||
|
||||
@@ -44,6 +44,8 @@ def test_secrets_upload(
|
||||
config["clan"]["networking"]["targetHost"] = addr
|
||||
config["clan"]["core"]["facts"]["secretUploadDirectory"] = str(flake.path / "facts")
|
||||
flake.refresh()
|
||||
|
||||
with monkeypatch.context():
|
||||
monkeypatch.chdir(str(flake.path))
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||
|
||||
@@ -81,7 +83,10 @@ def test_secrets_upload(
|
||||
age_keys[1].pubkey,
|
||||
]
|
||||
)
|
||||
monkeypatch.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
||||
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("SOPS_NIX_SECRET", age_keys[0].privkey)
|
||||
|
||||
cli.run(["secrets", "set", "--flake", str(flake.path), "vm1-age.key"])
|
||||
|
||||
flake_path = flake.path.joinpath("flake.nix")
|
||||
|
||||
@@ -32,8 +32,10 @@ def test_run(
|
||||
test_flake_with_core: FlakeForTest,
|
||||
age_keys: list["KeyPair"],
|
||||
) -> None:
|
||||
with monkeypatch.context():
|
||||
monkeypatch.chdir(test_flake_with_core.path)
|
||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"secrets",
|
||||
|
||||
@@ -302,6 +302,7 @@ class SecretStore(StoreBase):
|
||||
|
||||
for group in self.machine.deployment["sops"]["defaultGroups"]:
|
||||
allow_member(
|
||||
self.machine.flake_dir,
|
||||
groups_folder(secret_path),
|
||||
sops_groups_folder(self.machine.flake_dir),
|
||||
group,
|
||||
@@ -310,6 +311,7 @@ class SecretStore(StoreBase):
|
||||
)
|
||||
|
||||
update_keys(
|
||||
self.machine.flake_dir,
|
||||
secret_path,
|
||||
collect_keys_for_path(secret_path),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user