Compare commits

..

1 Commits

Author SHA1 Message Date
pinpox
4e17d9ec0f Revert "vars: retrieve generators for multiple machines"
This reverts commit 501d020562.
2025-09-01 15:00:21 +02:00
480 changed files with 6209 additions and 13919 deletions

View File

@@ -8,6 +8,6 @@ jobs:
runs-on: nix runs-on: nix
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: nix run --print-build-logs .#deploy-docs - run: nix run .#deploy-docs
env: env:
SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }} SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }}

2
.gitignore vendored
View File

@@ -52,5 +52,3 @@ pkgs/clan-app/ui/.fonts
*.gif *.gif
*.mp4 *.mp4
*.mkv *.mkv
.jj

4
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,4 @@
# Contributing to Clan
<!-- Local file: docs/CONTRIBUTING.md -->
Go to the Contributing guide at https://docs.clan.lol/guides/contributing/CONTRIBUTING

View File

@@ -30,7 +30,7 @@ In the Clan ecosystem, security is paramount. Learn how to handle secrets effect
The Clan project thrives on community contributions. We welcome everyone to contribute and collaborate: The Clan project thrives on community contributions. We welcome everyone to contribute and collaborate:
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/guides/contributing/CONTRIBUTING/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->. - **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/contributing/contributing/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
## Join the revolution ## Join the revolution

View File

@@ -0,0 +1,6 @@
{ fetchgit }:
fetchgit {
url = "https://git.clan.lol/clan/clan-core.git";
rev = "5d884cecc2585a29b6a3596681839d081b4de192";
sha256 = "09is1afmncamavb2q88qac37vmsijxzsy1iz1vr6gsyjq2rixaxc";
}

View File

@@ -12,6 +12,7 @@ let
elem elem
filter filter
filterAttrs filterAttrs
flip
genAttrs genAttrs
hasPrefix hasPrefix
pathExists pathExists
@@ -44,7 +45,7 @@ in
flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] ( flake.check = genAttrs [ "x86_64-linux" "aarch64-darwin" ] (
system: system:
let let
checks = filterAttrs ( checks = flip filterAttrs self.checks.${system} (
name: _check: name: _check:
!(hasPrefix "nixos-test-" name) !(hasPrefix "nixos-test-" name)
&& !(hasPrefix "nixos-" name) && !(hasPrefix "nixos-" name)
@@ -56,7 +57,7 @@ in
"clan-core-for-checks" "clan-core-for-checks"
"clan-deps" "clan-deps"
]) ])
) self.checks.${system}; );
in in
inputs.nixpkgs.legacyPackages.${system}.runCommand "fast-flake-checks-${system}" inputs.nixpkgs.legacyPackages.${system}.runCommand "fast-flake-checks-${system}"
{ passthru.checks = checks; } { passthru.checks = checks; }

View File

@@ -13,6 +13,8 @@
fileSystems."/".device = lib.mkDefault "/dev/vda"; fileSystems."/".device = lib.mkDefault "/dev/vda";
boot.loader.grub.device = lib.mkDefault "/dev/vda"; boot.loader.grub.device = lib.mkDefault "/dev/vda";
# We need to use `mkForce` because we inherit from `test-install-machine`
# which currently hardcodes `nixpkgs.hostPlatform`
nixpkgs.hostPlatform = lib.mkForce system; nixpkgs.hostPlatform = lib.mkForce system;
imports = [ self.nixosModules.test-flash-machine ]; imports = [ self.nixosModules.test-flash-machine ];
@@ -26,24 +28,10 @@
{ {
imports = [ self.nixosModules.test-install-machine-without-system ]; imports = [ self.nixosModules.test-install-machine-without-system ];
# We don't want our system to define any `vars` generators as these can't
# be generated as the flake is inside `/nix/store`.
clan.core.settings.state-version.enable = false;
clan.core.vars.generators.test = lib.mkForce { }; clan.core.vars.generators.test = lib.mkForce { };
disko.devices.disk.main.preCreateHook = lib.mkForce ""; disko.devices.disk.main.preCreateHook = lib.mkForce "";
# Every option here should match the options set through `clan flash write`
# if you get a mass rebuild on the disko derivation, this means you need to
# adjust something here. Also make sure that the injected json in clan flash write
# is up to date.
i18n.defaultLocale = "de_DE.UTF-8";
console.keyMap = "de";
services.xserver.xkb.layout = "de";
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target\n"
];
}; };
}; };
perSystem = perSystem =
@@ -56,11 +44,8 @@
dependencies = [ dependencies = [
pkgs.disko pkgs.disko
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
pkgs.glibcLocales
pkgs.kbd.out
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.ConfigIniFiles
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".pkgs.perlPackages.FileSlurp
pkgs.bubblewrap
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel
self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript self.nixosConfigurations."test-flash-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript
@@ -88,7 +73,7 @@
substituters = lib.mkForce [ ]; substituters = lib.mkForce [ ];
hashed-mirrors = null; hashed-mirrors = null;
connect-timeout = lib.mkForce 3; connect-timeout = lib.mkForce 3;
flake-registry = ""; flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}'';
experimental-features = [ experimental-features = [
"nix-command" "nix-command"
"flakes" "flakes"
@@ -97,10 +82,10 @@
}; };
testScript = '' testScript = ''
start_all() start_all()
machine.succeed("echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRWUusawhlIorx7VFeQJHmMkhl9X3QpnvOdhnV/bQNG root@target' > ./test_id_ed25519.pub")
# Some distros like to automount disks with spaces # Some distros like to automount disks with spaces
machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"') machine.succeed('mkdir -p "/mnt/with spaces" && mkfs.ext4 /dev/vdc && mount /dev/vdc "/mnt/with spaces"')
machine.succeed("clan flash write --ssh-pubkey ./test_id_ed25519.pub --keymap de --language de_DE.UTF-8 --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.hostPlatform.system}") machine.succeed("clan flash write --debug --flake ${self.checks.x86_64-linux.clan-core-for-checks} --yes --disk main /dev/vdc test-flash-machine-${pkgs.hostPlatform.system}")
''; '';
} { inherit pkgs self; }; } { inherit pkgs self; };
}; };

View File

@@ -1,8 +1,8 @@
{ {
config,
self, self,
lib, lib,
privateInputs, privateInputs,
... ...
}: }:
{ {
@@ -14,38 +14,31 @@
# you can get a new one by adding # you can get a new one by adding
# client.fail("cat test-flake/machines/test-install-machine/facter.json >&2") # client.fail("cat test-flake/machines/test-install-machine/facter.json >&2")
# to the installation test. # to the installation test.
clan.machines = { clan.machines.test-install-machine-without-system = {
test-install-machine-without-system = { fileSystems."/".device = lib.mkDefault "/dev/vda";
boot.loader.grub.device = lib.mkDefault "/dev/vda";
imports = [ self.nixosModules.test-install-machine-without-system ];
};
clan.machines.test-install-machine-with-system =
{ pkgs, ... }:
{
# https://git.clan.lol/clan/test-fixtures
facter.reportPath = builtins.fetchurl {
url = "https://git.clan.lol/clan/test-fixtures/raw/commit/4a2bc56d886578124b05060d3fb7eddc38c019f8/nixos-vm-facter-json/${pkgs.hostPlatform.system}.json";
sha256 =
{
aarch64-linux = "sha256:1rlfymk03rmfkm2qgrc8l5kj5i20srx79n1y1h4nzlpwaz0j7hh2";
x86_64-linux = "sha256:16myh0ll2gdwsiwkjw5ba4dl23ppwbsanxx214863j7nvzx42pws";
}
.${pkgs.hostPlatform.system};
};
fileSystems."/".device = lib.mkDefault "/dev/vda"; fileSystems."/".device = lib.mkDefault "/dev/vda";
boot.loader.grub.device = lib.mkDefault "/dev/vda"; boot.loader.grub.device = lib.mkDefault "/dev/vda";
imports = [ imports = [ self.nixosModules.test-install-machine-without-system ];
self.nixosModules.test-install-machine-without-system
];
}; };
}
// (lib.listToAttrs (
lib.map (
system:
lib.nameValuePair "test-install-machine-${system}" {
imports = [
self.nixosModules.test-install-machine-without-system
(
if privateInputs ? test-fixtures then
{
facter.reportPath = privateInputs.test-fixtures + /nixos-vm-facter-json/${system}.json;
}
else
{ nixpkgs.hostPlatform = system; }
)
];
fileSystems."/".device = lib.mkDefault "/dev/vda";
boot.loader.grub.device = lib.mkDefault "/dev/vda";
}
) (lib.filter (lib.hasSuffix "linux") config.systems)
));
flake.nixosModules = { flake.nixosModules = {
test-install-machine-without-system = test-install-machine-without-system =
{ lib, modulesPath, ... }: { lib, modulesPath, ... }:
@@ -160,9 +153,9 @@
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = [
privateInputs.clan-core-for-checks privateInputs.clan-core-for-checks
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.toplevel self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.toplevel
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.initialRamdisk self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.initialRamdisk
self.nixosConfigurations."test-install-machine-${pkgs.hostPlatform.system}".config.system.build.diskoScript self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.diskoScript
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.bash.drvPath pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
@@ -215,7 +208,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.x86_64-linux.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )
@@ -226,22 +219,6 @@
"${../assets/ssh/privkey}" "${../assets/ssh/privkey}"
) )
# Run clan install from host using port forwarding
clan_cmd = [
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
"machines",
"init-hardware-config",
"--debug",
"--flake", str(flake_dir),
"--yes", "test-install-machine-without-system",
"--host-key-check", "none",
"--target-host", f"nonrootuser@localhost:{ssh_conn.host_port}",
"-i", ssh_conn.ssh_key,
"--option", "store", os.environ['CLAN_TEST_STORE']
]
subprocess.run(clan_cmd, check=True)
# Run clan install from host using port forwarding # Run clan install from host using port forwarding
clan_cmd = [ clan_cmd = [
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
@@ -296,7 +273,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.x86_64-linux.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )

View File

@@ -147,11 +147,28 @@ let
]; ];
doCheck = false; doCheck = false;
}; };
# Common closure info
closureInfo = pkgs.closureInfo {
rootPaths = [
self.checks.x86_64-linux.clan-core-for-checks
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.toplevel
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.initialRamdisk
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.build.diskoScript
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-install-machine-with-system.config.system.clan.deployment.file
pkgs.stdenv.drvPath
pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir
]
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
};
in in
{ {
inherit inherit
target target
baseTestMachine baseTestMachine
nixosTestLib nixosTestLib
closureInfo
; ;
} }

View File

