move zerotier secret generation into nixos module
This commit is contained in:
parent
5d9ee64ddc
commit
74a3c85c29
12
flake.lock
12
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"
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
''}"
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
35
nixosModules/clanCore/zerotier/pyproject.toml
Normal file
35
nixosModules/clanCore/zerotier/pyproject.toml
Normal 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
|
||||
)/
|
||||
'''
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
from clan_cli.zerotier import create_network
|
||||
|
||||
|
||||
def test_create_network() -> None:
|
||||
network = create_network()
|
||||
assert network["networkid"]
|
Loading…
Reference in New Issue
Block a user