From 74a3c85c29cdab15239ae0c9bcb966dc97c2cb10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 26 Sep 2023 17:31:45 +0200 Subject: [PATCH] move zerotier secret generation into nixos module --- flake.lock | 12 ++-- flake.nix | 4 +- nixosModules/clanCore/secrets/default.nix | 57 ++++++++++++----- .../clanCore/secrets/password-store.nix | 8 +-- nixosModules/clanCore/secrets/sops.nix | 4 ++ nixosModules/clanCore/zerotier/default.nix | 29 ++++----- .../clanCore/zerotier/generate-network.py | 61 ++++++------------- nixosModules/clanCore/zerotier/pyproject.toml | 35 +++++++++++ pkgs/clan-cli/clan_cli/__init__.py | 5 +- pkgs/clan-cli/clan_cli/nix.py | 19 +----- pkgs/clan-cli/default.nix | 6 -- pkgs/clan-cli/flake-module.nix | 7 +-- .../tests/test_flake_with_core/flake.nix | 12 +--- pkgs/clan-cli/tests/test_secrets_generate.py | 16 +++-- pkgs/clan-cli/tests/test_zerotier.py | 6 -- 15 files changed, 142 insertions(+), 139 deletions(-) rename pkgs/clan-cli/clan_cli/zerotier/__init__.py => nixosModules/clanCore/zerotier/generate-network.py (73%) create mode 100644 nixosModules/clanCore/zerotier/pyproject.toml delete mode 100644 pkgs/clan-cli/tests/test_zerotier.py diff --git a/flake.lock b/flake.lock index 5a49e016..9f9e825a 100644 --- a/flake.lock +++ b/flake.lock @@ -99,16 +99,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1695360818, - "narHash": "sha256-JlkN3R/SSoMTa+CasbxS1gq+GpGxXQlNZRUh9+LIy/0=", - "owner": "NixOS", + "lastModified": 1695741452, + "narHash": "sha256-pDIQmCR0fyb6FKjvURaD6yC5YnE/+rxs5iFQQGgcoNE=", + "owner": "Mic92", "repo": "nixpkgs", - "rev": "e35dcc04a3853da485a396bdd332217d0ac9054f", + "rev": "bc160df717ed1e9defe6044092ea66950976e3ed", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-unstable", + "owner": "Mic92", + "ref": "fakeroot", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 6baad2b0..43c85fa9 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,9 @@ description = "clan.lol base operating system"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + #nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + # https://github.com/NixOS/nixpkgs/pull/257462 + nixpkgs.url = "github:Mic92/nixpkgs/fakeroot"; floco.url = "github:aakropotkin/floco"; floco.inputs.nixpkgs.follows = "nixpkgs"; disko.url = "github:nix-community/disko/party"; diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index ae7fc07d..5d0fe72e 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -9,6 +9,22 @@ one would have to define system.clan.generateSecrets and system.clan.uploadSecrets ''; }; + + options.clanCore.secretsDirectory = lib.mkOption { + type = lib.types.path; + description = '' + The directory where secrets are installed to. This is backend specific. + ''; + }; + + options.clanCore.secretsPrefix = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + Prefix for secrets. This is backend specific. + ''; + }; + options.clanCore.secrets = lib.mkOption { default = { }; type = lib.types.attrsOf @@ -31,22 +47,33 @@ The script is expected to generate all secrets and facts defined in the module. ''; }; - secrets = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule (secret: { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - name of the secret - ''; - default = secret.config._module.args.name; + secrets = + let + config' = config; + in + lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule ({ config, ... }: { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + name of the secret + ''; + default = config._module.args.name; + }; + path = lib.mkOption { + type = lib.types.str; + description = '' + path to a secret which is generated by the generator + ''; + default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}"; + }; }; - }; - })); - description = '' - path where the secret is located in the filesystem - ''; - }; + })); + description = '' + path where the secret is located in the filesystem + ''; + }; facts = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule (fact: { options = { diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix index 3db20e03..9163493a 100644 --- a/nixosModules/clanCore/secrets/password-store.nix +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -3,14 +3,8 @@ let passwordstoreDir = "\${PASSWORD_STORE_DIR:-$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 uploaded to. - ''; - }; config = lib.mkIf (config.clanCore.secretStore == "password-store") { + clanCore.secretsDirectory = passwordstoreDir; system.clan.generateSecrets = pkgs.writeScript "generate-secrets" '' #!/bin/sh set -efu diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index 11140c66..c174450e 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -3,6 +3,7 @@ let secretsDir = config.clanCore.clanDir + "/sops/secrets"; groupsDir = config.clanCore.clanDir + "/sops/groups"; + # My symlink is in the nixos module detected as a directory also it works in the repl. Is this because of pure evaluation? containsSymlink = path: builtins.pathExists path && (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink"); @@ -22,7 +23,10 @@ let in { config = lib.mkIf (config.clanCore.secretStore == "sops") { + clanCore.secretsDirectory = "/run/secrets"; + clanCore.secretsPrefix = config.clanCore.machineName + "-"; system.clan = { + generateSecrets = pkgs.writeScript "generate-secrets" '' #!${pkgs.python3}/bin/python import json diff --git a/nixosModules/clanCore/zerotier/default.nix b/nixosModules/clanCore/zerotier/default.nix index cab8414a..5d293acd 100644 --- a/nixosModules/clanCore/zerotier/default.nix +++ b/nixosModules/clanCore/zerotier/default.nix @@ -45,8 +45,8 @@ in { options.clan.networking.zerotier = { networkId = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; + type = lib.types.str; + default = config.clanCore.secrets.zerotier.facts."zerotier-network-id".value; description = '' zerotier networking id ''; @@ -63,6 +63,11 @@ in }; }; config = lib.mkMerge [ + ({ + # Override license so that we can build zerotierone without + # having to re-import nixpkgs. + services.zerotierone.package = lib.mkDefault (pkgs.zerotierone.overrideAttrs (_old: { meta = { }; })); + }) (lib.mkIf (cfg.networkId != null) { systemd.network.networks.zerotier = { matchConfig.Name = "zt*"; @@ -85,24 +90,20 @@ in # only the controller needs to have the key in the repo, the other clients can be dynamic # we generate the zerotier code manually for the controller, since it's part of the bootstrap command clanCore.secrets.zerotier = { - facts."zerotier.network.id" = { }; - secrets."zerotier.identity.secret" = { }; + facts."zerotier-network-id" = { }; + secrets."zerotier-identity-secret" = { }; generator = '' - TMPDIR=$(mktemp -d) - trap 'rm -rf "$TMPDIR"' EXIT - ${config.clanCore.clanPkgs.clan-cli}/bin/clan zerotier --outpath "$TMPDIR" - cp "$TMPDIR"/network.id "$facts"/zerotier.network.id - cp "$TMPDIR"/identity.secret "$secrets"/zerotier.identity.secret + export PATH=${lib.makeBinPath [ config.services.zerotierone.package pkgs.fakeroot ]} + ${pkgs.python3.interpreter} ${./generate-network.py} "$facts/zerotier-network-id" "$secrets/zerotier-identity-secret" ''; }; systemd.services.zerotierone.serviceConfig.ExecStartPre = [ - "+${pkgs.writeShellScript "init_zerotier" '' - cp /etc/secrets/zerotier.identity.secret /var/lib/zerotier-one/identity.secret - ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON networkConfig)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json - ''}" + "+${pkgs.writeShellScript "init-zerotier" '' + cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret + ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON networkConfig)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json + ''}" ]; }) ]; } - diff --git a/pkgs/clan-cli/clan_cli/zerotier/__init__.py b/nixosModules/clanCore/zerotier/generate-network.py similarity index 73% rename from pkgs/clan-cli/clan_cli/zerotier/__init__.py rename to nixosModules/clanCore/zerotier/generate-network.py index cca9403c..fb2a64fb 100644 --- a/pkgs/clan-cli/clan_cli/zerotier/__init__.py +++ b/nixosModules/clanCore/zerotier/generate-network.py @@ -9,8 +9,9 @@ from pathlib import Path from tempfile import TemporaryDirectory from typing import Any, Iterator, Optional -from ..errors import ClanError -from ..nix import nix_shell, unfree_nix_shell + +class ClanError(Exception): + pass def try_bind_port(port: int) -> bool: @@ -85,37 +86,17 @@ def zerotier_controller() -> Iterator[ZerotierController]: if controller_port is None: raise ClanError("cannot find a free port for zerotier controller") - cmd = unfree_nix_shell( - ["bash", "zerotierone"], ["bash", "-c", "command -v zerotier-one"] - ) - res = subprocess.run( - cmd, - check=True, - text=True, - stdout=subprocess.PIPE, - ) - zerotier_exe = res.stdout.strip() - if zerotier_exe is None: - raise ClanError("cannot find zerotier-one executable") - - if not zerotier_exe.startswith("/nix/store"): - raise ClanError( - f"zerotier-one executable needs to come from /nix/store: {zerotier_exe}" - ) with TemporaryDirectory() as d: tempdir = Path(d) home = tempdir / "zerotier-one" home.mkdir() - cmd = nix_shell( - ["fakeroot"], - [ - "fakeroot", - "--", - zerotier_exe, - f"-p{controller_port}", - str(home), - ], - ) + cmd = [ + "fakeroot", + "--", + "zerotier-one", + f"-p{controller_port}", + str(home), + ] with subprocess.Popen(cmd) as p: try: print( @@ -146,18 +127,16 @@ def create_network() -> dict: } -def main(args: argparse.Namespace) -> None: +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("network_id") + parser.add_argument("identity_secret") + args = parser.parse_args() + zerotier = create_network() - outpath = Path(args.outpath) - outpath.mkdir(parents=True, exist_ok=True) - with open(outpath / "network.id", "w+") as nwid_file: - nwid_file.write(zerotier["networkid"]) - with open(outpath / "identity.secret", "w+") as secret_file: - secret_file.write(zerotier["secret"]) + Path(args.network_id).write_text(zerotier["networkid"]) + Path(args.identity_secret).write_text(zerotier["secret"]) -def register_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "--outpath", help="directory to put the secret file to", required=True - ) - parser.set_defaults(func=main) +if __name__ == "__main__": + main() diff --git a/nixosModules/clanCore/zerotier/pyproject.toml b/nixosModules/clanCore/zerotier/pyproject.toml new file mode 100644 index 00000000..4c77b284 --- /dev/null +++ b/nixosModules/clanCore/zerotier/pyproject.toml @@ -0,0 +1,35 @@ +[tool.mypy] +python_version = "3.10" +warn_redundant_casts = true +disallow_untyped_calls = true +disallow_untyped_defs = true +no_implicit_optional = true +exclude = "clan_cli.nixpkgs" + +[tool.ruff] +line-length = 88 + +select = [ "E", "F", "I", "U", "N"] +ignore = [ "E501" ] + +[tool.black] +line-length = 88 +target-version = [ "py310" ] +include = "\\.pyi?$" +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling +)/ +''' diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 7956c8bd..c4f59f5f 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -3,7 +3,7 @@ import sys from types import ModuleType from typing import Optional -from . import config, create, machines, secrets, webui, zerotier +from . import config, create, machines, secrets, webui from .errors import ClanError from .ssh import cli as ssh_cli @@ -47,9 +47,6 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: parser_webui = subparsers.add_parser("webui", help="start webui") webui.register_parser(parser_webui) - parser_zerotier = subparsers.add_parser("zerotier", help="create zerotier network") - zerotier.register_parser(parser_zerotier) - if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/nix.py b/pkgs/clan-cli/clan_cli/nix.py index c265f632..84fd73da 100644 --- a/pkgs/clan-cli/clan_cli/nix.py +++ b/pkgs/clan-cli/clan_cli/nix.py @@ -4,7 +4,7 @@ import subprocess import tempfile from typing import Any -from .dirs import nixpkgs_flake, nixpkgs_source, unfree_nixpkgs +from .dirs import nixpkgs_flake, nixpkgs_source def nix_command(flags: list[str]) -> list[str]: @@ -82,20 +82,3 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]: + ["-c"] + cmd ) - - -def unfree_nix_shell(packages: list[str], cmd: list[str]) -> list[str]: - if os.environ.get("IN_NIX_SANDBOX"): - return cmd - return ( - nix_command( - [ - "shell", - "-f", - str(unfree_nixpkgs()), - ] - ) - + packages - + ["-c"] - + cmd - ) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 94738a5c..8adff94b 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -16,7 +16,6 @@ , sops , stdenv , wheel -, zerotierone , fakeroot , rsync , ui-assets @@ -49,7 +48,6 @@ let runtimeDependencies = [ bash nix - zerotierone fakeroot openssh sshpass @@ -77,10 +75,6 @@ let ''; nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } '' mkdir $out - mkdir -p $out/unfree - cat > $out/unfree/default.nix < $out/flake.nix << EOF { description = "dependencies for the clan-cli"; diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index 42201021..dbb91c07 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -6,16 +6,11 @@ }; packages = { clan-cli = pkgs.python3.pkgs.callPackage ./default.nix { - inherit (self'.packages) ui-assets zerotierone; + inherit (self'.packages) ui-assets; inherit (inputs) nixpkgs; }; clan-openapi = self'.packages.clan-cli.clan-openapi; default = self'.packages.clan-cli; - - # Override license so that we can build zerotierone without - # having to re-import nixpkgs. - zerotierone = pkgs.zerotierone.overrideAttrs (_old: { meta = { }; }); - ## End optional dependencies }; checks = self'.packages.clan-cli.tests; diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index 43f2d8de..13372350 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -10,19 +10,13 @@ clan = clan-core.lib.buildClan { directory = self; machines = { - vm1 = { modulesPath, ... }: { + vm1 = { modulesPath, lib, ... }: { imports = [ "${toString modulesPath}/virtualisation/qemu-vm.nix" ]; clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; + system.stateVersion = lib.version; - clanCore.secrets.testpassword = { - generator = '' - echo "secret1" > "$secrets/secret1" - echo "fact1" > "$facts/fact1" - ''; - secrets.secret1 = { }; - facts.fact1 = { }; - }; + clan.networking.zerotier.controller.enable = true; }; }; }; diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index d387cccb..142bea1f 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -24,15 +24,19 @@ def test_upload_secret( cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) cli.run(["secrets", "generate", "vm1"]) has_secret("vm1-age.key") - has_secret("vm1-secret1") - fact1 = machine_get_fact("vm1", "fact1") - assert fact1 == "fact1\n" + has_secret("vm1-zerotier-identity-secret") + network_id = machine_get_fact("vm1", "zerotier-network-id") + assert len(network_id) == 16 age_key = sops_secrets_folder().joinpath("vm1-age.key").joinpath("secret") - secret1 = sops_secrets_folder().joinpath("vm1-secret1").joinpath("secret") + identity_secret = ( + sops_secrets_folder() + .joinpath("vm1-zerotier-identity-secret") + .joinpath("secret") + ) age_key_mtime = age_key.lstat().st_mtime_ns - secret1_mtime = secret1.lstat().st_mtime_ns + secret1_mtime = identity_secret.lstat().st_mtime_ns # test idempotency cli.run(["secrets", "generate", "vm1"]) assert age_key.lstat().st_mtime_ns == age_key_mtime - assert secret1.lstat().st_mtime_ns == secret1_mtime + assert identity_secret.lstat().st_mtime_ns == secret1_mtime diff --git a/pkgs/clan-cli/tests/test_zerotier.py b/pkgs/clan-cli/tests/test_zerotier.py deleted file mode 100644 index cf1b2a33..00000000 --- a/pkgs/clan-cli/tests/test_zerotier.py +++ /dev/null @@ -1,6 +0,0 @@ -from clan_cli.zerotier import create_network - - -def test_create_network() -> None: - network = create_network() - assert network["networkid"]