@@ -29,34 +29,32 @@ nixosLib.runTest (
{ nodes, ... }: { nodes, ... }:
'' ''
import subprocess import subprocess
import tempfile from nixos_test_lib.nix_setup import setup_nix_in_nix # type: ignore[import-untyped]
from nixos_test_lib.nix_setup import setup_nix_in_nix
with tempfile.TemporaryDirectory() as temp_dir: setup_nix_in_nix(None) # No closure info for this test
setup_nix_in_nix(temp_dir, None) # No closure info for this test
start_all() start_all()
admin1.wait_for_unit("multi-user.target") admin1.wait_for_unit("multi-user.target")
peer1.wait_for_unit("multi-user.target") peer1.wait_for_unit("multi-user.target")
# peer1 should have the 'hello' file # peer1 should have the 'hello' file
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}") peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}")
ls_out = peer1.succeed("ls -la ${nodes.peer1.clan.core.vars.generators.new-service.files.a-secret.path}") ls_out = peer1.succeed("ls -la ${nodes.peer1.clan.core.vars.generators.new-service.files.a-secret.path}")
# Check that the file is owned by 'nobody' # Check that the file is owned by 'nobody'
assert "nobody" in ls_out, f"File is not owned by 'nobody': {ls_out}" assert "nobody" in ls_out, f"File is not owned by 'nobody': {ls_out}"
# Check that the file is in the 'users' group # Check that the file is in the 'users' group
assert "users" in ls_out, f"File is not in the 'users' group: {ls_out}" assert "users" in ls_out, f"File is not in the 'users' group: {ls_out}"
# Check that the file is in the '0644' mode # Check that the file is in the '0644' mode
assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}" assert "-rw-r--r--" in ls_out, f"File is not in the '0644' mode: {ls_out}"
# Run clan command # Run clan command
result = subprocess.run( result = subprocess.run(
["${ ["${
clan-core.packages.${hostPkgs.system}.clan-cli clan-core.packages.${hostPkgs.system}.clan-cli
}/bin/clan", "machines", "list", "--flake", "${config.clan.test.flakeForSandbox}"], }/bin/clan", "machines", "list", "--flake", "${config.clan.test.flakeForSandbox}"],
check=True check=True
) )
''; '';
} }
) )

View File

