re-encrypt secrets after rotating users/machines keys
This commit is contained in:
parent
b6d5f8a6ce
commit
0fa36252c2
@ -3,14 +3,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .. import tty
|
from .. import tty
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from .folders import sops_secrets_folder
|
from .secrets import update_secrets
|
||||||
from .secrets import collect_keys_for_path, list_secrets
|
from .sops import default_sops_key_path, generate_private_key, get_public_key
|
||||||
from .sops import (
|
|
||||||
default_sops_key_path,
|
|
||||||
generate_private_key,
|
|
||||||
get_public_key,
|
|
||||||
update_keys,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_key() -> str:
|
def generate_key() -> str:
|
||||||
@ -44,12 +38,7 @@ def show_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def update_command(args: argparse.Namespace) -> None:
|
def update_command(args: argparse.Namespace) -> None:
|
||||||
flake_dir = Path(args.flake)
|
flake_dir = Path(args.flake)
|
||||||
for name in list_secrets(flake_dir):
|
update_secrets(flake_dir)
|
||||||
secret_path = sops_secrets_folder(flake_dir) / name
|
|
||||||
update_keys(
|
|
||||||
secret_path,
|
|
||||||
list(sorted(collect_keys_for_path(secret_path))),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register_key_parser(parser: argparse.ArgumentParser) -> None:
|
def register_key_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
@ -6,6 +6,7 @@ from ..git import commit_files
|
|||||||
from ..machines.types import machine_name_type, validate_hostname
|
from ..machines.types import machine_name_type, validate_hostname
|
||||||
from . import secrets
|
from . import secrets
|
||||||
from .folders import list_objects, remove_object, sops_machines_folder
|
from .folders import list_objects, remove_object, sops_machines_folder
|
||||||
|
from .secrets import update_secrets
|
||||||
from .sops import read_key, write_key
|
from .sops import read_key, write_key
|
||||||
from .types import public_or_private_age_key_type, secret_name_type
|
from .types import public_or_private_age_key_type, secret_name_type
|
||||||
|
|
||||||
@ -13,6 +14,12 @@ from .types import public_or_private_age_key_type, secret_name_type
|
|||||||
def add_machine(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
def add_machine(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
||||||
path = sops_machines_folder(flake_dir) / name
|
path = sops_machines_folder(flake_dir) / name
|
||||||
write_key(path, key, force)
|
write_key(path, key, force)
|
||||||
|
paths = [path]
|
||||||
|
|
||||||
|
def filter_machine_secrets(secret: Path) -> bool:
|
||||||
|
return secret.joinpath("machines", name).exists()
|
||||||
|
|
||||||
|
paths.extend(update_secrets(flake_dir, filter_secrets=filter_machine_secrets))
|
||||||
commit_files(
|
commit_files(
|
||||||
[path],
|
[path],
|
||||||
flake_dir,
|
flake_dir,
|
||||||
|
@ -3,6 +3,7 @@ import getpass
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import IO
|
||||||
@ -21,6 +22,23 @@ from .sops import decrypt_file, encrypt_file, ensure_sops_key, read_key, update_
|
|||||||
from .types import VALID_SECRET_NAME, secret_name_type
|
from .types import VALID_SECRET_NAME, secret_name_type
|
||||||
|
|
||||||
|
|
||||||
|
def update_secrets(
|
||||||
|
flake_dir: Path, filter_secrets: Callable[[Path], bool] = lambda _: True
|
||||||
|
) -> list[Path]:
|
||||||
|
changed_files = []
|
||||||
|
for name in list_secrets(flake_dir):
|
||||||
|
secret_path = sops_secrets_folder(flake_dir) / name
|
||||||
|
if not filter_secrets(secret_path):
|
||||||
|
continue
|
||||||
|
changed_files.extend(
|
||||||
|
update_keys(
|
||||||
|
secret_path,
|
||||||
|
list(sorted(collect_keys_for_path(secret_path))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return changed_files
|
||||||
|
|
||||||
|
|
||||||
def collect_keys_for_type(folder: Path) -> set[str]:
|
def collect_keys_for_type(folder: Path) -> set[str]:
|
||||||
if not folder.exists():
|
if not folder.exists():
|
||||||
return set()
|
return set()
|
||||||
|
@ -117,8 +117,10 @@ def sops_manifest(keys: list[str]) -> Iterator[Path]:
|
|||||||
yield Path(manifest.name)
|
yield Path(manifest.name)
|
||||||
|
|
||||||
|
|
||||||
def update_keys(secret_path: Path, keys: list[str]) -> None:
|
def update_keys(secret_path: Path, keys: list[str]) -> list[Path]:
|
||||||
with sops_manifest(keys) as manifest:
|
with sops_manifest(keys) as manifest:
|
||||||
|
secret_path = secret_path / "secret"
|
||||||
|
time_before = secret_path.stat().st_mtime
|
||||||
cmd = nix_shell(
|
cmd = nix_shell(
|
||||||
["nixpkgs#sops"],
|
["nixpkgs#sops"],
|
||||||
[
|
[
|
||||||
@ -127,10 +129,13 @@ def update_keys(secret_path: Path, keys: list[str]) -> None:
|
|||||||
str(manifest),
|
str(manifest),
|
||||||
"updatekeys",
|
"updatekeys",
|
||||||
"--yes",
|
"--yes",
|
||||||
str(secret_path / "secret"),
|
str(secret_path),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
run(cmd, log=Log.BOTH, error_msg=f"Could not update keys for {secret_path}")
|
run(cmd, log=Log.BOTH, error_msg=f"Could not update keys for {secret_path}")
|
||||||
|
if time_before == secret_path.stat().st_mtime:
|
||||||
|
return []
|
||||||
|
return [secret_path]
|
||||||
|
|
||||||
|
|
||||||
def encrypt_file(
|
def encrypt_file(
|
||||||
@ -202,7 +207,9 @@ def write_key(path: Path, publickey: str, overwrite: bool) -> None:
|
|||||||
flags |= os.O_EXCL
|
flags |= os.O_EXCL
|
||||||
fd = os.open(path / "key.json", flags)
|
fd = os.open(path / "key.json", flags)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
raise ClanError(f"{path.name} already exists in {path}. Use --force to overwrite.")
|
raise ClanError(
|
||||||
|
f"{path.name} already exists in {path}. Use --force to overwrite."
|
||||||
|
)
|
||||||
with os.fdopen(fd, "w") as f:
|
with os.fdopen(fd, "w") as f:
|
||||||
json.dump({"publickey": publickey, "type": "age"}, f, indent=2)
|
json.dump({"publickey": publickey, "type": "age"}, f, indent=2)
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from ..errors import ClanError
|
|||||||
from ..git import commit_files
|
from ..git import commit_files
|
||||||
from . import secrets
|
from . import secrets
|
||||||
from .folders import list_objects, remove_object, sops_users_folder
|
from .folders import list_objects, remove_object, sops_users_folder
|
||||||
|
from .secrets import update_secrets
|
||||||
from .sops import read_key, write_key
|
from .sops import read_key, write_key
|
||||||
from .types import (
|
from .types import (
|
||||||
VALID_USER_NAME,
|
VALID_USER_NAME,
|
||||||
@ -16,9 +17,15 @@ from .types import (
|
|||||||
|
|
||||||
def add_user(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
def add_user(flake_dir: Path, name: str, key: str, force: bool) -> None:
|
||||||
path = sops_users_folder(flake_dir) / name
|
path = sops_users_folder(flake_dir) / name
|
||||||
|
|
||||||
|
def filter_user_secrets(secret: Path) -> bool:
|
||||||
|
return secret.joinpath("users", name).exists()
|
||||||
|
|
||||||
write_key(path, key, force)
|
write_key(path, key, force)
|
||||||
|
paths = [path]
|
||||||
|
paths.extend(update_secrets(flake_dir, filter_secrets=filter_user_secrets))
|
||||||
commit_files(
|
commit_files(
|
||||||
[path],
|
paths,
|
||||||
flake_dir,
|
flake_dir,
|
||||||
f"Add user {name} to secrets",
|
f"Add user {name} to secrets",
|
||||||
)
|
)
|
||||||
|
@ -37,9 +37,21 @@ def _test_identities(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert (sops_folder / what / "foo" / "key.json").exists()
|
assert (sops_folder / what / "foo" / "key.json").exists()
|
||||||
with pytest.raises(ClanError):
|
|
||||||
cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey])
|
|
||||||
|
|
||||||
|
with pytest.raises(ClanError): # raises "foo already exists"
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"--flake",
|
||||||
|
str(test_flake.path),
|
||||||
|
"secrets",
|
||||||
|
what,
|
||||||
|
"add",
|
||||||
|
"foo",
|
||||||
|
age_keys[0].pubkey,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# rotate the key
|
||||||
cli.run(
|
cli.run(
|
||||||
[
|
[
|
||||||
"--flake",
|
"--flake",
|
||||||
@ -49,7 +61,7 @@ def _test_identities(
|
|||||||
"add",
|
"add",
|
||||||
"-f",
|
"-f",
|
||||||
"foo",
|
"foo",
|
||||||
age_keys[0].privkey,
|
age_keys[1].privkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -65,7 +77,7 @@ def _test_identities(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
out = capsys.readouterr() # empty the buffer
|
out = capsys.readouterr() # empty the buffer
|
||||||
assert age_keys[0].pubkey in out.out
|
assert age_keys[1].pubkey in out.out
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
|
cli.run(["--flake", str(test_flake.path), "secrets", what, "list"])
|
||||||
@ -291,7 +303,7 @@ def test_secrets(
|
|||||||
"machines",
|
"machines",
|
||||||
"add",
|
"add",
|
||||||
"machine1",
|
"machine1",
|
||||||
age_keys[0].pubkey,
|
age_keys[1].pubkey,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
cli.run(
|
cli.run(
|
||||||
@ -309,6 +321,27 @@ def test_secrets(
|
|||||||
cli.run(["--flake", str(test_flake.path), "secrets", "machines", "list"])
|
cli.run(["--flake", str(test_flake.path), "secrets", "machines", "list"])
|
||||||
assert capsys.readouterr().out == "machine1\n"
|
assert capsys.readouterr().out == "machine1\n"
|
||||||
|
|
||||||
|
with use_key(age_keys[1].privkey, monkeypatch):
|
||||||
|
capsys.readouterr()
|
||||||
|
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||||
|
|
||||||
|
assert capsys.readouterr().out == "foo"
|
||||||
|
|
||||||
|
# rotate machines key
|
||||||
|
cli.run(
|
||||||
|
[
|
||||||
|
"--flake",
|
||||||
|
str(test_flake.path),
|
||||||
|
"secrets",
|
||||||
|
"machines",
|
||||||
|
"add",
|
||||||
|
"-f",
|
||||||
|
"machine1",
|
||||||
|
age_keys[0].privkey,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# should also rotate the encrypted secret
|
||||||
with use_key(age_keys[0].privkey, monkeypatch):
|
with use_key(age_keys[0].privkey, monkeypatch):
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
cli.run(["--flake", str(test_flake.path), "secrets", "get", "key"])
|
||||||
|
Loading…
Reference in New Issue
Block a user