move zerotier secret generation into nixos module
Some checks failed
checks-impure / test (pull_request) Failing after 7s
checks / test (pull_request) Successful in 23s

This commit is contained in:
Jörg Thalheim 2023-09-26 17:31:45 +02:00
parent 5d9ee64ddc
commit 74a3c85c29
15 changed files with 142 additions and 139 deletions

View File

@ -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"
}

View File

@ -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";

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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
''}"
];
})
];
}

View File

@ -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()

View File

@ -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
)/
'''

View File

@ -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)

View File

@ -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
)

View File

@ -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 <<EOF
import "${nixpkgs}" { config = { allowUnfree = true; overlays = []; }; }
EOF
cat > $out/flake.nix << EOF
{
description = "dependencies for the clan-cli";

View File

@ -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;

View File

@ -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;
};
};
};

View File

@ -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

View File

@ -1,6 +0,0 @@
from clan_cli.zerotier import create_network
def test_create_network() -> None:
network = create_network()
assert network["networkid"]