@@ -27,9 +27,7 @@
modules.new-service = { modules.new-service = {
_class = "clan.service"; _class = "clan.service";
manifest.name = "new-service"; manifest.name = "new-service";
roles.peer = { roles.peer = { };
description = "A peer that uses the new-service to generate some files.";
};
perMachine = { perMachine = {
nixosModule = { nixosModule = {
# This should be generated by: # This should be generated by:

View File

@@ -34,9 +34,7 @@ nixosLib.runTest (
modules.new-service = { modules.new-service = {
_class = "clan.service"; _class = "clan.service";
manifest.name = "new-service"; manifest.name = "new-service";
roles.peer = { roles.peer = { };
description = "A peer that uses the new-service to generate some files.";
};
perMachine = { perMachine = {
nixosModule = { nixosModule = {
# This should be generated by: # This should be generated by:

View File

@@ -67,15 +67,6 @@
]; ];
}; };
nix.settings = {
flake-registry = "";
# required for setting the `flake-registry`
experimental-features = [
"nix-command"
"flakes"
];
};
# Define the mounts that exist in the container to prevent them from being stopped # Define the mounts that exist in the container to prevent them from being stopped
fileSystems = { fileSystems = {
"/" = { "/" = {
@@ -115,13 +106,12 @@
let let
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = [
self.packages.${pkgs.hostPlatform.system}.clan-cli self.packages.${pkgs.system}.clan-cli
self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks self.checks.${pkgs.system}.clan-core-for-checks
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.bash.drvPath pkgs.bash.drvPath
pkgs.buildPackages.xorg.lndir pkgs.buildPackages.xorg.lndir
pkgs.bubblewrap
] ]
++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
}; };
@@ -132,7 +122,7 @@
imports = [ self.nixosModules.test-update-machine ]; imports = [ self.nixosModules.test-update-machine ];
}; };
extraPythonPackages = _p: [ extraPythonPackages = _p: [
self.legacyPackages.${pkgs.hostPlatform.system}.nixosTestLib self.legacyPackages.${pkgs.system}.nixosTestLib
]; ];
testScript = '' testScript = ''
@@ -154,7 +144,7 @@
# Prepare test flake and Nix store # Prepare test flake and Nix store
flake_dir = prepare_test_flake( flake_dir = prepare_test_flake(
temp_dir, temp_dir,
"${self.checks.${pkgs.hostPlatform.system}.clan-core-for-checks}", "${self.checks.x86_64-linux.clan-core-for-checks}",
"${closureInfo}" "${closureInfo}"
) )
(flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists (flake_dir / ".clan-flake").write_text("") # Ensure .clan-flake exists
@@ -221,13 +211,12 @@
[ [
"${pkgs.nix}/bin/nix", "${pkgs.nix}/bin/nix",
"copy", "copy",
"--from",
f"{temp_dir}/store",
"--to", "--to",
"ssh://root@192.168.1.1", "ssh://root@192.168.1.1",
"--no-check-sigs", "--no-check-sigs",
f"${self.packages.${pkgs.hostPlatform.system}.clan-cli}", f"${self.packages.${pkgs.system}.clan-cli}",
"--extra-experimental-features", "nix-command flakes", "--extra-experimental-features", "nix-command flakes",
"--from", f"{os.environ["TMPDIR"]}/store"
], ],
check=True, check=True,
env={ env={
@@ -242,7 +231,7 @@
"-o", "UserKnownHostsFile=/dev/null", "-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
f"root@192.168.1.1", f"root@192.168.1.1",
"${self.packages.${pkgs.hostPlatform.system}.clan-cli}/bin/clan", "${self.packages.${pkgs.system}.clan-cli}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",
@@ -270,7 +259,7 @@
# Run clan update command # Run clan update command
subprocess.run([ subprocess.run([
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",
@@ -297,7 +286,7 @@
# Run clan update command with --build-host # Run clan update command with --build-host
subprocess.run([ subprocess.run([
"${self.packages.${pkgs.hostPlatform.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
"machines", "machines",
"update", "update",
"--debug", "--debug",

View File

@@ -1,14 +1,15 @@
{ ... }:
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/admin"; manifest.name = "clan-core/admin";
manifest.description = "Adds a root user with ssh access"; manifest.description = "Convenient Administration for the Clan App";
manifest.categories = [ "Utility" ]; manifest.categories = [ "Utility" ];
roles.default = { roles.default = {
description = "Placeholder role to apply the admin service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
options = { options = {
allowedKeys = lib.mkOption { allowedKeys = lib.mkOption {
default = { }; default = { };

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -5,11 +5,11 @@ inventory.instances = {
borgbackup = { borgbackup = {
module = { module = {
name = "borgbackup"; name = "borgbackup";
input = "clan-core"; input = "clan";
}; };
roles.client.machines."jon".settings = { roles.client.machines."jon".settings = {
destinations."storagebox" = { destinations."storagebox" = {
repo = "username@hostname:/./borgbackup"; repo = "username@$hostname:/./borgbackup";
rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh''; rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh'';
}; };
}; };

View File

@@ -9,7 +9,7 @@
# TODO: a client can only be in one instance, add constraint # TODO: a client can only be in one instance, add constraint
roles.server = { roles.server = {
description = "A borgbackup server that stores the backups of clients.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -54,7 +54,7 @@
authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machineName)) ]; authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machineName)) ];
# }; # };
# }) machinesWithKey; # }) machinesWithKey;
}) (roles.client.machines or { }); }) roles.client.machines;
in in
hosts; hosts;
}; };
@@ -62,7 +62,6 @@
}; };
roles.client = { roles.client = {
description = "A borgbackup client that backs up to all borgbackup server roles.";
interface = interface =
{ {
lib, lib,
@@ -188,7 +187,7 @@
config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes"; } -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes";
}; };
}) (builtins.attrNames (roles.server.machines or { })); }) (builtins.attrNames roles.server.machines);
in in
(builtins.listToAttrs destinations); (builtins.listToAttrs destinations);

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -1,32 +0,0 @@
This service sets up a certificate authority (CA) that can issue certificates to
other machines in your clan. For this the `ca` role is used.
It additionally provides a `default` role, that can be applied to all machines
in your clan and will make sure they trust your CA.
## Example Usage
The following configuration would add a CA for the top level domain `.foo`. If
the machine `server` now hosts a webservice at `https://something.foo`, it will
get a certificate from `ca` which is valid inside your clan. The machine
`client` will trust this certificate if it makes a request to
`https://something.foo`.
This clan service can be combined with the `coredns` service for easy to deploy,
SSL secured clan-internal service hosting.
```nix
inventory = {
machines.ca = { };
machines.client = { };
machines.server = { };
instances."certificates" = {
module.name = "certificates";
module.input = "self";
roles.ca.machines.ca.settings.tlds = [ "foo" ];
roles.default.machines.client = { };
roles.default.machines.server = { };
};
};
```

View File

@@ -1,246 +0,0 @@
{ ... }:
{
_class = "clan.service";
manifest.name = "certificates";
manifest.description = "Sets up a PKI certificate chain using step-ca";
manifest.categories = [ "Network" ];
manifest.readme = builtins.readFile ./README.md;
roles.ca = {
description = "A certificate authority that issues and signs certificates for other machines.";
interface =
{ lib, ... }:
{
options.acmeEmail = lib.mkOption {
type = lib.types.str;
default = "none@none.tld";
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
options.tlds = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Top level domain for this CA. Certificates will be issued and trusted for *.<tld>";
};
options.expire = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "When the certificate should expire.";
default = "8760h";
example = "8760h";
};
};
perInstance =
{ settings, ... }:
{
nixosModule =
{
config,
pkgs,
lib,
...
}:
let
domains = map (tld: "ca.${tld}") settings.tlds;
in
{
security.acme.defaults.email = settings.acmeEmail;
security.acme = {
certs = builtins.listToAttrs (
map (domain: {
name = domain;
value = {
server = "https://${domain}:1443/acme/acme/directory";
};
}) domains
);
};
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts = builtins.listToAttrs (
map (domain: {
name = domain;
value = {
addSSL = true;
enableACME = true;
locations."/".proxyPass = "https://localhost:1443";
locations."= /ca.crt".alias =
config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path;
};
}) domains
);
};
clan.core.vars.generators = {
# Intermediate key generator
"step-intermediate-key" = {
files."intermediate.key" = {
secret = true;
deploy = true;
owner = "step-ca";
group = "step-ca";
};
runtimeInputs = [ pkgs.step-cli ];
script = ''
step crypto keypair --kty EC --curve P-256 --no-password --insecure $out/intermediate.pub $out/intermediate.key
'';
};
# Intermediate certificate generator
"step-intermediate-cert" = {
files."intermediate.crt".secret = false;
dependencies = [
"step-ca"
"step-intermediate-key"
];
runtimeInputs = [ pkgs.step-cli ];
script = ''
# Create intermediate certificate
step certificate create \
--ca $in/step-ca/ca.crt \
--ca-key $in/step-ca/ca.key \
--ca-password-file /dev/null \
--key $in/step-intermediate-key/intermediate.key \
--template ${pkgs.writeText "intermediate.tmpl" ''
{
"subject": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 0
},
"nameConstraints": {
"critical": true,
"permittedDNSDomains": [${
(lib.strings.concatStringsSep "," (map (tld: ''"${tld}"'') settings.tlds))
}]
}
}
''} ${lib.optionalString (settings.expire != null) "--not-after ${settings.expire}"} \
--not-before=-12h \
--no-password --insecure \
"Clan Intermediate CA" \
$out/intermediate.crt
'';
};
};
services.step-ca = {
enable = true;
intermediatePasswordFile = "/dev/null";
address = "0.0.0.0";
port = 1443;
settings = {
root = config.clan.core.vars.generators.step-ca.files."ca.crt".path;
crt = config.clan.core.vars.generators.step-intermediate-cert.files."intermediate.crt".path;
key = config.clan.core.vars.generators.step-intermediate-key.files."intermediate.key".path;
dnsNames = domains;
logger.format = "text";
db = {
type = "badger";
dataSource = "/var/lib/step-ca/db";
};
authority = {
provisioners = [
{
type = "ACME";
name = "acme";
forceCN = true;
}
];
claims = {
maxTLSCertDuration = "2160h";
defaultTLSCertDuration = "2160h";
};
backdate = "1m0s";
};
tls = {
cipherSuites = [
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
];
minVersion = 1.2;
maxVersion = 1.3;
renegotiation = false;
};
};
};
};
};
};
# Empty role, so we can add non-ca machins to the instance to trust the CA
roles.default = {
description = "A machine that trusts the CA and can get certificates issued by it.";
interface =
{ lib, ... }:
{
options.acmeEmail = lib.mkOption {
type = lib.types.str;
default = "none@none.tld";
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
};
perInstance =
{ settings, ... }:
{
nixosModule.security.acme.defaults.email = settings.acmeEmail;
};
};
# All machines (independent of role) will trust the CA
perMachine.nixosModule =
{ pkgs, config, ... }:
{
# Root CA generator
clan.core.vars.generators = {
"step-ca" = {
share = true;
files."ca.key" = {
secret = true;
deploy = false;
};
files."ca.crt".secret = false;
runtimeInputs = [ pkgs.step-cli ];
script = ''
step certificate create --template ${pkgs.writeText "root.tmpl" ''
{
"subject": {{ toJson .Subject }},
"issuer": {{ toJson .Subject }},
"keyUsage": ["certSign", "crlSign"],
"basicConstraints": {
"isCA": true,
"maxPathLen": 1
}
}
''} "Clan Root CA" $out/ca.crt $out/ca.key \
--kty EC --curve P-256 \
--not-after=8760h \
--not-before=-12h \
--no-password --insecure
'';
};
};
security.pki.certificateFiles = [ config.clan.core.vars.generators."step-ca".files."ca.crt".path ];
environment.systemPackages = [ pkgs.openssl ];
security.acme.acceptTerms = true;
};
}

View File

@@ -1,17 +0,0 @@
{
...
}:
let
module = ./default.nix;
in
{
clan.modules.certificates = module;
perSystem =
{ ... }:
{
clan.nixosTests.certificates = {
imports = [ ./tests/vm/default.nix ];
clan.modules.certificates = module;
};
};
}

View File

@@ -1,84 +0,0 @@
{
name = "certificates";
clan = {
directory = ./.;
inventory = {
machines.ca = { }; # 192.168.1.1
machines.client = { }; # 192.168.1.2
machines.server = { }; # 192.168.1.3
instances."certificates" = {
module.name = "certificates";
module.input = "self";
roles.ca.machines.ca.settings.tlds = [ "foo" ];
roles.default.machines.client = { };
roles.default.machines.server = { };
};
};
};
nodes =
let
hostConfig = ''
192.168.1.1 ca.foo
192.168.1.3 test.foo
'';
in
{
client.networking.extraHosts = hostConfig;
ca.networking.extraHosts = hostConfig;
server = {
networking.extraHosts = hostConfig;
# TODO: Could this be set automatically?
# I would like to get this information from the coredns module, but we
# cannot model dependencies yet
security.acme.certs."test.foo".server = "https://ca.foo/acme/acme/directory";
# Host a simple service on 'server', with SSL provided via our CA. 'client'
# should be able to curl it via https and accept the certificates
# presented
networking.firewall.allowedTCPPorts = [
80
443
];
services.nginx = {
enable = true;
virtualHosts."test.foo" = {
enableACME = true;
forceSSL = true;
locations."/" = {
return = "200 'test server response'";
extraConfig = "add_header Content-Type text/plain;";
};
};
};
};
};
testScript = ''
start_all()
import time
time.sleep(3)
ca.succeed("systemctl restart acme-order-renew-ca.foo.service ")
time.sleep(3)
server.succeed("systemctl restart acme-test.foo.service")
# It takes a while for the correct certs to appear (before that self-signed
# are presented by nginx) so we wait for a bit.
client.wait_until_succeeds("curl -v https://test.foo")
# Show certificate information for debugging
client.succeed("openssl s_client -connect test.foo:443 -servername test.foo </dev/null 2>/dev/null | openssl x509 -text -noout 1>&2")
'';
}

View File

@@ -1,6 +0,0 @@
[
{
"publickey": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43",
"type": "age"
}
]

View File

@@ -1,6 +0,0 @@
[
{
"publickey": "age1js225d8jc507sgcg0fdfv2x3xv3asm4ds5c6s4hp37nq8spxu95sc5x3ce",
"type": "age"
}
]

View File

@@ -1,6 +0,0 @@
[
{
"publickey": "age1nwuh8lc604mnz5r8ku8zswyswnwv02excw237c0cmtlejp7xfp8sdrcwfa",
"type": "age"
}
]

View File

@@ -1,15 +0,0 @@
{
"data": "ENC[AES256_GCM,data:6+XilULKRuWtAZ6B8Lj9UqCfi1T6dmqrDqBNXqS4SvBwM1bIWiL6juaT1Q7ByOexzID7tY740gmQBqTey54uLydh8mW0m4ZtUqw=,iv:9kscsrMPBGkutTnxrc5nrc7tQXpzLxw+929pUDKqTu0=,tag:753uIjm8ZRs0xsjiejEY8g==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1d3kycldZRXhmR0FqTXJp\nWWU0MDBYNmxxbFE5M2xKYm5KWnQ0MXBHNEM4CjN4RFFVcFlkd3pjTFVDQ3Vackdj\nVTVhMWoxdFpsWHp5S1p4L05kYk5LUkkKLS0tIENtZFZZTjY2amFVQmZLZFplQzBC\nZm1vWFI4MXR1ZHIxTTQ5VXdSYUhvOTQKte0bKjXQ0xA8FrpuChjDUvjVqp97D8kT\n3tVh6scdjxW48VSBZP1GRmqcMqCdj75GvJTbWeNEV4PDBW7GI0UW+Q==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-02T08:42:39Z",
"mac": "ENC[AES256_GCM,data:AftMorrH7qX5ctVu5evYHn5h9pC4Mmm2VYaAV8Hy0PKTc777jNsL6DrxFVV3NVqtecpwrzZFWKgzukcdcRJe4veVeBrusmoZYtifH0AWZTEVpVlr2UXYYxCDmNZt1WHfVUo40bT//X6QM0ye6a/2Y1jYPbMbryQNcGmnpk9PDvU=,iv:5nk+d8hzA05LQp7ZHRbIgiENg2Ha6J6YzyducM6zcNU=,tag:dy1hqWVzMu/+fSK57h9ZCA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../users/admin

View File

@@ -1,15 +0,0 @@
{
"data": "ENC[AES256_GCM,data:jdTuGQUYvT1yXei1RHKsOCsABmMlkcLuziHDVhA7NequZeNu0fSbrJTXQDCHsDGhlYRcjU5EsEDT750xdleXuD3Gs9zWvPVobI4=,iv:YVow3K1j6fzRF9bRfIEpuOkO/nRpku/UQxWNGC+UJQQ=,tag:cNLM5R7uu6QpwPB9K6MYzg==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvOVF2WXRSL0NpQzFZR01I\nNU85TGcyQmVDazN1dmpuRFVTZEg5NDRKTGhrCk1IVjFSU1V6WHBVRnFWcHkyVERr\nTjFKbW1mQ2FWOWhjN2VPamMxVEQ5VkkKLS0tIENVUGlhanhuWGtDKzBzRmk2dE4v\nMXZBRXNMa3IrOTZTNHRUWVE3UXEwSWMK2cBLoL/H/Vxd/klVrqVLdX9Mww5j7gw/\nEWc5/hN+km6XoW+DiJxVG4qaJ7qqld6u5ZnKgJT+2h9CfjA04I2akg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-02T08:42:51Z",
"mac": "ENC[AES256_GCM,data:zOBQVM2Ydu4v0+Fw3p3cEU+5+7eKaadV0tKro1JVOxclG1Vs6Myq57nw2eWf5JxIl0ulL+FavPKY26qOQ3aqcGOT3PMRlCda9z+0oSn9Im9bE/DzAGmoH/bp76kFkgTTOCZTMUoqJ+UJqv0qy1BH/92sSSKmYshEX6d1vr5ISrw=,iv:i9ZW4sLxOCan4UokHlySVr1CW39nCTusG4DmEPj/gIw=,tag:iZBDPHDkE3Vt5mFcFu1TPQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../users/admin

View File

@@ -1,15 +0,0 @@
{
"data": "ENC[AES256_GCM,data:5CJuHcxJMXZJ8GqAeG3BrbWtT1kade4kxgJsn1cRpmr1UgN0ZVYnluPEiBscClNSOzcc6vcrBpfTI3dj1tASKTLP58M+GDBFQDo=,iv:gsK7XqBGkYCoqAvyFlIXuJ27PKSbTmy7f6cgTmT2gow=,tag:qG5KejkBvy9ytfhGXa/Mnw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxbzVqYkplTzJKN1pwS3VM\naFFIK2VsR3lYUVExYW9ieERBL0tlcFZtVzJRCkpiLzdmWmFlOUZ5QUJ4WkhXZ2tQ\nZm92YXBCV0RpYnIydUdEVTRiamI4bjAKLS0tIG93a2htS1hFcjBOeVFnNCtQTHVr\na2FPYjVGbWtORjJVWXE5bndPU1RWcXMKikMEB7X+kb7OtiyqXn3HRpLYkCdoayDh\n7cjGnplk17q25/lRNHM4JVS5isFfuftCl01enESqkvgq+cwuFwa9DQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-02T08:42:59Z",
"mac": "ENC[AES256_GCM,data:xybV2D0xukZnH2OwRpIugPnS7LN9AbgGKwFioPJc1FQWx9TxMUVDwgMN6V5WrhWkXgF2zP4krtDYpEz4Vq+LbOjcnTUteuCc+7pMHubuRuip7j+M32MH1kuf4bVZuXbCfvm7brGxe83FzjoioLqzA8g/X6Q1q7/ErkNeFjluC3Q=,iv:QEW3EUKSRZY3fbXlP7z+SffWkQeXwMAa5K8RQW7NvPE=,tag:DhFxY7xr7H1Wbd527swD0Q==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../users/admin

View File

@@ -1,4 +0,0 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -1,12 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBsDCCAVegAwIBAgIQbT1Ivm+uwyf0HNkJfan2BTAKBggqhkjOPQQDAjAXMRUw
EwYDVQQDEwxDbGFuIFJvb3QgQ0EwHhcNMjUwOTAxMjA0MzAzWhcNMjYwOTAyMDg0
MzAzWjAfMR0wGwYDVQQDExRDbGFuIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABDXCNrUIotju9P1U6JxLV43sOxLlRphQJS4dM+lvjTZc
aQ+HwQg0AHVlQNRwS3JqKrJJtJVyKbZklh6eFaDPoj6jfTB7MA4GA1UdDwEB/wQE
AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRKHaccHgP2ccSWVBWN
zGoDdTg7aTAfBgNVHSMEGDAWgBSfsnz4phMJx9su/kgeF/FbZQCBgzAVBgNVHR4B
Af8ECzAJoAcwBYIDZm9vMAoGCCqGSM49BAMCA0cAMEQCICiUDk1zGNzpS/iVKLfW
zUGaCagpn2mCx4xAXQM9UranAiAn68nVYGWjkzhU31wyCAupxOjw7Bt96XXqIAz9
hLLtMA==
-----END CERTIFICATE-----

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:Auonh9fa7jSkld1Zyxw74x5ydj6Xc+0SOgiqumVETNCfner9K96Rmv1PkREuHNGWPsnzyEM3pRT8ijvu3QoKvy9QPCCewyT07Wqe4G74+bk1iMeAHsV3To6kHs6M8OISvE+CmG0+hlLmdfRSabTzyWPLHbOjvFTEEuA5G7xiryacSYOE++eeEHdn+oUDh/IMTcfLjCGMjsXFikx1Hb+ofeRTlCg47+0w4MXVvQkOzQB5V2C694jZXvZ19jd/ioqr8YASz2xatGvqwW6cpZxqOWyZJ0UAj/6yFk6tZWifqVB3wgU=,iv:ITFCrDkeWl4GWCebVq15ei9QmkOLDwUIYojKZ2TU6JU=,tag:8k4iYbCIusUykY79H86WUQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsT25UbjJTQ2tzbnQyUm9p\neWx1UlZIeVpocnBqUCt0YnFlN2FOU25Lb0hNCmdXUUsyalRTbHRRQ0NLSGc1YllV\nUXRwaENhaXU1WmdnVDE0UWprUUUyeDAKLS0tIHV3dHU3aG5JclM0V3FadzN0SU14\ndFptbEJUNXQ4QVlqbkJ1TjAvdDQwSGsKcKPWUjhK7wzIpdIdksMShF2fpLdDTUBS\nZiU7P1T+3psxad9qhapvU0JrAY+9veFaYVEHha2aN/XKs8HqUcTp3A==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1yd2cden7jav8x4nzx2fwze2fsa5j0qm2m3t7zum765z3u4gj433q7dqj43",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjZFVteVZwVGVmRE9NT3hG\nNGMyS3FSaXluM1FpeUp6SDVMUEpwYzg5SmdvCkRPU0QyU1JicGNkdlMyQWVkT0k3\nL2YrbDhWeGk4WFhxcUFmTmhZQ0pEQncKLS0tIG85Ui9rKzBJQ2VkMFBUQTMvSTlu\nbm8rZ09Wa24rQkNvTTNtYTZBN3MrZlkK7cjNhlUKZdOrRq/nKUsbUQgNTzX8jO+0\nzADpz6WCMvsJ15xazc10BGh03OtdMWl5tcoWMaZ71HWtI9Gip5DH0w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-02T08:42:42Z",
"mac": "ENC[AES256_GCM,data:9xlO5Yis8DG/y8GjvP63NltD4xEL7zqdHL2cQE8gAoh/ZamAmK5ZL0ld80mB3eIYEPKZYvmUYI4Lkrge2ZdqyDoubrW+eJ3dxn9+StxA9FzXYwUE0t+bbsNJfOOp/kDojf060qLGsu0kAGKd2ca4WiDccR0Cieky335C7Zzhi/Q=,iv:bWQ4wr0CJHSN+6ipUbkYTDWZJyFQjDKszfpVX9EEUsY=,tag:kADIFgJBEGCvr5fPbbdEDA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1,10 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBcTCCARigAwIBAgIRAIix99+AE7Y+uyiLGaRHEhUwCgYIKoZIzj0EAwIwFzEV
MBMGA1UEAxMMQ2xhbiBSb290IENBMB4XDTI1MDkwMTIwNDI1N1oXDTI2MDkwMjA4
NDI1N1owFzEVMBMGA1UEAxMMQ2xhbiBSb290IENBMFkwEwYHKoZIzj0CAQYIKoZI
zj0DAQcDQgAEk7nn9kzxI+xkRmNMlxD+7T78UqV3aqus0foJh6uu1CHC+XaebMcw
JN95nAe3oYA3yZG6Mnq9nCxsYha4EhzGYqNFMEMwDgYDVR0PAQH/BAQDAgEGMBIG
A1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFJ+yfPimEwnH2y7+SB4X8VtlAIGD
MAoGCCqGSM49BAMCA0cAMEQCIBId/CcbT5MPFL90xa+XQz+gVTdRwsu6Bg7ehMso
Bj0oAiBjSlttd5yeuZGXBm+O0Gl+WdKV60QlrWutNewXFS4UpQ==
-----END CERTIFICATE-----

View File

@@ -1,15 +0,0 @@
{
"data": "ENC[AES256_GCM,data:PnEXteU3I7U0OKgE+oR3xjHdLWYTpJjM/jlzxtGU0uP2pUBuQv3LxtEz+cP0ZsafHLNq2iNJ7xpUEE0g4d3M296S56oSocK3fREWBiJFiaC7SAEUiil1l3UCwHn7LzmdEmn8Kq7T+FK89wwqtVWIASLo2gZC/yHE5eEanEATTchGLSNiHJRzZ8n0Ekm8EFUA6czOqA5nPQHaSmeLzu1g80lSSi1ICly6dJksa6DVucwOyVFYFEeq8Dfyc1eyP8L1ee0D7QFYBMduYOXTKPtNnyDmdaQMj7cMMvE7fn04idIiAqw=,iv:nvLmAfFk2GXnnUy+Afr648R60Ou13eu9UKykkiA8Y+4=,tag:lTTAxfG0EDCU6u7xlW6xSQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMjNWUm5NbktQeTRWRjJE\nWWFZc2Rsa3I5aitPSno1WnhORENNcng5OHprCjNUQVhBVHFBcWFjaW5UdmxKTnZw\nQlI4MDk5Wkp0RElCeWgzZ2dFQkF2dkkKLS0tIDVreTkydnJ0RDdHSHlQeVV6bGlP\nTmpJOVBSb2dkVS9TZG5SRmFjdnQ1b3cKQ5XvwH1jD4XPVs5RzOotBDq8kiE6S5k2\nDBv6ugjsM5qV7/oGP9H69aSB4jKPZjEn3yiNw++Oorc8uXd5kSGh7w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-02T08:43:00Z",
"mac": "ENC[AES256_GCM,data:3jFf66UyZUWEtPdPu809LCS3K/Hc6zbnluystl3eXS+KGI+dCoYmN9hQruRNBRxf6jli2RIlArmmEPBDQVt67gG/qugTdT12krWnYAZ78iocmOnkf44fWxn/pqVnn4JYpjEYRgy8ueGDnUkwvpGWVZpcXw5659YeDQuYOJ2mq0U=,iv:3k7fBPrABdLItQ2Z+Mx8Nx0eIEKo93zG/23K+Q5Hl3I=,tag:aehAObdx//DEjbKlOeM7iQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../sops/users/admin

View File

@@ -8,7 +8,7 @@ The service consists of two roles:
- A `server` role: This is the DNS-server that will be queried when trying to - A `server` role: This is the DNS-server that will be queried when trying to
resolve clan-internal services. It defines the top-level domain. resolve clan-internal services. It defines the top-level domain.
- A `default` role: This does two things. First, it sets up the nameservers so - A `default` role: This does two things. First, it sets up the nameservers so
that clan-internal queries are resolved via the `server` machine, while thatclan-internal queries are resolved via the `server` machine, while
external queries are resolved as normal via DHCP. Second, it allows exposing external queries are resolved as normal via DHCP. Second, it allows exposing
services (see example below). services (see example below).
@@ -45,15 +45,13 @@ inventory = {
# Add the default role to all machines, including `client` # Add the default role to all machines, including `client`
roles.default.tags.all = { }; roles.default.tags.all = { };
# DNS server queries to http://<name>.foo are resolved here # DNS server
roles.server.machines."dnsserver".settings = { roles.server.machines."dnsserver".settings = {
ip = "192.168.1.2"; ip = "192.168.1.2";
tld = "foo"; tld = "foo";
}; };
# First service # First service
# Registers http://one.foo will resolve to 192.168.1.3
# underlying service runs on server01
roles.default.machines."server01".settings = { roles.default.machines."server01".settings = {
ip = "192.168.1.3"; ip = "192.168.1.3";
services = [ "one" ]; services = [ "one" ];

View File

@@ -1,5 +1,4 @@
{ ... }: { ... }:
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "coredns"; manifest.name = "coredns";
@@ -8,7 +7,7 @@
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.server = { roles.server = {
description = "A DNS server that resolves services in the clan network.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -26,12 +25,6 @@
# TODO: Set a default # TODO: Set a default
description = "IP for the DNS to listen on"; description = "IP for the DNS to listen on";
}; };
options.dnsPort = lib.mkOption {
type = lib.types.int;
default = 1053;
description = "Port of the clan-internal DNS server";
};
}; };
perInstance = perInstance =
@@ -49,8 +42,8 @@
}: }:
{ {
networking.firewall.allowedTCPPorts = [ settings.dnsPort ]; networking.firewall.allowedTCPPorts = [ 53 ];
networking.firewall.allowedUDPPorts = [ settings.dnsPort ]; networking.firewall.allowedUDPPorts = [ 53 ];
services.coredns = services.coredns =
let let
@@ -81,29 +74,22 @@
in in
{ {
enable = true; enable = true;
config = config = ''
. {
forward . 1.1.1.1
cache 30
}
let ${settings.tld} {
dnsPort = builtins.toString settings.dnsPort; file ${zonefile}
in }
'';
''
.:${dnsPort} {
forward . 1.1.1.1
cache 30
}
${settings.tld}:${dnsPort} {
file ${zonefile}
}
'';
}; };
}; };
}; };
}; };
roles.default = { roles.default = {
description = "A machine that registers the 'server' role as resolver and registers services under the configured TLD in the resolver.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -121,16 +107,10 @@
# TODO: Set a default # TODO: Set a default
description = "IP on which the services will listen"; description = "IP on which the services will listen";
}; };
options.dnsPort = lib.mkOption {
type = lib.types.int;
default = 1053;
description = "Port of the clan-internal DNS server";
};
}; };
perInstance = perInstance =
{ roles, settings, ... }: { roles, ... }:
{ {
nixosModule = nixosModule =
{ lib, ... }: { lib, ... }:
@@ -167,7 +147,7 @@
]; ];
stub-zone = map (m: { stub-zone = map (m: {
name = "${roles.server.machines.${m}.settings.tld}."; name = "${roles.server.machines.${m}.settings.tld}.";
stub-addr = "${roles.server.machines.${m}.settings.ip}@${builtins.toString settings.dnsPort}"; stub-addr = "${roles.server.machines.${m}.settings.ip}";
}) (lib.attrNames roles.server.machines); }) (lib.attrNames roles.server.machines);
}; };
}; };

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -95,15 +95,18 @@
for m in machines: for m in machines:
m.wait_for_unit("network-online.target") m.wait_for_unit("network-online.target")
# import time
# time.sleep(2333333)
# This should work, but is borken in tests i think? Instead we dig directly # This should work, but is borken in tests i think? Instead we dig directly
# client.succeed("curl -k -v http://one.foo") # client.succeed("curl -k -v http://one.foo")
# client.succeed("curl -k -v http://two.foo") # client.succeed("curl -k -v http://two.foo")
answer = client.succeed("dig @192.168.1.2 -p 1053 one.foo") answer = client.succeed("dig @192.168.1.2 one.foo")
assert "192.168.1.3" in answer, "IP not found" assert "192.168.1.3" in answer, "IP not found"
answer = client.succeed("dig @192.168.1.2 -p 1053 two.foo") answer = client.succeed("dig @192.168.1.2 two.foo")
assert "192.168.1.4" in answer, "IP not found" assert "192.168.1.4" in answer, "IP not found"
''; '';

View File

@@ -101,7 +101,6 @@ in
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.admin = { roles.admin = {
description = "A data-mesher admin node that bootstraps the network and can sign new nodes into the network.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -178,7 +177,6 @@ in
}; };
roles.signer = { roles.signer = {
description = "A data-mesher signer node that can sign new nodes into the network.";
interface = sharedInterface; interface = sharedInterface;
perInstance = perInstance =
{ {
@@ -210,7 +208,6 @@ in
}; };
roles.peer = { roles.peer = {
description = "A data-mesher peer node that connects to the network.";
interface = sharedInterface; interface = sharedInterface;
perInstance = perInstance =
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -2,12 +2,11 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/dyndns"; manifest.name = "clan-core/dyndns";
manifest.description = "A dynamic DNS service to auto update domain IPs"; manifest.description = "A dynamic DNS service to update domain IPs";
manifest.categories = [ "Network" ]; manifest.categories = [ "Network" ];
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.default = { roles.default = {
description = "Placeholder role to apply the dyndns service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -2,34 +2,31 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/emergency-access"; manifest.name = "clan-core/emergency-access";
manifest.description = "Set recovery password for emergency access to machine to debug boot issues"; manifest.description = "Set recovery password for emergency access to machine";
manifest.categories = [ "System" ]; manifest.categories = [ "System" ];
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.default = { roles.default.perInstance = {
description = "Placeholder role to apply the emergency-access service"; nixosModule =
perInstance = { { config, pkgs, ... }:
nixosModule = {
{ config, pkgs, ... }: boot.initrd.systemd.emergencyAccess =
{ config.clan.core.vars.generators.emergency-access.files.password-hash.value;
boot.initrd.systemd.emergencyAccess =
config.clan.core.vars.generators.emergency-access.files.password-hash.value;
clan.core.vars.generators.emergency-access = { clan.core.vars.generators.emergency-access = {
runtimeInputs = [ runtimeInputs = [
pkgs.coreutils pkgs.coreutils
pkgs.mkpasswd pkgs.mkpasswd
pkgs.xkcdpass pkgs.xkcdpass
]; ];
files.password.deploy = false; files.password.deploy = false;
files.password-hash.secret = false; files.password-hash.secret = false;
script = '' script = ''
xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > $out/password xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > $out/password
mkpasswd -s -m sha-512 < $out/password | tr -d "\n" > $out/password-hash mkpasswd -s -m sha-512 < $out/password | tr -d "\n" > $out/password-hash
''; '';
};
}; };
}; };
}; };
} }

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
{ {
clan.modules = { clan.modules = {
emergency-access = ./default.nix; emergency-access = lib.modules.importApply ./default.nix { };
}; };
} }

View File

@@ -6,7 +6,7 @@
manifest.categories = [ "System" ]; manifest.categories = [ "System" ];
roles.default = { roles.default = {
description = "Placeholder role to apply the garage service";
perInstance.nixosModule = perInstance.nixosModule =
{ {
config, config,

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -4,6 +4,7 @@
# The test for this module in ./tests/vm/default.nix shows an example of how # The test for this module in ./tests/vm/default.nix shows an example of how
# the service is used. # the service is used.
{ packages }:
{ ... }: { ... }:
{ {
_class = "clan.service"; _class = "clan.service";
@@ -14,7 +15,6 @@
# defined in this file directly (e.g. the "morning" role) or split up into a # defined in this file directly (e.g. the "morning" role) or split up into a
# separate file (e.g. the "evening" role) # separate file (e.g. the "evening" role)
roles.morning = { roles.morning = {
description = "A morning greeting machine";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -34,17 +34,20 @@
settings, settings,
# The name of this instance of the service # The name of this instance of the service
instanceName,
# The current machine # The current machine
machine,
# All roles of this service, with their assigned machines # All roles of this service, with their assigned machines
roles,
... ...
}: }:
{ {
# Analog to 'perSystem' of flake-parts. # Analog to 'perSystem' of flake-parts.
# For every instance of this service we will add a nixosModule to a morning-machine # For every instance of this service we will add a nixosModule to a morning-machine
nixosModule = nixosModule =
{ ... }: { config, ... }:
{ {
# Interaction examples what you could do here: # Interaction examples what you could do here:
# - Get some settings of this machine # - Get some settings of this machine
@@ -68,7 +71,6 @@
# the interface here, so we can see all settings of the service in one place, # the interface here, so we can see all settings of the service in one place,
# but you can also move it to the respective file # but you can also move it to the respective file
roles.evening = { roles.evening = {
description = "An evening greeting machine";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -5,7 +5,9 @@
... ...
}: }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -6,7 +6,5 @@
manifest.categories = [ "Utility" ]; manifest.categories = [ "Utility" ];
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.default = { roles.default = { };
description = "Placeholder role to apply the importer service";
};
} }

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
{ {
clan.modules = { clan.modules = {
importer = ./default.nix; importer = lib.modules.importApply ./default.nix { };
}; };
} }

View File

@@ -2,13 +2,12 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/internet"; manifest.name = "clan-core/internet";
manifest.description = "Part of the clan networking abstraction to define how to reach machines from outside the clan network over the internet, if defined has the highest priority"; manifest.description = "direct access (or via ssh jumphost) to machines";
manifest.categories = [ manifest.categories = [
"System" "System"
"Network" "Network"
]; ];
roles.default = { roles.default = {
description = "Placeholder role to apply the internet service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -32,6 +31,7 @@
{ {
roles, roles,
lib, lib,
settings,
... ...
}: }:
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -2,12 +2,11 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "localbackup"; manifest.name = "localbackup";
manifest.description = "Automatically backups current machine to local directory or a mounted drive."; manifest.description = "Automatically backups current machine to local directory.";
manifest.categories = [ "System" ]; manifest.categories = [ "System" ];
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.default = { roles.default = {
description = "Placeholder role to apply the localbackup service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules.localbackup = module; clan.modules.localbackup = module;

View File

@@ -6,7 +6,6 @@
manifest.categories = [ "Social" ]; manifest.categories = [ "Social" ];
roles.default = { roles.default = {
description = "Placeholder role to apply the matrix-synapse service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -145,7 +144,7 @@
}; };
} }
// lib.mapAttrs' ( // lib.mapAttrs' (
_name: user: name: user:
lib.nameValuePair "matrix-password-${user.name}" { lib.nameValuePair "matrix-password-${user.name}" {
files."matrix-password-${user.name}" = { }; files."matrix-password-${user.name}" = { };
migrateFact = "matrix-password-${user.name}"; migrateFact = "matrix-password-${user.name}";

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules.matrix-synapse = module; clan.modules.matrix-synapse = module;

View File

@@ -1,3 +1,4 @@
{ packages }:
{ ... }: { ... }:
{ {
_class = "clan.service"; _class = "clan.service";
@@ -6,20 +7,19 @@
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.telegraf = { roles.telegraf = {
description = "Placeholder role to apply the telegraf monitoring agent";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
options.allowAllInterfaces = lib.mkOption { options.allowAllInterfaces = lib.mkOption {
type = lib.types.nullOr lib.types.bool; type = lib.types.bool;
default = null; default = false;
description = "Deprecated. Has no effect."; description = "If true, Telegraf will listen on all interfaces. Otherwise, it will only listen on the interfaces specified in `interfaces`";
}; };
options.interfaces = lib.mkOption { options.interfaces = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str); type = lib.types.listOf lib.types.str;
default = null; default = [ "zt+" ];
description = "Deprecated. Has no effect."; description = "List of interfaces to expose the metrics to";
}; };
}; };
}; };

View File

@@ -1,24 +1,22 @@
{ {
lib,
self, self,
lib,
... ...
}: }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in in
{ {
clan.modules.monitoring = module; clan.modules.monitoring = module;
perSystem = perSystem =
{ pkgs, ... }: { ... }:
{ {
clan.nixosTests.monitoring = { clan.nixosTests.monitoring = {
imports = [ imports = [ ./tests/vm/default.nix ];
(lib.modules.importApply ./tests/vm/default.nix {
inherit (self) packages;
inherit pkgs;
})
];
clan.modules.monitoring = module; clan.modules.monitoring = module;
}; };
}; };

View File

@@ -11,54 +11,34 @@
... ...
}: }:
let let
jsonpath = "/tmp/telegraf.json";
auth_user = "prometheus"; auth_user = "prometheus";
in in
{ {
warnings =
lib.optionals (settings.allowAllInterfaces != null) [
"monitoring.settings.allowAllInterfaces is deprecated and and has no effect. Please remove it from your inventory."
"The monitoring service will now always listen on all interfaces over https."
]
++ (lib.optionals (settings.interfaces != null) [
"monitoring.settings.interfaces is deprecated and and has no effect. Please remove it from your inventory."
"The monitoring service will now always listen on all interfaces over https."
]);
networking.firewall.allowedTCPPorts = [ networking.firewall.interfaces = lib.mkIf (settings.allowAllInterfaces == false) (
builtins.listToAttrs (
map (name: {
inherit name;
value.allowedTCPPorts = [
9273
9990
];
}) settings.interfaces
)
);
networking.firewall.allowedTCPPorts = lib.mkIf (settings.allowAllInterfaces == true) [
9273 9273
9990 9990
]; ];
clan.core.vars.generators."telegraf-certs" = {
files.crt = {
restartUnits = [ "telegraf.service" ];
deploy = true;
secret = false;
};
files.key = {
mode = "0600";
restartUnits = [ "telegraf.service" ];
};
runtimeInputs = [
pkgs.openssl
];
script = ''
openssl req -x509 -nodes -newkey rsa:4096 \
-keyout "$out"/key \
-out "$out"/crt \
-subj "/C=US/ST=CA/L=San Francisco/O=Example Corp/OU=IT/CN=example.com"
'';
};
clan.core.vars.generators."telegraf" = { clan.core.vars.generators."telegraf" = {
files.password.restartUnits = [ "telegraf.service" ]; files.password.restartUnits = [ "telegraf.service" ];
files.password-env.restartUnits = [ "telegraf.service" ]; files.password-env.restartUnits = [ "telegraf.service" ];
files.miniserve-auth.restartUnits = [ "telegraf.service" ]; files.miniserve-auth.restartUnits = [ "telegraf.service" ];
dependencies = [ "telegraf-certs" ];
runtimeInputs = [ runtimeInputs = [
pkgs.coreutils pkgs.coreutils
pkgs.xkcdpass pkgs.xkcdpass
@@ -76,38 +56,7 @@
systemd.services.telegraf-json = { systemd.services.telegraf-json = {
enable = true; enable = true;
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
after = [ "telegraf.service" ]; script = "${pkgs.miniserve}/bin/miniserve -p 9990 ${jsonpath} --auth-file ${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}";
requires = [ "telegraf.service" ];
serviceConfig = {
LoadCredential = [
"auth_file_path:${config.clan.core.vars.generators.telegraf.files.miniserve-auth.path}"
"telegraf_crt_path:${config.clan.core.vars.generators.telegraf-certs.files.crt.path}"
"telegraf_key_path:${config.clan.core.vars.generators.telegraf-certs.files.key.path}"
];
Environment = [
"AUTH_FILE_PATH=%d/auth_file_path"
"CRT_PATH=%d/telegraf_crt_path"
"KEY_PATH=%d/telegraf_key_path"
];
Restart = "on-failure";
User = "telegraf";
Group = "telegraf";
RuntimeDirectory = "telegraf-www";
};
script = "${pkgs.miniserve}/bin/miniserve -p 9990 /run/telegraf-www --auth-file \"$AUTH_FILE_PATH\" --tls-cert \"$CRT_PATH\" --tls-key \"$KEY_PATH\"";
};
systemd.services.telegraf = {
serviceConfig = {
LoadCredential = [
"telegraf_crt_path:${config.clan.core.vars.generators.telegraf-certs.files.crt.path}"
"telegraf_key_path:${config.clan.core.vars.generators.telegraf-certs.files.key.path}"
];
Environment = [
"CRT_PATH=%d/telegraf_crt_path"
"KEY_PATH=%d/telegraf_key_path"
];
};
}; };
services.telegraf = { services.telegraf = {
@@ -115,7 +64,6 @@
environmentFiles = [ environmentFiles = [
(builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path) (builtins.toString config.clan.core.vars.generators.telegraf.files.password-env.path)
]; ];
extraConfig = { extraConfig = {
agent.interval = "60s"; agent.interval = "60s";
inputs = { inputs = {
@@ -151,12 +99,10 @@
metric_version = 2; metric_version = 2;
basic_username = "${auth_user}"; basic_username = "${auth_user}";
basic_password = "$${BASIC_AUTH_PWD}"; basic_password = "$${BASIC_AUTH_PWD}";
tls_cert = "$${CRT_PATH}";
tls_key = "$${KEY_PATH}";
}; };
outputs.file = { outputs.file = {
files = [ "/run/telegraf-www/telegraf.json" ]; files = [ jsonpath ];
data_format = "json"; data_format = "json";
json_timestamp_units = "1s"; json_timestamp_units = "1s";
}; };

View File

@@ -1,95 +1,24 @@
{ ... }:
{ {
name = "monitoring"; name = "monitoring";
clan = { clan = {
directory = ./.; directory = ./.;
inventory = { inventory = {
machines.peer1 = { machines.peer1 = { };
deploy.targetHost = "[2001:db8:1::1]";
};
instances."test" = { instances."test" = {
module.name = "monitoring"; module.name = "monitoring";
module.input = "self"; module.input = "self";
roles.telegraf.machines.peer1 = { }; roles.telegraf.machines.peer1 = { };
}; };
}; };
}; };
nodes = {
peer1 =
{ lib, ... }:
{
services.telegraf.extraConfig = {
agent.interval = lib.mkForce "1s";
outputs.prometheus_client = {
# BUG: We have to disable basic auth here because the prometheus_client
# output plugin will otherwise deadlock Telegraf on startup.
basic_password = lib.mkForce "";
basic_username = lib.mkForce "";
};
};
};
};
# !!! ANY CHANGES HERE MUST BE REFLECTED IN:
# clan_lib/metrics/telegraf.py::get_metrics
testScript = testScript =
{ nodes, ... }: { ... }:
'' ''
import time
import os
import sys
import subprocess
import ssl
import json
import shlex
import urllib.request
from base64 import b64encode
start_all() start_all()
peer1.wait_for_unit("network-online.target")
peer1.wait_for_unit("telegraf.service")
peer1.wait_for_unit("telegraf-json.service")
# Fetch the basic auth password from the secret file
password = peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.telegraf.files.password.path}").strip()
credentials = f"prometheus:{password}"
print("Using credentials:", credentials)
peer1.succeed(f"curl -k -u {credentials} https://localhost:9990/telegraf.json")
peer1.succeed(f"curl -k -u {credentials} https://localhost:9273/metrics")
cert_path = "${nodes.peer1.clan.core.vars.generators.telegraf-certs.files.crt.path}"
url = "https://192.168.1.1:9990/telegraf.json" # HTTPS required
print("Waiting for /var/run/telegraf-www/telegraf.json to be bigger then 200 bytes")
peer1.wait_until_succeeds(f"test \"$(stat -c%s /var/run/telegraf-www/telegraf.json)\" -ge 200", timeout=30)
encoded_credentials = b64encode(credentials.encode("utf-8")).decode("utf-8")
headers = {"Authorization": f"Basic {encoded_credentials}"}
req = urllib.request.Request(url, headers=headers) # noqa: S310
# Trust the provided CA/server certificate
context = ssl.create_default_context(cafile=cert_path)
context.check_hostname = False
context.verify_mode = ssl.CERT_REQUIRED
found_system = False
with urllib.request.urlopen(req, context=context, timeout=5) as response:
for raw_line in response:
line_str = raw_line.decode("utf-8").strip()
if not line_str:
continue
obj = json.loads(line_str)
if obj.get("name") == "nixos_systems":
found_system = True
print("Found nixos_systems metric in json output")
break
assert found_system, "nixos_systems metric not found in json output"
''; '';
} }

View File

@@ -1,6 +0,0 @@
[
{
"publickey": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"type": "age"
}
]

View File

@@ -1,15 +0,0 @@
{
"data": "ENC[AES256_GCM,data:ACFpRJRDIgVPurZwHYW0J1MnvyuiRGnXMeQj1nb9rDAIqHbZzZk8+E0Nu1+EdXwk78ziP6tHR1GQP2ILTtpLME4lXXRVjouW5Eo=,iv:ctR1HENO3XGIq1/gzYi47nateYzsSK317EKn92ptqDI=,tag:q1yuk/ZMx3nuORkiT/XXqg==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMUtabnp3V0dzNFFYRzk0\nd0ZJbUtDMXRPRGxpRjhYR1MyQzdJYWdJTUFrCjBNV0pPTTlIOHBBbzlEQkFzVy92\ndENxcDdIZlNDSm1oZTNveUtIeVc3MXcKLS0tIGtocENjMFNYT0s1LzhYNy92QU5G\nREVEdjErb0xPSE1yb0g5bGlackh6bEUKwxBoDteD7+JfnlFF71CHx4oEdV/TFYcF\n3JPYUbTWAIyMtUu/CLbX+Pn9Mv+McrEIqhwT7TWL/YbELKVadX/k5Q==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:4631iJmioJ2vZ2PTFbdEJu7UqtyQbp43XBlgEbFAviGZdugb3weVI24rJ8m1Rdnxq8uciEeiX6YHBhURdWQY4JNm2wTGnjz7e2PwQ8FCwOmxCcIQPpdKKsziq/M4HArgD66eUxIWfTt1yJfHgBcUuuANbrbH8MirllT+hJTBhqE=,iv:rM8a/MpKbK7DlqjuR4BG77XDHLK11Q+E2rzZLDJalhk=,tag:bbGMn4anXrLHg4eLA0/CXA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../users/admin

View File

@@ -1,4 +0,0 @@
{
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"type": "age"
}

View File

@@ -1,33 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFuTCCA6GgAwIBAgIUMXnA00bMrYvYSq0PjU5/HhXTpmcwDQYJKoZIhvcNAQEL
BQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJh
bmNpc2NvMRUwEwYDVQQKDAxFeGFtcGxlIENvcnAxCzAJBgNVBAsMAklUMRQwEgYD
VQQDDAtleGFtcGxlLmNvbTAeFw0yNTA5MTgxNDMzMzZaFw0yNTEwMTgxNDMzMzZa
MGwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNU2FuIEZyYW5j
aXNjbzEVMBMGA1UECgwMRXhhbXBsZSBDb3JwMQswCQYDVQQLDAJJVDEUMBIGA1UE
AwwLZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7
sdy27E/XMAyKrgeFcXY70R/vX0gx6EcZlWGp2vZSUVAfW1ni/Vq/LVC02sxGEGwv
10+42yP2yghi89doKo8oCoLsbVu+Pi+TmRsgAijy4jN8pHqbn9/Vk8M8utLa1u4z
VonSIx9pzCYd2+IIdwVuWoyPAAnK/JIKS3n0A8KWkZ/1lq6YDl2whj8iY4YF2Ekg
M0SWhquLZiaApAs7STTYvcP7iLfL4U6cH65dRAbwWMpMErPuLf/CedkXiSUp8Zqx
YIXXE5lf7wqt7tM6k6BHic9FEzAo1HnBWBXV5eB5fs1lX9M1VPmx43XINCfzKwxE
xODtIBrmvj+qOp6/ihBsu3LlOoOikxmL+T9Wgvf7fOuFC4BgmX85mGUV+EMZCDoJ
44jlwFF8wgrfG/ZawkP+opNsQLsdOm9DbAdWpx5+JYdgWBahjxuH4z2eIiBmMKgj
puqDgXdZzcERiYtOEEn0p0tvIkVLO3Tm2GjtHbmg1yF2nwsZjupGfcOGTVX4Zi5x
ZCs7vYgBtZy96kNAuyZcFl8eBUr/oVg//i3Zc9Vnw/UJryB7I6dvj228hlrSz0Ve
pGoeZXbcCzRv8NX2V0V1VTtrblSA3w5WRxVzK7UAVetPZ4dlJX+eyx3x2wiC3TiW
ZYH8haFubQqr1h9oXFHgDE5xYZKr51T3SRGfpn6KvQIDAQABo1MwUTAdBgNVHQ4E
FgQUJHOErJYWaGdla1XhxWha4XBKFYgwHwYDVR0jBBgwFoAUJHOErJYWaGdla1Xh
xWha4XBKFYgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXqcg
DW6qzFccR+JTqNR5HBOneB07LxaUqfBTAzU5GTRljY3mVpnTa6vVvXlStChqdmwU
JJdRhWzTpzE4K92l4UKiYKy486PT1ff34aPLPX5BB9OzL4dgvC3gO0MYDJ84AFZl
6BN/MRTinioG+s14SsxmgcUTl+HXsxt75r3WKjXvqECqhONLPXEXDJ6TVmfb2yd5
X9cE6HLS2IXqfvs0EdXmQhSQVS7AlUQWZPDeoBTDUA1tT6ZKCcG0BuHEFnHxg4Yg
W9xp/wMJCEly+9eNJYZYzyK1AHRGnTMRCSifTJEybwI4A35v68FyRLfAC0lM2qVL
yQIGjj55+r4yGCK7bySSKjs59LLLxi6Px3S61OxAYq9KMT65nBLK9JAPFyTnikw9
q/xW208lL+kcRtG+ARo5ycx5QUjWdsHn7TCnqxnDhHznwSV4KGbJFaGQZTtgfcz0
g5a1GwxqHjEZ9IWiN38f2l4kpLLybKhwVQMYeG000s7rDa5hgjbh13qtQN6vUvI6
VozzZPnFcR1Rsa8RR9njDugxbVwlJQfGkoMiMZwNGgXnZRC2XaI6SCyPwqTPBuVP
ZR1eWv4qwsIGKJzJYcdChb5dimlTuVSfZmONpnrOP/4mhQLyaWr3XLqxxP3mIXsz
k1PNWTkgLsXO8DNkCudxcvPElXfmaw6zwaLrZys=
-----END CERTIFICATE-----

View File

@@ -1 +0,0 @@
../../../../../../sops/machines/peer1

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data: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,iv:q3j8EAokyyxiszf+wyRqxEr2igaD1bX7YnFx/NbsGg8=,tag:HKKYWRJEUwW2/TxL+5dSng==,type:str]",
"sops": {
"age": [
{
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaeXRjU214aWk5ajl1aW9E\naGJlb1ViaVRmMTBHdkFDQUNDZS94WFZiNUNvCllmWTJBck9hR3U3V09VWDZwQ2xI\nd3ZEQnBIUG5ZSTVIdS8rQ2FMYVhyNk0KLS0tIEE1UG8rSzFyU01sVXhGVHpoaE9i\nSis4Qi9tMGFqbTNMTDZUVk1ZdXkrM28Km4VkfaOsZ69ckjvrg+os43H/O1IoWHzC\nt4LqZRz1Tk7/d1aLWavSPPjVYrCOMZeNBqGbQpGfjjuXrafClRNQdQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3R1RHTGViTnRLVVkyM3J0\nbm96cGVPTlo4NXBNL0g1eEVSNG9DUkgwVFRBCmRKVTlMRmV3Tmg2RTZIclBlWlcr\ndzI5MUxhcllzbE1IMDNxa08zVkpITmsKLS0tIG01Y2dyQkY3UmRudFk2d0p6bThn\nemlaWnZoS3p4VHhMTFFwTm9VN0ttYzQKVbLFgtK6NIRIiryWHeeOPD45iwUds4QD\n7b8xYYoxlo+DETggxK6Vz3IdT/BSK5bFtgAxl864b5gW+Aw4c6AO5w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:37Z",
"mac": "ENC[AES256_GCM,data:XKCnd0QrAlOCECSeSvbLYHMLbmUh4fMRnLaTb5ARoP4Zc9joWGsCaRZxokc2/sG4BXA/6pkbQXHyIOudKbcBpVjjvs9E+6Mnzt53nfRoH/iOkYPbN2EO49okVZJXW0M1rlBxrxvGuiDlz2p2p6L7neKLy4EB482pYea5+dUr2Yw=,iv:oj/MkZCfkvCmAb79uzEvKwEAm1bKtWhS4rPRAWSgRgw=,tag:h5TPPILXkhJplnDT2Gqtfw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../../sops/users/admin

View File

@@ -1 +0,0 @@
../../../../../../sops/machines/peer1

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:Q0Vn7J0nERccBYT8HZxHF0Zi5qxmMu40n0H1c+L2SCRF6vRLdURxXKDwvh8xtTU=,iv:ucExjoYDFYy19GsBbNNldJRPBSpT+L+x4PrwTG+m2K8=,tag:/Quupyy/nnUNZsDudEMmNA==,type:str]",
"sops": {
"age": [
{
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWWo5OEJ5N1RTR0xMaDhL\nQnlUV2RrRXIzM01OemhQWjVkd3FNZjRhR2dzCi9IeE56b3VZTkNkdW9DMzVia3Zx\nbklxWmFpenRjdEIrc0ZDTGdmSTAxRTQKLS0tIHZJdjdYUzhhY0YzQjRqS0psZmpI\nVHJpUjNZNHRpc2ZWSml1TVNNejhiT28K8TTP/J+XspXZ7TVYj9YaBhEodPIXjojB\nRLqAIgJXRaK4NCLukC6l0IMii6w5J/512RnO2ZBTGhKfbdLfyLOFqg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrZVc5b0FhbzNXcG1zUDlD\neEVWcWpSRkRCMkxBTHdBM3dCbjVpR3FBa0VjCitlTmx4eUJOMHlaU0dFZEhpK3ZD\nZzlMQXVuZWpnaUNmQW9kOGtOaGVDMU0KLS0tIFNlUi9LSzF0UEJCSVBiRlRSNFQz\nNHhMbmNlRXd4ZEJQWVcvTWdCRWEzMUkKls7RbmNOdPDx8z15F+7qay9qIWx6jNsN\nTahT+GgbG29t1aGQCb0yEzKuUyAp39maxxSWToPsfCgJSYJ8RYiUng==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:g+9/fRiqom2+W28ZpiF+oBj9V6ieq5Xz3sRz3GyzvHoLr6yw51JvpG2QuYNYANW0WCiUjFDkU0qPj/9gLHcuX52nc+gNaTzznb1QGPg7WCGSQI7xaMzyYsPxHpg/BOdj5CL8GyLiOWstD1ch0kc3bJmyu68sJUs04uGtHAADzsE=,iv:oASrYaZarEPDu0R3hd/jMazLgwG5r//hIdMyU/tN15o=,tag:o1fgf5oy+rlWXg88FN5Nfw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../../sops/users/admin

View File

@@ -1 +0,0 @@
../../../../../../sops/machines/peer1

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:4NIUEK05kEQAKjR8F9mU3M/XvtZXw+X6CejVI0usMcb4WzagNz7XTVDhLWXZ9St5Ev0Y,iv:bD2+rDLMoWSqUAIZRJof0wRrJVya1xwZUTIJBdCs98I=,tag:g2s4byFHTzwU3ikcBGMElA==,type:str]",
"sops": {
"age": [
{
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQeVh2M2tqSGlOVkpzNlhU\nd0pMd1R0c0tQWnZzdXViWmtxcjl1Wk1Ka0FNCnBUUWJVbjlyR1hSNGpXNWlPRHJB\nNnMzN3BMQ2NDamFBMlhHbVdJUEZ6cjQKLS0tIEJjWmI0ZDl1NXgrSW9uc0R0LzAr\neEwwOC9DdDg2RTJHQ0M3QTFlcVBaSE0K2Du4NguefdEyY1gS6OuVdO3gHga4omcR\n8B+K1wUfIQbArxZLawPxrj7WNDoW5d4mF9fA3MeV1DFyc4KwtYZmUw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvWkdBakVrMVR4RU8xdDlF\nRDkvL0Mrb3ltazhIMjRLZDVlSTVlaFY2ODBBCnlQM2s0SGEvZjFDN3dGWDhIN0dK\nenhQbjZ1ZS9QZzg5SE5XazZXS3dFSkkKLS0tIHJhKzhadGpjTXd4L3hOQkhpR0Fy\nYzhTN2dxVSt3OE5uZFpuWmVlYW4vd1kKwHOxP0C5mLcm4oIT/sGQtUsdsmu3LSN0\nSola5+N+IrAZ+HKnuZlDLZ5JmJSc5j/YhGNn7KR1xhkhfGSS1e3UZw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:ehbrYqTJcsBKGHUB25JHFnKXrJ6z3LkcElZ89xVr4XxLet+odbhsjIoP2FCcxex7PlXcegMduhHBpXwNGUbX+IUNAXTxlWA9CLDmYhWuS2WLiEVXrS11NE03/zUyHdVx/C38dbIPrWD9iaYSrAiuOyfqDTh9k/Bn7vehLTtadoE=,iv:Nk2WVuJydi5tfsb1Mib4A6NocBCDp9QoIbSadq3bIDI=,tag:IaoyfCv3SkmtemXMR9XnkA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../../sops/users/admin

View File

@@ -1 +0,0 @@
../../../../../../sops/machines/peer1

View File

@@ -1,19 +0,0 @@
{
"data": "ENC[AES256_GCM,data:0BmP+NwG/NGe6R5yU55/MdPEQ8E5u+VXWtvstHc4GpDtmBY=,iv:vo8XBcN7KcYjiyKvvp+XDOdP9yR9B7wJi0XlaiCdVbk=,tag:brK9ntAPSuOvw/C+oDo51g==,type:str]",
"sops": {
"age": [
{
"recipient": "age1ntpf7lqqw4zrk8swjvwtyfak7f2wg04uf7ggu6vk2yyt9qt74qkswn25ck",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4Tk1INGtybUVlejlNNlZE\nVms3TkdRVVF1T0E4TmV3NmxvYWVEL2U3WVhNCjJIaHhBcWVlMEYxRjg5bzJpTWdJ\neUhaRTNRTmtlTW0zUXQxTVZEMkQ2MFEKLS0tIFNGWDI4b2FXTE8xQ2xqb0cyK3FI\ncktHWnE5c1ZSVFpmQU1HZmU2VVB1QmcK/s1fVmwpMMg4BYkkAJzSY7hVQWae1F7g\nmfH8EGlr74mifWUNEbd49/K13nl8atQx6bcau83JIEQR+yyihuY4Jw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsL2FXVytUUVZnVU90bG5L\nYURiYjgwN3RuTldWMGl4clpUWmxkeUsrVzM0CkhKZFgwWHl4dWhNSWRQRXVPNDR6\na3hHNmp2RG9YNDhNM2MyV2FuOGY2UlUKLS0tIFpNU2tNOHdhRDRTdHhYWVh2NGZa\nU3J3S0hpclZzWGIwTlFyczdNZkZSZTAKXCZrLaIOVq90ejoKMaRiK0xNw8WOPcnm\nz2uxProEYvQhY8k29mhCFX5HCN0tGn1XTtHeDL7uHuKuFsnSG/fgYQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-09-18T14:33:39Z",
"mac": "ENC[AES256_GCM,data:QkGJKj/H+MI9Mr9Up5NDUToSddY5eTz47egc2+IatfxR8RebKJ2/mYaeLV26vPdmY60bIac4N/nZkoa6IVBhkHHMvsEHsx3nD6Lro9Wf/pWP8Zddzr90LF5p2+wusq25JutKQiPKOb2gmrcagmSsH/7V/UqI/my3PMeKmw6irhw=,iv:hOtHF/cDFdNfvqCKRhJsOwAHEiQmCPjENzsg23sKG+Q=,tag:K7qG9b4fQD0VbAV8OYp3vw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

@@ -1 +0,0 @@
../../../../../../sops/users/admin

View File

@@ -2,14 +2,13 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/mycelium"; manifest.name = "clan-core/mycelium";
manifest.description = "End-2-end encrypted P2P IPv6 overlay network"; manifest.description = "End-2-end encrypted IPv6 overlay network";
manifest.categories = [ manifest.categories = [
"System" "System"
"Network" "Network"
]; ];
roles.peer = { roles.peer = {
description = "A peer in the mycelium network";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -8,7 +8,6 @@
]; ];
roles.default = { roles.default = {
description = "Placeholder role to apply the packages service";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -1,91 +1,36 @@
# Clan service: sshd The `sshd` Clan service manages SSH to make it easy to securely access your machines over the internet. The service uses `vars` to store the SSH host keys for each machine to ensure they remain stable across deployments.
What it does
- Generates and persists SSH host keys via `vars`.
- Optionally issues CAsigned host certificates for servers.
- Installs the `server` CA public key into `clients` `known_hosts` for TOFUless verification.
`sshd` also generates SSH certificates for both servers and clients allowing for certificate-based authentication for SSH.
When to use it The service also disables password-based authentication over SSH, to access your machines you'll need to use public key authentication or certificate-based authentication.
- ZeroTOFU SSH for dynamic fleets: admins/CI can connect to frequently rebuilt hosts (e.g., server-1.example.com) without prompts or perhost `known_hosts` churn.
Roles ## Usage
- Server: runs sshd, presents a CAsigned host certificate for `<machine>.<domain>`.
- Client: trusts the CA for the given domains to verify servers certificates.
Tip: assign both roles to a machine if it should both present a cert and verify others.
Quick start (with host certificates)
Useful if you never want to get a prompt about trusting the ssh fingerprint.
```nix
{
inventory.instances = {
sshd-with-certs = {
module = { name = "sshd"; input = "clan-core"; };
# Servers present certificates for <machine>.example.com
roles.server.tags.all = { };
roles.server.settings = {
certificate.searchDomains = [ "example.com" ];
# Optional: also add RSA host keys
# hostKeys.rsa.enable = true;
};
# Clients trust the CA for *.example.com
roles.client.tags.all = { };
roles.client.settings = {
certificate.searchDomains = [ "example.com" ];
};
};
};
}
```
Basic: only add persistent host keys (ed25519), no certificates
Useful if you want to get an ssh "trust this server" prompt once and then never again.
```nix ```nix
{ {
inventory.instances = { inventory.instances = {
# By default this service only generates ed25519 host keys
sshd-basic = { sshd-basic = {
module = { module = {
name = "sshd"; name = "sshd";
input = "clan-core"; input = "clan-core";
}; };
roles.server.tags.all = { }; roles.server.tags.all = { };
roles.client.tags.all = { };
}; };
};
}
```
Example: selective trust per environment # Also generate RSA host keys for all servers
Admins should trust only production; CI should trust prod and staging. Servers are reachable under both domains. sshd-with-rsa = {
```nix module = {
{ name = "sshd";
inventory.instances = { input = "clan-core";
sshd-env-scoped = { };
module = { name = "sshd"; input = "clan-core"; };
# Servers present certs for both prod and staging FQDNs
roles.server.tags.all = { }; roles.server.tags.all = { };
roles.server.settings = { roles.server.settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ]; hostKeys.rsa.enable = true;
};
# Admin laptop: trust prod only
roles.client.machines."admin-laptop".settings = {
certificate.searchDomains = [ "prod.example.com" ];
};
# CI runner: trust prod and staging
roles.client.machines."ci-runner-1".settings = {
certificate.searchDomains = [ "prod.example.com" "staging.example.com" ];
}; };
roles.client.tags.all = { };
}; };
}; };
} }
``` ```
- Admin -> server1.prod.example.com: zeroTOFU (verified via cert).
- Admin -> server1.staging.example.com: falls back to TOFU (or is blocked by policy).
- CI -> either prod or staging: zeroTOFU for both.
Note: server and client searchDomains dont have to be identical; they only need to overlap for the hostnames you actually use.
Notes
- Connect using a name that matches a cert principal (e.g., `server1.example.com`); wildcards are not allowed inside the certificate.
- CA private key stays in `vars` (not deployed); only the CA public key is distributed.
- Logins still require your user SSH keys on the server (passwords are disabled).

View File

@@ -10,7 +10,6 @@
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.client = { roles.client = {
description = "Installs the SSH CA public key into known_hosts for the configured domains, so this machine can verify servers host certificates without TOFU prompts.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
@@ -39,6 +38,7 @@
... ...
}: }:
{ {
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) { clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
share = true; share = true;
files.id_ed25519.deploy = false; files.id_ed25519.deploy = false;
@@ -64,12 +64,11 @@
}; };
roles.server = { roles.server = {
description = "Runs sshd with persistent host keys and (if certificate.searchDomains is set) a CAsigned host certificate for <machine>.<domain>, enabling TOFUless verification by clients that trust the CA.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {
options = { options = {
hostKeys.rsa.enable = lib.mkEnableOption "generating a RSA host key"; hostKeys.rsa.enable = lib.mkEnableOption "Generate RSA host key";
certificate = { certificate = {
searchDomains = lib.mkOption { searchDomains = lib.mkOption {
@@ -97,7 +96,9 @@
... ...
}: }:
{ {
clan.core.vars.generators = { clan.core.vars.generators = {
openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) { openssh-ca = lib.mkIf (settings.certificate.searchDomains != [ ]) {
share = true; share = true;
files.id_ed25519.deploy = false; files.id_ed25519.deploy = false;

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -13,7 +13,7 @@
} }
``` ```
Now the folder `~/syncthing/documents` will be shared and kept in sync with all your machines. Now the folder `~/syncthing/documents` will be shared with all your machines.
## Documentation ## Documentation

View File

@@ -11,7 +11,6 @@
manifest.readme = builtins.readFile ./README.md; manifest.readme = builtins.readFile ./README.md;
roles.peer = { roles.peer = {
description = "A peer in the syncthing cluster that syncs files with other peers.";
interface = interface =
{ lib, ... }: { lib, ... }:
{ {

View File

@@ -5,7 +5,9 @@
... ...
}: }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix {
inherit (self) packages;
};
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -11,9 +11,7 @@
pkgs.syncthing pkgs.syncthing
]; ];
script = '' script = ''
export TMPDIR=/tmp syncthing generate --config "$out"
TEMPORARY=$(mktemp -d)
syncthing generate --config "$out" --data "$TEMPORARY"
mv "$out"/key.pem "$out"/key mv "$out"/key.pem "$out"/key
mv "$out"/cert.pem "$out"/cert mv "$out"/cert.pem "$out"/cert
cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id cat "$out"/config.xml | grep -oP '(?<=<device id=")[^"]+' | uniq > "$out"/id

View File

@@ -2,17 +2,13 @@
{ {
_class = "clan.service"; _class = "clan.service";
manifest.name = "clan-core/tor"; manifest.name = "clan-core/tor";
manifest.description = "Part of the clan networking abstraction to define how to reach machines through the Tor network, if used has the lowest priority"; manifest.description = "Onion routing, use Hidden services to connect your machines";
manifest.categories = [ manifest.categories = [
"System" "System"
"Network" "Network"
]; ];
roles.client = { roles.client = {
description = ''
Enables a continuosly running Tor proxy on the machine, allowing access to other machines via the Tor network.
If not enabled, a Tor proxy will be started automatically when required.
'';
perInstance = perInstance =
{ {
... ...
@@ -35,7 +31,6 @@
}; };
roles.server = { roles.server = {
description = "Sets up a Tor onion service for the machine, thus making it reachable over Tor.";
# interface = # interface =
# { lib, ... }: # { lib, ... }:
# { # {
@@ -63,7 +58,7 @@
priority = lib.mkDefault 10; priority = lib.mkDefault 10;
# TODO add user space network support to clan-cli # TODO add user space network support to clan-cli
module = "clan_lib.network.tor"; module = "clan_lib.network.tor";
peers = lib.mapAttrs (name: _machine: { peers = lib.mapAttrs (name: machine: {
host.var = { host.var = {
machine = name; machine = name;
generator = "tor_${instanceName}"; generator = "tor_${instanceName}";

View File

@@ -1,6 +1,6 @@
{ ... }: { lib, ... }:
let let
module = ./default.nix; module = lib.modules.importApply ./default.nix { };
in in
{ {
clan.modules = { clan.modules = {

View File

@@ -7,7 +7,7 @@ inventory.instances = {
clan-cache = { clan-cache = {
module = { module = {
name = "trusted-nix-caches"; name = "trusted-nix-caches";
input = "clan-core"; input = "clan";
}; };
roles.default.machines.draper = { }; roles.default.machines.draper = { };
}; };

Some files were not shown because too many files have changed in this diff Show More