From 3f6fa0eeca368e510f0f295b7ca18365efe2b158 Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 6 Sep 2023 16:08:36 +0200 Subject: [PATCH 01/18] clanCore secrets: add secretStore option --- nixosModules/clanCore/secrets/default.nix | 7 +++++++ nixosModules/clanCore/secrets/sops.nix | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 48fef2956..fa961ddda 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -1,5 +1,12 @@ { config, lib, ... }: { + options.clanCore.secretStore = lib.mkOption { + type = lib.types.enum [ "sops" "password-store" "custom" ]; + default = "sops"; + description = '' + method to store secrets + ''; + }; options.clanCore.secrets = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule (secret: { diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index ab9772282..237148911 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -21,7 +21,7 @@ let secrets = filterDir containsMachineOrGroups secretsDir; in { - config = { + config = lib.mkIf (config.clanCore.secretStore == "sops") { system.clan.generateSecrets = pkgs.writeScript "generate-secrets" '' #!/bin/sh set -efu From 5285423479136a5a71f2246a1e9a27444d64c024 Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 6 Sep 2023 16:09:43 +0200 Subject: [PATCH 02/18] secrets: add password-store implementation --- nixosModules/clanCore/secrets/default.nix | 3 +- .../clanCore/secrets/password-store.nix | 120 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 nixosModules/clanCore/secrets/password-store.nix diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index fa961ddda..51c6aa34e 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -68,6 +68,7 @@ })); }; imports = [ - ./sops.nix # for now we have only one implementation, thats why we import it here and not in clanModules + ./sops.nix + ./password-store.nix ]; } diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix new file mode 100644 index 000000000..7b559a5c1 --- /dev/null +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -0,0 +1,120 @@ +{ config, lib, pkgs, ... }: +let + passwordstoreDir = "$HOME/.password-store"; +in +{ + options.clan.password-store.targetDirectory = lib.mkOption { + type = lib.types.path; + default = "/etc/secrets"; + description = '' + The directory where the password store is deployed to. + ''; + }; + config = lib.mkIf (config.clanCore.secretStore == "password-store") { + system.clan.generateSecrets = pkgs.writeScript "generate-secrets" '' + #!/bin/sh + set -efu + set -x # remove for prod + + PATH=${lib.makeBinPath [ + pkgs.pass + ]}:$PATH + + # TODO maybe initialize password store if it doesn't exist yet + + ${lib.foldlAttrs (acc: n: v: '' + ${acc} + # ${n} + # if any of the secrets are missing, we regenerate all connected facts/secrets + (if ! ${lib.concatMapStringsSep " && " (x: "pass show machines/${config.clanCore.machineName}/${x.name} >/dev/null") (lib.attrValues v.secrets)}; then + + facts=$(mktemp -d) + trap "rm -rf $facts" EXIT + secrets=$(mktemp -d) + trap "rm -rf $secrets" EXIT + ${v.generator} + + ${lib.concatMapStrings (fact: '' + mkdir -p "$(dirname ${fact.path})" + cp "$facts"/${fact.name} ${fact.path} + '') (lib.attrValues v.facts)} + + ${lib.concatMapStrings (secret: '' + cat "$secrets"/${secret.name} | pass insert -m machines/${config.clanCore.machineName}/${secret.name} + '') (lib.attrValues v.secrets)} + fi) + '') "" config.clanCore.secrets} + ''; + system.clan.deploySecrets = pkgs.writeScript "deploy-secrets" '' + #!/bin/sh + set -efu + set -x # remove for prod + + target=$1 + + umask 0077 + + PATH=${lib.makeBinPath [ + pkgs.pass + pkgs.git + pkgs.findutils + pkgs.openssh + pkgs.rsync + ]}:$PATH + + if test -e ${passwordstoreDir}/.git; then + local_pass_info=$( + git -C ${passwordstoreDir} log -1 --format=%H machines/${config.clanCore.machineName} + # we append a hash for every symlink, otherwise we would miss updates on + # files where the symlink points to + find ${passwordstoreDir}/machines/${config.clanCore.machineName} -type l \ + -exec realpath {} + | + sort | + xargs -r -n 1 git -C ${passwordstoreDir} log -1 --format=%H + ) + remote_pass_info=$(ssh "$target" -- ${lib.escapeShellArg '' + cat ${config.clan.password-store.targetDirectory}/.pass_info || : + ''}) + + if test "$local_pass_info" = "$remote_pass_info"; then + echo secrets already match + exit 0 + fi + fi + + tmp_dir=$(mktemp -dt populate-pass.XXXXXXXX) + trap cleanup EXIT + cleanup() { + rm -fR "$tmp_dir" + } + + find ${passwordstoreDir}/machines/${config.clanCore.machineName} -type f -follow ! -name .gpg-id | + while read -r gpg_path; do + + rel_name=''${gpg_path#${passwordstoreDir}} + rel_name=''${rel_name%.gpg} + + pass_date=$( + if test -e ${passwordstoreDir}/.git; then + git -C ${passwordstoreDir} log -1 --format=%aI "$gpg_path" + fi + ) + pass_name=$rel_name + tmp_path=$tmp_dir/$(basename $rel_name) + + mkdir -p "$(dirname "$tmp_path")" + pass show "$pass_name" > "$tmp_path" + if [ -n "$pass_date" ]; then + touch -d "$pass_date" "$tmp_path" + fi + done + + if test -n "''${local_pass_info-}"; then + echo "$local_pass_info" > "$tmp_dir"/.pass_info + fi + + rsync --mkpath --delete -a "$tmp_dir"/ "$target":${config.clan.password-store.targetDirectory}/ + ''; + }; +} + From 798e85ee8a36fa10a654afec7a889d8e89f260c5 Mon Sep 17 00:00:00 2001 From: lassulus Date: Sat, 9 Sep 2023 15:38:28 +0200 Subject: [PATCH 03/18] clan secrets generate: use get_clan_flake_toplevel --- pkgs/clan-cli/clan_cli/secrets/generate.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index 0b01a8c85..2c0fb9a93 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -4,8 +4,11 @@ import sys from clan_cli.errors import ClanError +from ..dirs import get_clan_flake_toplevel -def get_secret_script(machine: str) -> None: + +def generate_secrets(machine: str) -> None: + clan_flake = get_clan_flake_toplevel() proc = subprocess.run( [ "nix", @@ -13,12 +16,13 @@ def get_secret_script(machine: str) -> None: "--impure", "--print-out-paths", "--expr", - "let f = builtins.getFlake (toString ./.); in " - f"(f.nixosConfigurations.{machine}.extendModules " - "{ modules = [{ clanCore.clanDir = toString ./.; }]; })" - ".config.system.clan.generateSecrets", + f'let f = builtins.getFlake "{clan_flake}"; in ' + "(f.nixosConfigurations." + f"{machine}" + ".extendModules { modules = [{ clanCore.clanDir = " + f"{clan_flake}" + "; }]; }).config.system.clan.generateSecrets", ], - check=True, capture_output=True, text=True, ) @@ -30,7 +34,6 @@ def get_secret_script(machine: str) -> None: print(secret_generator_script) secret_generator = subprocess.run( [secret_generator_script], - check=True, ) if secret_generator.returncode != 0: @@ -40,7 +43,7 @@ def get_secret_script(machine: str) -> None: def generate_command(args: argparse.Namespace) -> None: - get_secret_script(args.machine) + generate_secrets(args.machine) def register_generate_parser(parser: argparse.ArgumentParser) -> None: From 285041026da56463d697c9ff8e4ca850b79e587a Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 11 Sep 2023 21:42:45 +0200 Subject: [PATCH 04/18] clanCore sops: add dummy deployScript --- nixosModules/clanCore/secrets/sops.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index 237148911..f209d5317 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -64,6 +64,9 @@ in fi) '') "" config.clanCore.secrets} ''; + system.clan.deploySecrets = pkgs.writeScript "deploy-secrets" '' + echo deployment is not needed for sops secret store, since the secrets are part of the flake + ''; sops.secrets = builtins.mapAttrs (name: _: { sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret"; From ac13c5b76be4a4ef6a98eb61c9e345eea13edbfe Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 11 Sep 2023 21:44:39 +0200 Subject: [PATCH 05/18] clan-cli secrets: add deploy subcommand --- pkgs/clan-cli/clan_cli/secrets/__init__.py | 4 ++ pkgs/clan-cli/clan_cli/secrets/deploy.py | 53 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 pkgs/clan-cli/clan_cli/secrets/deploy.py diff --git a/pkgs/clan-cli/clan_cli/secrets/__init__.py b/pkgs/clan-cli/clan_cli/secrets/__init__.py index 3c161d3b5..4515bcf63 100644 --- a/pkgs/clan-cli/clan_cli/secrets/__init__.py +++ b/pkgs/clan-cli/clan_cli/secrets/__init__.py @@ -1,6 +1,7 @@ # !/usr/bin/env python3 import argparse +from .deploy import register_deploy_parser from .generate import register_generate_parser from .groups import register_groups_parser from .import_sops import register_import_sops_parser @@ -36,6 +37,9 @@ def register_parser(parser: argparse.ArgumentParser) -> None: ) register_generate_parser(parser_generate) + parser_deploy = subparser.add_parser("deploy", help="deploy secrets for machines") + register_deploy_parser(parser_deploy) + parser_key = subparser.add_parser("key", help="create and show age keys") register_key_parser(parser_key) diff --git a/pkgs/clan-cli/clan_cli/secrets/deploy.py b/pkgs/clan-cli/clan_cli/secrets/deploy.py new file mode 100644 index 000000000..16c1a1cfc --- /dev/null +++ b/pkgs/clan-cli/clan_cli/secrets/deploy.py @@ -0,0 +1,53 @@ +import argparse +import subprocess +import sys + +from clan_cli.errors import ClanError + +from ..dirs import get_clan_flake_toplevel + + +def deploy_secrets(machine: str) -> None: + clan_flake = get_clan_flake_toplevel() + proc = subprocess.run( + [ + "nix", + "build", + "--impure", + "--print-out-paths", + "--expr", + f'let f = builtins.getFlake "{clan_flake}"; in ' + "(f.nixosConfigurations." + f"{machine}" + ".extendModules { modules = [{ clanCore.clanDir = " + f"{clan_flake}" + "; }]; }).config.system.clan.deploySecrets", + ], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + print(proc.stderr, file=sys.stderr) + raise ClanError(f"failed to deploy secrets:\n{proc.stderr}") + + secret_deploy_script = proc.stdout.strip() + secret_deploy = subprocess.run( + [secret_deploy_script], + ) + + if secret_deploy.returncode != 0: + raise ClanError("failed to deploy secrets") + else: + print("successfully deployed secrets") + + +def deploy_command(args: argparse.Namespace) -> None: + deploy_secrets(args.machine) + + +def register_deploy_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "machine", + help="The machine to deploy secrets to", + ) + parser.set_defaults(func=deploy_command) From 3b0701f275e9edc83e2305fec438d999075b4551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Sep 2023 20:33:51 +0200 Subject: [PATCH 06/18] deploy: use nix-flake-archive instead of rsync to upload --- pkgs/clan-cli/clan_cli/machines/update.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index bed009f83..e6f7dcf80 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -1,5 +1,6 @@ import argparse import json +import os import subprocess from ..ssh import Host, HostGroup, HostKeyCheck @@ -13,11 +14,13 @@ def deploy_nixos(hosts: HostGroup) -> None: def deploy(h: Host) -> None: target = f"{h.user or 'root'}@{h.host}" ssh_arg = f"-p {h.port}" if h.port else "" + env = os.environ.copy() + env["NIX_SSHOPTS"] = ssh_arg res = h.run_local( ["nix", "flake", "archive", "--to", f"ssh://{target}", "--json"], check=True, stdout=subprocess.PIPE, - extra_env=dict(NIX_SSHOPTS=ssh_arg), + extra_env=env ) data = json.loads(res.stdout) path = data["path"] From 6153a9ee71a3b1d53cb078865fac6323d1ff8f1c Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 13 Sep 2023 23:16:56 +0200 Subject: [PATCH 07/18] clanCore.secrets: set default and add generate/deploy composite --- nixosModules/clanCore/secrets/default.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 51c6aa34e..622097624 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -1,4 +1,4 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: { options.clanCore.secretStore = lib.mkOption { type = lib.types.enum [ "sops" "password-store" "custom" ]; @@ -8,6 +8,7 @@ ''; }; options.clanCore.secrets = lib.mkOption { + default = { }; type = lib.types.attrsOf (lib.types.submodule (secret: { options = { @@ -67,6 +68,10 @@ }; })); }; + config.system.build.generateDeploySecrets = pkgs.writeScript "generate_deploy_secrets" '' + ${config.system.build.generateSecrets} + ${config.system.build.deploySecrets} + ''; imports = [ ./sops.nix ./password-store.nix From 23c979f8db79d1580b561faed209b31fe28ffc8c Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 13 Sep 2023 23:18:05 +0200 Subject: [PATCH 08/18] secrets deploy/generate: use nix_build_machine --- pkgs/clan-cli/clan_cli/secrets/deploy.py | 30 ++++++++++------------ pkgs/clan-cli/clan_cli/secrets/generate.py | 25 ++++++++---------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/secrets/deploy.py b/pkgs/clan-cli/clan_cli/secrets/deploy.py index 16c1a1cfc..50fc984af 100644 --- a/pkgs/clan-cli/clan_cli/secrets/deploy.py +++ b/pkgs/clan-cli/clan_cli/secrets/deploy.py @@ -4,25 +4,20 @@ import sys from clan_cli.errors import ClanError -from ..dirs import get_clan_flake_toplevel +from ..nix import nix_build_machine def deploy_secrets(machine: str) -> None: - clan_flake = get_clan_flake_toplevel() proc = subprocess.run( - [ - "nix", - "build", - "--impure", - "--print-out-paths", - "--expr", - f'let f = builtins.getFlake "{clan_flake}"; in ' - "(f.nixosConfigurations." - f"{machine}" - ".extendModules { modules = [{ clanCore.clanDir = " - f"{clan_flake}" - "; }]; }).config.system.clan.deploySecrets", - ], + nix_build_machine( + machine=machine, + attr=[ + "config", + "system", + "clan", + "deploySecrets", + ], + ), capture_output=True, text=True, ) @@ -32,7 +27,10 @@ def deploy_secrets(machine: str) -> None: secret_deploy_script = proc.stdout.strip() secret_deploy = subprocess.run( - [secret_deploy_script], + [ + secret_deploy_script, + f"root@{machine}", + ], ) if secret_deploy.returncode != 0: diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index 2c0fb9a93..3093eee18 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -4,25 +4,20 @@ import sys from clan_cli.errors import ClanError -from ..dirs import get_clan_flake_toplevel +from ..nix import nix_build_machine def generate_secrets(machine: str) -> None: - clan_flake = get_clan_flake_toplevel() proc = subprocess.run( - [ - "nix", - "build", - "--impure", - "--print-out-paths", - "--expr", - f'let f = builtins.getFlake "{clan_flake}"; in ' - "(f.nixosConfigurations." - f"{machine}" - ".extendModules { modules = [{ clanCore.clanDir = " - f"{clan_flake}" - "; }]; }).config.system.clan.generateSecrets", - ], + nix_build_machine( + machine=machine, + attr=[ + "config", + "system", + "clan", + "generateSecrets", + ], + ), capture_output=True, text=True, ) From 0e3f8bb3f9fea30f03e3117ac62ff99d0de29b62 Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 13 Sep 2023 23:18:54 +0200 Subject: [PATCH 09/18] clan-cli nix_build_machine: cast flake_url to str --- pkgs/clan-cli/clan_cli/nix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 559b82387..6d08a33e9 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -10,7 +10,7 @@ def nix_build_machine( machine: str, attr: list[str], flake_url: Path | None = None ) -> list[str]: if flake_url is None: - flake_url = get_clan_flake_toplevel() + flake_url = str(get_clan_flake_toplevel()) payload = json.dumps( dict( clan_flake=flake_url, From c487280ba9a095795bbc9bb1a1e564b9e61b63ec Mon Sep 17 00:00:00 2001 From: lassulus Date: Wed, 13 Sep 2023 23:19:39 +0200 Subject: [PATCH 10/18] clan-cli machines update: generate and deploy secrets --- pkgs/clan-cli/clan_cli/machines/update.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index e6f7dcf80..f2d2df54e 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -4,6 +4,8 @@ import os import subprocess from ..ssh import Host, HostGroup, HostKeyCheck +from ..secrets.deploy import deploy_secrets +from ..secrets.generate import generate_secrets def deploy_nixos(hosts: HostGroup) -> None: @@ -32,6 +34,9 @@ def deploy_nixos(hosts: HostGroup) -> None: ssh_arg += " -i " + h.key if h.key else "" + generate_secrets(h.host) + deploy_secrets(h.host) + flake_attr = h.meta.get("flake_attr", "") if flake_attr: flake_attr = "#" + flake_attr From acf1c0b87ae6da3f59a3d0453cd8a9eef0b801eb Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 10:37:16 +0200 Subject: [PATCH 11/18] lib jsonschema: add path --- lib/jsonschema/default.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 26c399e45..90a8fb2bd 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -7,6 +7,7 @@ let float = "number"; int = "integer"; str = "string"; + path = "string"; # TODO add prober path checks }; # remove _module attribute from options @@ -103,6 +104,13 @@ rec { type = "string"; } + # parse string + else if option.type.name == "path" + # return jsonschema property definition for path + then default // description // { + type = "string"; + } + # parse enum else if option.type.name == "enum" # return jsonschema property definition for enum From 55fc055549028cecefa8280b1bae85b95b215da7 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 10:42:04 +0200 Subject: [PATCH 12/18] clan_cli/nix: convert path to string --- pkgs/clan-cli/clan_cli/nix.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 6d08a33e9..06b564fd1 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -10,10 +10,10 @@ def nix_build_machine( machine: str, attr: list[str], flake_url: Path | None = None ) -> list[str]: if flake_url is None: - flake_url = str(get_clan_flake_toplevel()) + flake_url = get_clan_flake_toplevel() payload = json.dumps( dict( - clan_flake=flake_url, + clan_flake=flake_url.as_posix(), machine=machine, attr=attr, ) From c5786614bf7e82c26d085bf4e0baef39cb4f080f Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 13:49:20 +0200 Subject: [PATCH 13/18] clan-cli secrets: deploy -> upload --- nixosModules/clanCore/secrets/default.nix | 6 +-- .../clanCore/secrets/password-store.nix | 4 +- nixosModules/clanCore/secrets/sops.nix | 4 +- pkgs/clan-cli/clan_cli/machines/update.py | 4 +- pkgs/clan-cli/clan_cli/secrets/__init__.py | 6 +-- pkgs/clan-cli/clan_cli/secrets/deploy.py | 51 ------------------- pkgs/clan-cli/clan_cli/secrets/upload.py | 51 +++++++++++++++++++ 7 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 pkgs/clan-cli/clan_cli/secrets/deploy.py create mode 100644 pkgs/clan-cli/clan_cli/secrets/upload.py diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 622097624..f3e7b5ec8 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -68,9 +68,9 @@ }; })); }; - config.system.build.generateDeploySecrets = pkgs.writeScript "generate_deploy_secrets" '' - ${config.system.build.generateSecrets} - ${config.system.build.deploySecrets} + config.system.build.generateUploadSecrets = pkgs.writeScript "generate_upload_secrets" '' + ${config.system.clan.generateSecrets} + ${config.system.clan.uploadSecrets} ''; imports = [ ./sops.nix diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix index 7b559a5c1..bc11b9e39 100644 --- a/nixosModules/clanCore/secrets/password-store.nix +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -7,7 +7,7 @@ in type = lib.types.path; default = "/etc/secrets"; description = '' - The directory where the password store is deployed to. + The directory where the password store is uploaded to. ''; }; config = lib.mkIf (config.clanCore.secretStore == "password-store") { @@ -45,7 +45,7 @@ in fi) '') "" config.clanCore.secrets} ''; - system.clan.deploySecrets = pkgs.writeScript "deploy-secrets" '' + system.clan.uploadSecrets = pkgs.writeScript "upload-secrets" '' #!/bin/sh set -efu set -x # remove for prod diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index f209d5317..5fe455d8a 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -64,8 +64,8 @@ in fi) '') "" config.clanCore.secrets} ''; - system.clan.deploySecrets = pkgs.writeScript "deploy-secrets" '' - echo deployment is not needed for sops secret store, since the secrets are part of the flake + system.clan.uploadSecrets = pkgs.writeScript "upload-secrets" '' + echo upload is not needed for sops secret store, since the secrets are part of the flake ''; sops.secrets = builtins.mapAttrs (name: _: { diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index f2d2df54e..30726b68b 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -4,7 +4,7 @@ import os import subprocess from ..ssh import Host, HostGroup, HostKeyCheck -from ..secrets.deploy import deploy_secrets +from ..secrets.upload import upload_secrets from ..secrets.generate import generate_secrets @@ -35,7 +35,7 @@ def deploy_nixos(hosts: HostGroup) -> None: ssh_arg += " -i " + h.key if h.key else "" generate_secrets(h.host) - deploy_secrets(h.host) + upload_secrets(h.host) flake_attr = h.meta.get("flake_attr", "") if flake_attr: diff --git a/pkgs/clan-cli/clan_cli/secrets/__init__.py b/pkgs/clan-cli/clan_cli/secrets/__init__.py index 4515bcf63..01ac958d2 100644 --- a/pkgs/clan-cli/clan_cli/secrets/__init__.py +++ b/pkgs/clan-cli/clan_cli/secrets/__init__.py @@ -1,13 +1,13 @@ # !/usr/bin/env python3 import argparse -from .deploy import register_deploy_parser from .generate import register_generate_parser from .groups import register_groups_parser from .import_sops import register_import_sops_parser from .key import register_key_parser from .machines import register_machines_parser from .secrets import register_secrets_parser +from .upload import register_upload_parser from .users import register_users_parser @@ -37,8 +37,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None: ) register_generate_parser(parser_generate) - parser_deploy = subparser.add_parser("deploy", help="deploy secrets for machines") - register_deploy_parser(parser_deploy) + parser_upload = subparser.add_parser("upload", help="upload secrets for machines") + register_upload_parser(parser_upload) parser_key = subparser.add_parser("key", help="create and show age keys") register_key_parser(parser_key) diff --git a/pkgs/clan-cli/clan_cli/secrets/deploy.py b/pkgs/clan-cli/clan_cli/secrets/deploy.py deleted file mode 100644 index 50fc984af..000000000 --- a/pkgs/clan-cli/clan_cli/secrets/deploy.py +++ /dev/null @@ -1,51 +0,0 @@ -import argparse -import subprocess -import sys - -from clan_cli.errors import ClanError - -from ..nix import nix_build_machine - - -def deploy_secrets(machine: str) -> None: - proc = subprocess.run( - nix_build_machine( - machine=machine, - attr=[ - "config", - "system", - "clan", - "deploySecrets", - ], - ), - capture_output=True, - text=True, - ) - if proc.returncode != 0: - print(proc.stderr, file=sys.stderr) - raise ClanError(f"failed to deploy secrets:\n{proc.stderr}") - - secret_deploy_script = proc.stdout.strip() - secret_deploy = subprocess.run( - [ - secret_deploy_script, - f"root@{machine}", - ], - ) - - if secret_deploy.returncode != 0: - raise ClanError("failed to deploy secrets") - else: - print("successfully deployed secrets") - - -def deploy_command(args: argparse.Namespace) -> None: - deploy_secrets(args.machine) - - -def register_deploy_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "machine", - help="The machine to deploy secrets to", - ) - parser.set_defaults(func=deploy_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/upload.py b/pkgs/clan-cli/clan_cli/secrets/upload.py new file mode 100644 index 000000000..344a00632 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/secrets/upload.py @@ -0,0 +1,51 @@ +import argparse +import subprocess +import sys + +from clan_cli.errors import ClanError + +from ..nix import nix_build_machine + + +def upload_secrets(machine: str) -> None: + proc = subprocess.run( + nix_build_machine( + machine=machine, + attr=[ + "config", + "system", + "clan", + "uploadSecrets", + ], + ), + capture_output=True, + text=True, + ) + if proc.returncode != 0: + print(proc.stderr, file=sys.stderr) + raise ClanError(f"failed to upload secrets:\n{proc.stderr}") + + secret_upload_script = proc.stdout.strip() + secret_upload = subprocess.run( + [ + secret_upload_script, + f"root@{machine}", + ], + ) + + if secret_upload.returncode != 0: + raise ClanError("failed to upload secrets") + else: + print("successfully uploaded secrets") + + +def upload_command(args: argparse.Namespace) -> None: + upload_secrets(args.machine) + + +def register_upload_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "machine", + help="The machine to upload secrets to", + ) + parser.set_defaults(func=upload_command) From c5c2a848c7ab36809728b531a5e8cc25b70fdeae Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 13:49:57 +0200 Subject: [PATCH 14/18] secrets pass: append openssh to PATH so we use systems openssh first --- nixosModules/clanCore/secrets/password-store.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix index bc11b9e39..0cb5873cf 100644 --- a/nixosModules/clanCore/secrets/password-store.nix +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -58,9 +58,8 @@ in pkgs.pass pkgs.git pkgs.findutils - pkgs.openssh pkgs.rsync - ]}:$PATH + ]}:$PATH:${lib.getBin pkgs.openssh} if test -e ${passwordstoreDir}/.git; then local_pass_info=$( From a59e8478fac17d9ca4c9c97dff506008793fc87c Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 13:51:13 +0200 Subject: [PATCH 15/18] clan-cli nix_build_machine: don't create result link --- pkgs/clan-cli/clan_cli/nix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 06b564fd1..212b55b9f 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -22,6 +22,7 @@ def nix_build_machine( return [ "nix", "build", + "--no-link", "--impure", "--print-out-paths", "--expr", From 6b7301cefbd406c0246804f160687f6f712d1e49 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 14:15:51 +0200 Subject: [PATCH 16/18] clanCore secrets: document custom store --- nixosModules/clanCore/secrets/default.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index f3e7b5ec8..e8c115e30 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -5,6 +5,8 @@ default = "sops"; description = '' method to store secrets + custom can be used to define a custom secret store. + one would have to define system.clan.generateSecrets and system.clan.uploadSecrets ''; }; options.clanCore.secrets = lib.mkOption { From 0132abc547f432f4ba1ee7b92fa027c450e3dae2 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 15:22:13 +0200 Subject: [PATCH 17/18] secrets: use CLAN_DIR instead of clanCore.clanDir for fact storage --- nixosModules/clanCore/secrets/default.nix | 4 +-- .../clanCore/secrets/password-store.nix | 7 ++-- nixosModules/clanCore/secrets/sops.nix | 5 +-- pkgs/clan-cli/clan_cli/nix.py | 34 +++---------------- pkgs/clan-cli/clan_cli/secrets/generate.py | 21 +++++++----- pkgs/clan-cli/clan_cli/secrets/upload.py | 17 +++++----- 6 files changed, 32 insertions(+), 56 deletions(-) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index e8c115e30..700f5d078 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -59,10 +59,10 @@ description = '' path to a fact which is generated by the generator ''; - default = "${config.clanCore.clanDir}/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; + default = "machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; }; value = lib.mkOption { - default = builtins.readFile fact.config.path; + default = builtins.readFile "${config.clanCore.clanDir}/fact.config.path"; }; }; })); diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix index 0cb5873cf..3db20e03a 100644 --- a/nixosModules/clanCore/secrets/password-store.nix +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -1,6 +1,6 @@ { config, lib, pkgs, ... }: let - passwordstoreDir = "$HOME/.password-store"; + passwordstoreDir = "\${PASSWORD_STORE_DIR:-$HOME/.password-store}"; in { options.clan.password-store.targetDirectory = lib.mkOption { @@ -14,8 +14,8 @@ in system.clan.generateSecrets = pkgs.writeScript "generate-secrets" '' #!/bin/sh set -efu - set -x # remove for prod + test -d "$CLAN_DIR" PATH=${lib.makeBinPath [ pkgs.pass ]}:$PATH @@ -36,7 +36,7 @@ in ${lib.concatMapStrings (fact: '' mkdir -p "$(dirname ${fact.path})" - cp "$facts"/${fact.name} ${fact.path} + cp "$facts"/${fact.name} "$CLAN_DIR"/${fact.path} '') (lib.attrValues v.facts)} ${lib.concatMapStrings (secret: '' @@ -48,7 +48,6 @@ in system.clan.uploadSecrets = pkgs.writeScript "upload-secrets" '' #!/bin/sh set -efu - set -x # remove for prod target=$1 diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index 5fe455d8a..25d0af6e6 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -25,7 +25,8 @@ in system.clan.generateSecrets = pkgs.writeScript "generate-secrets" '' #!/bin/sh set -efu - set -x # remove for prod + + test -d "$CLAN_DIR" PATH=$PATH:${lib.makeBinPath [ config.clanCore.clanPkgs.clan-cli @@ -55,7 +56,7 @@ in ${lib.concatMapStrings (fact: '' mkdir -p "$(dirname ${fact.path})" - cp "$facts"/${fact.name} ${fact.path} + cp "$facts"/${fact.name} "$CLAN_DIR"/${fact.path} '') (lib.attrValues v.facts)} ${lib.concatMapStrings (secret: '' diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index 212b55b9f..0a34face1 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -1,44 +1,18 @@ -import json import os import tempfile -from pathlib import Path -from .dirs import get_clan_flake_toplevel, nixpkgs_flake, nixpkgs_source, unfree_nixpkgs +from .dirs import nixpkgs_flake, nixpkgs_source, unfree_nixpkgs -def nix_build_machine( - machine: str, attr: list[str], flake_url: Path | None = None +def nix_build( + flags: list[str], ) -> list[str]: - if flake_url is None: - flake_url = get_clan_flake_toplevel() - payload = json.dumps( - dict( - clan_flake=flake_url.as_posix(), - machine=machine, - attr=attr, - ) - ) - escaped_payload = json.dumps(payload) return [ "nix", "build", "--no-link", - "--impure", "--print-out-paths", - "--expr", - f"let args = builtins.fromJSON {escaped_payload}; in " - """ - let - flake = builtins.getFlake args.clan_flake; - config = flake.nixosConfigurations.${args.machine}.extendModules { - modules = [{ - clanCore.clanDir = args.clan_flake; - }]; - }; - in - flake.inputs.nixpkgs.lib.getAttrFromPath args.attr config - """, - ] + ] + flags def nix_eval(flags: list[str]) -> list[str]: diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index 3093eee18..782bb555c 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -1,22 +1,24 @@ import argparse +import os import subprocess import sys from clan_cli.errors import ClanError -from ..nix import nix_build_machine +from ..dirs import get_clan_flake_toplevel +from ..nix import nix_build def generate_secrets(machine: str) -> None: + clan_dir = get_clan_flake_toplevel().as_posix().strip() + env = os.environ.copy() + env["CLAN_DIR"] = clan_dir + proc = subprocess.run( - nix_build_machine( - machine=machine, - attr=[ - "config", - "system", - "clan", - "generateSecrets", - ], + nix_build( + [ + f'path:{clan_dir}#nixosConfigurations."{machine}".config.system.clan.generateSecrets' + ] ), capture_output=True, text=True, @@ -29,6 +31,7 @@ def generate_secrets(machine: str) -> None: print(secret_generator_script) secret_generator = subprocess.run( [secret_generator_script], + env=env, ) if secret_generator.returncode != 0: diff --git a/pkgs/clan-cli/clan_cli/secrets/upload.py b/pkgs/clan-cli/clan_cli/secrets/upload.py index 344a00632..8dc4afe3b 100644 --- a/pkgs/clan-cli/clan_cli/secrets/upload.py +++ b/pkgs/clan-cli/clan_cli/secrets/upload.py @@ -4,19 +4,18 @@ import sys from clan_cli.errors import ClanError -from ..nix import nix_build_machine +from ..dirs import get_clan_flake_toplevel +from ..nix import nix_build def upload_secrets(machine: str) -> None: + clan_dir = get_clan_flake_toplevel().as_posix() + proc = subprocess.run( - nix_build_machine( - machine=machine, - attr=[ - "config", - "system", - "clan", - "uploadSecrets", - ], + nix_build( + [ + f'{clan_dir}#nixosConfigurations."{machine}".config.system.clan.uploadSecrets' + ] ), capture_output=True, text=True, From 8d29d0e69c4f3cd93ee2ca69d2d2d971ddb150b3 Mon Sep 17 00:00:00 2001 From: lassulus Date: Thu, 14 Sep 2023 16:57:38 +0200 Subject: [PATCH 18/18] clan-cli: get deploymentAddress from clan.networking --- nixosModules/clanCore/flake-module.nix | 1 + nixosModules/clanCore/networking.nix | 15 +++++++ pkgs/clan-cli/clan_cli/machines/update.py | 51 ++++++++++++++++------- pkgs/clan-cli/clan_cli/secrets/upload.py | 24 +++++++---- 4 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 nixosModules/clanCore/networking.nix diff --git a/nixosModules/clanCore/flake-module.nix b/nixosModules/clanCore/flake-module.nix index da174fff9..868a52375 100644 --- a/nixosModules/clanCore/flake-module.nix +++ b/nixosModules/clanCore/flake-module.nix @@ -3,6 +3,7 @@ imports = [ ./secrets ./zerotier.nix + ./networking.nix inputs.sops-nix.nixosModules.sops # just some example options. Can be removed later ./bloatware diff --git a/nixosModules/clanCore/networking.nix b/nixosModules/clanCore/networking.nix new file mode 100644 index 000000000..813939df3 --- /dev/null +++ b/nixosModules/clanCore/networking.nix @@ -0,0 +1,15 @@ +{ config, lib, ... }: +{ + options.clan.networking = { + deploymentAddress = lib.mkOption { + description = '' + The target SSH node for deployment. + + By default, the node's attribute name will be used. + If set to null, only local deployment will be supported. + ''; + type = lib.types.nullOr lib.types.str; + default = "root@${config.networking.hostName}"; + }; + }; +} diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index 30726b68b..963b0a15c 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -2,10 +2,13 @@ import argparse import json import os import subprocess +from typing import Optional -from ..ssh import Host, HostGroup, HostKeyCheck -from ..secrets.upload import upload_secrets +from ..dirs import get_clan_flake_toplevel +from ..nix import nix_eval from ..secrets.generate import generate_secrets +from ..secrets.upload import upload_secrets +from ..ssh import Host, HostGroup, HostKeyCheck def deploy_nixos(hosts: HostGroup) -> None: @@ -22,7 +25,7 @@ def deploy_nixos(hosts: HostGroup) -> None: ["nix", "flake", "archive", "--to", f"ssh://{target}", "--json"], check=True, stdout=subprocess.PIPE, - extra_env=env + extra_env=env, ) data = json.loads(res.stdout) path = data["path"] @@ -75,20 +78,36 @@ def deploy_nixos(hosts: HostGroup) -> None: # FIXME: we want some kind of inventory here. def update(args: argparse.Namespace) -> None: - meta = {} - if args.flake_uri: - meta["flake_uri"] = args.flake_uri - if args.flake_attr: - meta["flake_attr"] = args.flake_attr - deploy_nixos(HostGroup([Host(args.host, user=args.user, meta=meta)])) + clan_dir = get_clan_flake_toplevel().as_posix() + host = json.loads( + subprocess.run( + nix_eval( + [ + f'{clan_dir}#nixosConfigurations."{args.machine}".config.clan.networking.deploymentAddress' + ] + ), + stdout=subprocess.PIPE, + check=True, + text=True, + ).stdout + ) + parts = host.split("@") + user: Optional[str] = None + if len(parts) > 1: + user = parts[0] + hostname = parts[1] + else: + hostname = parts[0] + maybe_port = hostname.split(":") + port = None + if len(maybe_port) > 1: + hostname = maybe_port[0] + port = int(maybe_port[1]) + print(f"deploying {host}") + deploy_nixos(HostGroup([Host(host=hostname, port=port, user=user)])) def register_update_parser(parser: argparse.ArgumentParser) -> None: - # TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with - - parser.add_argument("--flake-uri", type=str, default=".#", help="nix flake uri") - parser.add_argument( - "--flake-attr", type=str, help="nixos configuration in the flake" - ) - parser.add_argument("--user", type=str, default="root") - parser.add_argument("host", type=str) + parser.add_argument("--target-host", type=str, default="root") + parser.add_argument("machine", type=str) parser.set_defaults(func=update) diff --git a/pkgs/clan-cli/clan_cli/secrets/upload.py b/pkgs/clan-cli/clan_cli/secrets/upload.py index 8dc4afe3b..38a27b96f 100644 --- a/pkgs/clan-cli/clan_cli/secrets/upload.py +++ b/pkgs/clan-cli/clan_cli/secrets/upload.py @@ -1,11 +1,11 @@ import argparse +import json import subprocess -import sys from clan_cli.errors import ClanError from ..dirs import get_clan_flake_toplevel -from ..nix import nix_build +from ..nix import nix_build, nix_eval def upload_secrets(machine: str) -> None: @@ -17,18 +17,28 @@ def upload_secrets(machine: str) -> None: f'{clan_dir}#nixosConfigurations."{machine}".config.system.clan.uploadSecrets' ] ), - capture_output=True, + stdout=subprocess.PIPE, text=True, + check=True, + ) + host = json.loads( + subprocess.run( + nix_eval( + [ + f'{clan_dir}#nixosConfigurations."{machine}".config.clan.networking.deploymentAddress' + ] + ), + stdout=subprocess.PIPE, + text=True, + check=True, + ).stdout ) - if proc.returncode != 0: - print(proc.stderr, file=sys.stderr) - raise ClanError(f"failed to upload secrets:\n{proc.stderr}") secret_upload_script = proc.stdout.strip() secret_upload = subprocess.run( [ secret_upload_script, - f"root@{machine}", + host, ], )