Compare commits

..

5 Commits

Author SHA1 Message Date
pinpox
998aaec269 wip 2025-07-22 17:39:50 +02:00
pinpox
77bb690b87 wip 2025-07-22 16:57:24 +02:00
pinpox
85e968c4f7 wip 2025-07-22 16:26:42 +02:00
pinpox
f84da1cf62 Add wait_for_file testing helper 2025-07-22 16:23:20 +02:00
pinpox
af8f4f00c2 add syncthing services 2025-07-22 16:00:01 +02:00
212 changed files with 3492 additions and 5552 deletions

View File

@@ -24,7 +24,7 @@ If you're new to Clan and eager to dive in, start with our quickstart guide and
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
- **Secrets Management**: Securely manage secrets by consulting [Vars](https://docs.clan.lol/concepts/generators/)<!-- [secrets.md](docs/site/concepts/generators.md) -->.
- **Secrets Management**: Securely manage secrets by consulting [secrets](https://docs.clan.lol/guides/getting-started/secrets/)<!-- [secrets.md](docs/site/guides/getting-started/secrets.md) -->.
### Contributing to Clan

View File

@@ -4,7 +4,7 @@ description = "Statically configure borgbackup with sane defaults."
!!! Danger "Deprecated"
Use [borgbackup](borgbackup.md) instead.
Don't use borgbackup-static through [inventory](../../concepts/inventory.md).
Don't use borgbackup-static through [inventory](../../guides/inventory.md).
This module implements the `borgbackup` backend and implements sane defaults
for backup management through `borgbackup` for members of the clan.

View File

@@ -12,7 +12,7 @@ After the system was installed/deployed the following command can be used to dis
clan vars get [machine_name] root-password/root-password
```
See also: [Vars](../../concepts/generators.md)
See also: [Vars](../../guides/vars-backend.md)
To regenerate the password run:
```

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/peer.nix ];
}

View File

@@ -1,6 +0,0 @@
{ ... }:
{
imports = [
../shared.nix
];
}

View File

@@ -1,21 +0,0 @@
{ config, lib, ... }:
let
instanceNames = builtins.attrNames config.clan.inventory.services.syncthing;
instanceName = builtins.head instanceNames;
instance = config.clan.inventory.services.syncthing.${instanceName};
introducer = builtins.head instance.roles.introducer.machines;
introducerId = "${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value";
in
{
imports = [
../shared.nix
];
clan.syncthing.introducer = lib.strings.removeSuffix "\n" (
if builtins.pathExists introducerId then
builtins.readFile introducerId
else
throw "${introducerId} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
);
}

View File

@@ -16,7 +16,7 @@ After the system was installed/deployed the following command can be used to dis
clan vars get [machine_name] root-password/root-password
```
See also: [Vars](../../concepts/generators.md)
See also: [Vars](../../guides/vars-backend.md)
To regenerate the password run:
```

View File

@@ -1,59 +1,9 @@
## Usage
BorgBackup (short: Borg) gives you:
```nix
inventory.instances = {
borgbackup = {
module = {
name = "borgbackup";
input = "clan";
};
roles.client.machines."jon".settings = {
destinations."storagebox" = {
repo = "username@$hostname:/./borgbackup";
rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh'';
};
};
roles.server.machines = { };
};
};
```
The input should be named according to your flake input. Jon is configured as a
client machine with a destination pointing to a Hetzner Storage Box.
## Overview
This guide explains how to set up and manage
[BorgBackup](https://borgbackup.readthedocs.io/) for secure, efficient backups
in a clan network. BorgBackup provides:
- Space efficient storage of backups with deduplication
- Secure, authenticated encryption
- Compression: lz4, zstd, zlib, lzma or none
- Mountable backups with FUSE
- Space efficient storage of backups.
- Secure, authenticated encryption.
- Compression: lz4, zstd, zlib, lzma or none.
- Mountable backups with FUSE.
- Easy installation on multiple platforms: Linux, macOS, BSD, …
- Free software (BSD license).
- Backed by a large and active open-source community.
## Roles
### 1. Client
Clients are machines that create and send backups to various destinations. Each
client can have multiple backup destinations configured.
### 2. Server
Servers act as backup repositories, receiving and storing backups from client
machines. They can be dedicated backup servers within your clan network.
## Backup destinations
This service allows you to perform backups to multiple `destinations`.
Destinations can be:
- **Local**: Local disk storage
- **Server**: Your own borgbackup server (using the `server` role)
- **Third-party services**: Such as Hetzner's Storage Box
For a more comprehensive guide on backups look into the guide section.

View File

@@ -1,215 +0,0 @@
{ nixpkgs, ... }:
let
mkResticGenerator = machineName: rec {
files."restic-rest-pass" = { };
files."restic-rest-env" = { };
files."restic-repokey" = { };
share = true;
validation.name = script;
runtimeInputs = [ nixpkgs.xkcdpass ];
script = ''
xkcdpass -n 4 -d - > "$out/restic-rest-pass"
xkcdpass -n 4 -d - > "$out/restic-repokey"
echo "RESTIC_REST_USERNAME=${machineName}" > $out/restic-rest-env
echo "RESTIC_REST_PASSWORD=$(cat $out/restic-rest-pass)" >> $out/restic-rest-env
'';
};
in
{
_class = "clan.service";
manifest.name = "restic";
# Define what roles exist
roles.server = {
interface =
{ lib, ... }:
# system ,
# {
# ...
# }:
# # let pkgs = import nixpkgs { inherit system; }; in
{
# These options can be set via 'roles.server.settings'
# options.restic.package = lib.mkPackageOption pkgs "restic" { };
options.directory = lib.mkOption {
type = lib.types.str;
default = "/var/lib/restic";
description = ''
The directory where the restic repositories are stored.
'';
};
};
perInstance =
{ settings, roles, ... }:
{
nixosModule =
{
config,
lib,
pkgs,
...
}:
{
clan.core.vars.generators = {
restic-cert = rec {
files."restic-key" = { };
files."restic-cert".secret = false;
share = true;
runtimeInputs = with pkgs; [
coreutils
openssl
];
validation.script = script;
# TODO openssl will ask for Country, City, etc.
# Can we pass those as prompts?
# We use a wildcard cert for *.restic
script = ''
openssl req -newkey rsa:2048 -nodes -x509 \
-keyout $out/restic-key \
-out $out/restic-cert \
-addext "subjectAltName = DNS:*.restic"
'';
};
restic-server = {
files."htpasswd" = { };
runtimeInputs = [ pkgs.apacheHttpd ];
# depend on all client's generators
dependencies = lib.unique (
map (machine: "restic-${machine}") (lib.attrNames roles.client.machines)
);
script = lib.concatMapStringsSep "\n" (machine: ''
cat $in/restic-${machine}/restic-rest-pass | \
htpasswd -B -i -n ${machine} >> $out/htpasswd
'') (lib.attrNames roles.client.machines);
};
}
// builtins.listToAttrs (
map (machine: {
name = "restic-${machine}";
value = (mkResticGenerator machine);
}) (lib.attrNames roles.client.machines)
);
systemd.paths.restic-htpasswd = {
description = "Watch restic-rest .htpasswd for changes";
pathConfig = {
PathChanged = config.clan.core.vars.generators."restic-server".files."htpasswd".path;
Unit = "restic-rest-server.service";
};
};
systemd.services.restic-rest-server.serviceConfig = {
# TODO not sure if this is the correct fix
RestrictAddressFamilies = lib.mkForce "AF_INET AF_INET6";
LoadCredential = [
"restic-cert:${config.clan.core.vars.generators."restic-cert".files."restic-cert".path}"
"restic-key:${config.clan.core.vars.generators."restic-cert".files."restic-key".path}"
"htpasswd:${config.clan.core.vars.generators."restic-server".files."htpasswd".path}"
];
};
networking.firewall.allowedTCPPorts = [ 8124 ];
services.restic.server = rec {
enable = true;
# prometheus = true;
privateRepos = true;
listenAddress = "0.0.0.0:8124";
dataDir = settings.directory;
extraFlags = [
"--htpasswd-file=%d/htpasswd"
"--listen=${listenAddress}"
"--tls"
"--tls-cert=%d/restic-cert"
"--tls-key=%d/restic-key"
];
};
};
};
};
roles.client = {
# TODO
};
# roles.client = {
# interface = {
# # These options can be set via 'roles.client.settings'
# options.ipRanges = mkOption { type = listOf str; };
# };
#
# # Maps over all instances and produces one result per instance.
# perInstance = { instanceName, settings, machine, roles, ... }: {
# # Analog to 'perSystem' of flake-parts.
# # For every instance of this service we will add a nixosModule to a client-machine
# nixosModule = { config, ... }: {
# # Interaction examples what you could do here:
# # - Get some settings of this machine
# # settings.ipRanges
# #
# # - Get all controller names:
# # allControllerNames = lib.attrNames roles.controller.machines
# #
# # - Get all roles of the machine:
# # machine.roles
# #
# # - Get the settings that where applied to a specific controller machine:
# # roles.controller.machines.jon.settings
# #
# # Add one systemd service for every instance
# systemd.services.zerotier-client-${instanceName} = {
# # ... depend on the '.config' and 'perInstance arguments'
# };
# };
# }
# };
# Maps over all machines and produces one result per machine.
# perMachine = { instances, machine, ... }: {
# # Analog to 'perSystem' of flake-parts.
# # For every machine of this service we will add exactly one nixosModule to a machine
# nixosModule = { config, ... }: {
# # Interaction examples what you could do here:
# # - Get the name of this machine
# # machine.name
# #
# # - Get all roles of this machine across all instances:
# # machine.roles
# #
# # - Get the settings of a specific instance of a specific machine
# # instances.foo.roles.peer.machines.jon.settings
# #
# # Globally enable something
# networking.enable = true;
# };
# };
}

View File

@@ -1,25 +0,0 @@
{ self, inputs, ... }:
let
restic-module = import ../restic { inherit (inputs) nixpkgs; };
in
{
perSystem =
{ pkgs, ... }:
let
nixosTestArgs = {
inherit pkgs;
inherit self;
};
in
{
checks = {
restic = import ./tests {
# inherit (self) clanLib;
inherit restic-module
pkgs self;
} nixosTestArgs;
};
};
clan.inventory.modules.restic = restic-module;
}

View File

@@ -1,81 +0,0 @@
{
pkgs,
self,
clanLib,
restic-module,
...
}:
clanLib.test.makeTestClan {
inherit pkgs self;
nixosTest = (
{ ... }:
{
name = "restic";
clan = {
/*
# Desired inventory for a complete test:
inventory = {
machines = {
# - Two backup servers (destinations)
# - Three backup clients:
# - client_1 and client_2 should backup to both servers
# - client_3 should only backup to server_2
server_1 = { };
server_2 = { };
client_1.tags = [ "backup_to1" "backup_to2" ];
client_2.tags = [ "backup_to1" "backup_to2" ];
client_3.tags = [ "backup_to1" ];
};
services = {
# Apply roles based on tags for clients
restic.backup_dest1.roles.client.tags = [ "backup_to1" ];
restic.backup_dest2.roles.client.tags = [ "backup_to2" ];
# Apply roles based on machine names for servers
restic.backup_dest1.roles.server.machines = ["server_1"];
restic.backup_dest2.roles.server.machines = ["server_2"];
};
};
*/
inventory = {
# Define machines
machines.client_machine = { };
machines.server_machine = { };
modules.restic = restic-module;
instances.test-restic = {
module.name = "restic";
roles.server.machines.server_machine = {
# Server settings
settings.directory = "/var/lib/restic";
};
roles.client.machines.client_machine = {
# Client settings
};
};
};
directory = ./.;
};
# defaults = { };
#
# nodes = { };
testScript = ''
start_all()
server_machine.wait_for_unit("restic-rest-server")
'';
}
);
}

View File

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

View File

@@ -1,11 +1,7 @@
---
description = "A secure, file synchronization app for devices over networks, offering a private alternative to cloud services."
features = [ "inventory" ]
[constraints]
roles.introducer.min = 1
roles.introducer.max = 1
---
**Warning**: This module was written with our VM integration in mind likely won't work outside of this context. They will be generalized in future.
## Usage
@@ -26,7 +22,7 @@ We recommend configuring this module as an sync-service through the provided opt
- **Share Folders**: Select folders to share with connected devices and configure permissions and synchronization parameters.
!!! info
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#clan.syncthing.introducer)
Clan automatically discovers other devices. Automatic discovery requires one machine to be an [introducer](#roles.introducer)
If that is not the case you can add the other device by its Device ID manually.
You can find and share Device IDs under the "Add Device" button in the Web GUI. (`127.0.0.1:8384`)
@@ -37,4 +33,4 @@ We recommend configuring this module as an sync-service through the provided opt
## Support
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).
- **Documentation**: Extensive documentation is available on the [Syncthing website](https://docs.syncthing.net/).

View File

@@ -0,0 +1,174 @@
{
# lib,
# config,
# pkgs,
...
}:
{
_class = "clan.service";
manifest.name = "clan-core/syncthing";
manifest.description = "A secure, file synchronization app for devices over networks";
roles.introducer = {
interface =
{ lib, ... }:
{
options = {
# TODO is this option even needed or used anywhere?
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
# default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
};
perInstance =
{
# instanceName,
roles,
settings,
...
}:
{
nixosModule =
{
...
}:
{
_module.args = {
inherit settings roles;
introducerID = null;
};
imports = [
./shared.nix
];
};
};
};
roles.peer = {
interface =
{ lib, ... }:
{
options = {
# TODO is this option even needed or used anywhere?
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
# default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
};
perInstance =
{
# instanceName,
roles,
settings,
...
}:
{
nixosModule =
{
lib,
config,
...
}:
{
_module.args =
let
introducer = builtins.head (lib.attrNames roles.introducer.machines);
introducerIDPath =
if settings.introducer == null then
"${config.clan.core.settings.directory}/vars/per-machine/${introducer}/syncthing/id/value"
else
"${config.clan.core.settings.directory}/vars/per-machine/${settings.introducer}/syncthing/id/value";
introducerID = lib.strings.removeSuffix "\n" (
if builtins.pathExists introducerIDPath then
builtins.readFile introducerIDPath
else
throw "${introducerIDPath} does not exists. Please run `clan vars generate ${introducer}` to generate the introducer device id"
);
in
{
inherit settings roles introducerID;
};
imports = [
./shared.nix
];
};
};
};
}

View File

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

View File

@@ -2,49 +2,11 @@
config,
pkgs,
lib,
settings,
introducerID,
...
}:
{
options.clan.syncthing = {
id = lib.mkOption {
description = ''
The ID of the machine.
It is generated automatically by default.
'';
type = lib.types.nullOr lib.types.str;
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
default = config.clan.core.vars.generators.syncthing.files."id".value;
defaultText = "config.clan.core.vars.generators.syncthing.files.\"id\".value";
};
introducer = lib.mkOption {
description = ''
The introducer for the machine.
'';
type = lib.types.nullOr lib.types.str;
default = null;
};
autoAcceptDevices = lib.mkOption {
description = ''
Auto accept incoming device requests.
Should only be used on the introducer.
'';
type = lib.types.bool;
default = false;
};
autoShares = lib.mkOption {
description = ''
Auto share the following Folders by their ID's with introduced devices.
Should only be used on the introducer.
'';
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"folder1"
"folder2"
];
};
};
imports = [
{
# Syncthing ports: 8384 for remote access to GUI
@@ -65,7 +27,7 @@
{
assertion = lib.all (
attr: builtins.hasAttr attr config.services.syncthing.settings.folders
) config.clan.syncthing.autoShares;
) settings.autoShares;
message = ''
Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device.
'';
@@ -80,12 +42,8 @@
services.syncthing = {
enable = true;
overrideFolders = lib.mkDefault (
if (config.clan.syncthing.introducer == null) then true else false
);
overrideDevices = lib.mkDefault (
if (config.clan.syncthing.introducer == null) then true else false
);
overrideFolders = lib.mkDefault (if (introducerID == null) then true else false);
overrideDevices = lib.mkDefault (if (introducerID == null) then true else false);
key = lib.mkDefault config.clan.core.vars.generators.syncthing.files."key".path or null;
cert = lib.mkDefault config.clan.core.vars.generators.syncthing.files."cert".path or null;
@@ -98,13 +56,13 @@
devices =
{ }
// (
if (config.clan.syncthing.introducer == null) then
if (introducerID == null) then
{ }
else
{
"${config.clan.syncthing.introducer}" = {
"${introducerID}" = {
name = "introducer";
id = config.clan.syncthing.introducer;
id = introducerID;
introducer = true;
autoAcceptFolders = true;
};
@@ -112,6 +70,7 @@
);
};
};
systemd.services.syncthing-auto-accept =
let
baseAddress = "127.0.0.1:8384";
@@ -120,7 +79,7 @@
SharedFolderById = "/rest/config/folders/";
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
in
lib.mkIf config.clan.syncthing.autoAcceptDevices {
lib.mkIf settings.autoAcceptDevices {
description = "Syncthing auto accept devices";
requisite = [ "syncthing.service" ];
after = [ "syncthing.service" ];
@@ -138,7 +97,7 @@
${lib.getExe pkgs.curl} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice}
# get all shared folders by their ID
for folder in ${builtins.toString config.clan.syncthing.autoShares}; do
for folder in ${builtins.toString settings.autoShares}; do
SHARED_IDS=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${lib.getExe pkgs.jq} ."devices")
PATCHED_IDS=$(echo $SHARED_IDS | ${lib.getExe pkgs.jq} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]")
${lib.getExe pkgs.curl} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder"
@@ -147,7 +106,7 @@
'';
};
systemd.timers.syncthing-auto-accept = lib.mkIf config.clan.syncthing.autoAcceptDevices {
systemd.timers.syncthing-auto-accept = lib.mkIf settings.autoAcceptDevices {
description = "Syncthing Auto Accept";
wantedBy = [ "syncthing-auto-accept.service" ];
@@ -162,7 +121,7 @@
let
apiKey = config.clan.core.vars.generators.syncthing.files."apikey".path;
in
lib.mkIf config.clan.syncthing.autoAcceptDevices {
lib.mkIf settings.autoAcceptDevices {
description = "Set the api key";
after = [ "syncthing-init.service" ];
wantedBy = [ "multi-user.target" ];
@@ -182,7 +141,6 @@
};
clan.core.vars.generators.syncthing = {
migrateFact = "syncthing";
files."key".group = config.services.syncthing.group;
files."key".owner = config.services.syncthing.user;

View File

@@ -0,0 +1,99 @@
{
name = "syncthing";
clan = {
test.useContainers = false;
directory = ./.;
inventory = {
machines.introducer = { };
machines.peer1 = { };
machines.peer2 = { };
instances."test" = {
module.name = "@clan/syncthing";
module.input = "self";
roles.introducer.machines.introducer.settings = {
autoAcceptDevices = true;
autoShares = [ "Shared" ];
};
roles.peer.machines.peer1 = { };
roles.peer.machines.peer2 = { };
};
};
};
nodes = {
# peer2.console.keyMap = "colemak";
peer1.services.syncthing.openDefaultPorts = true;
peer2.services.syncthing.openDefaultPorts = true;
introducer = {
services.syncthing.openDefaultPorts = true;
# For faster Tests
systemd.timers.syncthing-auto-accept.timerConfig = {
OnActiveSec = 1;
OnUnitActiveSec = 1;
};
services.syncthing.settings.folders = {
"Shared" = {
enable = true;
path = "~/Shared";
versioning = {
type = "trashcan";
params = {
cleanoutDays = "30";
};
};
};
};
};
};
testScript = ''
start_all()
# import time
# time.sleep(500000)
introducer.wait_for_unit("syncthing.service")
peer1.wait_for_unit("syncthing.service")
peer2.wait_for_unit("syncthing.service")
# Check that syncthing web interface is accessible
introducer.wait_for_open_port(8384)
peer1.wait_for_open_port(8384)
peer2.wait_for_open_port(8384)
# Basic connectivity test
introducer.succeed("curl -s http://127.0.0.1:8384")
peer1.succeed("curl -s http://127.0.0.1:8384")
peer2.succeed("curl -s http://127.0.0.1:8384")
# Check that folders are created correctly
peer1.execute("ls -la /var/lib/syncthing")
peer2.execute("ls -la /var/lib/syncthing")
peer1.wait_for_file("/var/lib/syncthing/Shared")
peer2.wait_for_file("/var/lib/syncthing/Shared")
# Check file synchronisation from peer1 to peer2
introducer.shutdown()
peer1.execute("echo hello > /var/lib/syncthing/Shared/hello")
# peer2.wait_until_succeeds("timeout 5 cat /var/lib/syncthing/Shared")
peer2.wait_for_file("/var/lib/syncthing/Shared/hello")
out = peer2.succeed("cat /var/lib/syncthing/Shared/hello")
assert "hello" in out
'';
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:JbpTTfHD92NlaUR7xAyJFoqD+4mYDlpE1gdWuCsrMyar8rUzS6vX7i7ymd69K0tPAT/UUZAmNacPFwvjTkZmdv+/719FNBkowrc=,iv:ZHTcm+V1dNZ07kRQEDNFYh8NMMwZ5g5cq0Tg281Aaec=,tag:tjAJRuQrRC0JYhS0tA+VUw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJZWV1ZUZlODZ2OXlXbzVX\nSTkzV0VFL1NJVXpzKy9hTy9NN3gzeWxJbzE4CnJZc0JYd2s3ci85aDFuQ3pJbmtT\nSWZGRmREM25nWjhkZ1hGNE0rcVpMbFEKLS0tIDk3aHZ6ZVlaTmhOV3B4b3g5MzV4\nUUNWcXdHcTlVVEw3UGlxQWtBSDdpMk0K6nCih/rHq4vLS/oDz8cbjY8TVVsQmzaW\nivSd3WhpUaRdigyw/u3/5Lmaii1awy2qJdyREbzzUVgJPfoZ87pabw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:15Z",
"mac": "ENC[AES256_GCM,data:KoxJwNfRO1SDlgCc5p9+ZDP6rXOAUXG48ousVXKgNfR+qyS9i0FIYjgJxsSxzsYyn0Md7fbbJdX8MEnJZkgkTn0pJ46HfHsD4oiE66AF4pcgdIssTo4BX6RvoqbCdtS6hi6dpyrW7j1PPhwO3DRhaFIO58Nk1fxcVpyATzm8Gyg=,iv:SYotgqC8fA80mmjYZxUM0p+MUGxRYKHCd1pscS3HVt0=,tag:3XrQabwt+KEtk8JLZ4HTPA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:Lr3bPR9MjNvwBYPIQg0D4qIDhRbD++ZOpuGvz093d+DWva1b4h1jhcsnmziOvINZQ3vVpizklkASRWo757FOJLLV1LiXNqiZAbY=,iv:HbsNN9Who9BFTHEUrRVAA5MAkadXVqTGEsq5kTPZdQo=,tag:pGW8oINEGRjDgg1JoHdUEQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBM1Q5UEpvaDQ4K25oTWN3\nbmdPaTdZUTFFR0xUVEttajNXOSt1T3BnTjJnCkE2eUVleXJKckovdkw3ak1xUGZj\nK3hET0E0OU5KYlZLL2daZFNaZ3cyS00KLS0tIHFwMW9LTHNLR1JwaHdiMnc2bWhH\nMWtwMElUYW50ZXlGZG1US1hiWlNoRjAKTg62lhjMCG1uPtxAmq5L7QGwmlwvGnxG\n+qTZHAPAoTUaWtnJfJpueGB1OJbr4HbUH0gN0cBqq7Y0DGIyvGqidA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:1fh6Jp+y6jGI3NgZMgGcuVHX3GLXYH9LKbPG4cVwOk1otX94zr0UcrVOSgH3m9J7QpGlFl48HwCfoNZzkRVmX633Px1UZQopOSZLao8Ao7ZcAwP3EmIowwJBC8//pYIhE6JwPlIlRbHOQRDd8HhIM1VwNjc8dUBBX0VyTAgyT4w=,iv:0Z722NhqETyVY+mkerERVw9TmKx0aASdSdYYdNucmCg=,tag:PT56bX50YajhlHQttz+Ffw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -0,0 +1,15 @@
{
"data": "ENC[AES256_GCM,data:XgQnGe/dmXyir8gDOgcsdYok1d1blDBx+AFDLw+tXBzv5FY1pSbonSuOKmEVWDEmRCR3o1D8qiuUrDsa68D3als69A/bRhYHR+A=,iv:JzNB2VjE+HHAOQXkN3t95wQPiBBj/c93X5JHP8fosHg=,tag:fcAG53Tp+38NAfqSObIa4A==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6amIrbk9uV1IyQ20zMExo\nL0RrMmxOa3dEOGNmZFhIWXF1Sjc1K3VtdkdFCklLdEVNYnpsclRUdUtyTmVteWhu\nMHpPQmYzSEVldytGazJ1azBnQWJsaEUKLS0tIEZFY0hMNnV1TE9jODdpdy80eXRx\nMDZWSGt1MnhYQWJoMmNoaW1KWGVsVk0Ki0VQLz79+QQeiOri0aBqHEsVessIyjX8\nv3OZAjwMglPNv3j4CIqY/F4sfrAYxKUNB7g0Ui56BZlrG/i68EupAg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:081iGB26aJv0067lLJVetcKOyzzaHys6W70hcxB9010kpyU6AkxNt+HCa2lvJNAv+lls4WXgM6WsD/KODkDvbZsP4U3P9sJqY4RbTqJvypN4yjmzogneB7GVOenMQ8ywbm+ILM54nx8Enn6GPm4YX6yTat4WVTFFd+dJYmfBBmM=,iv:rXOqKtBSZvwozT49Zhp6aBpLXWlim7KLRGg6yIb2vkQ=,tag:vmjlvW0PNlHYs7syshJETg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:/dsV5uaNTBmB82lbzlQHGyRbFeXM9l32igrLcfGG13td,iv:6PB0LqjRtvrSYxeOPN+261VqacGg0jczCyyF4FZQa/I=,tag:4GlcYRhYhHVdSkwDSzcT6Q==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKYmJIUVhVQ3c3ZnhweUhU\nZnR6THB1Y2RjWExPckIwNEl1SlRGeVJORUVVCkk1ZzZwbGNwc1JQOFRpN2MwT0xG\nZTErNzVmSnhPazJJVHBKUnl6d2lYaVkKLS0tIHJRclM2WEYzQTRwVXRHcUUxSTlt\na1pHamdpdm9hUU9PSndybzdlMWY3NkkKcTnYp/5fUTdiNr0ajJeAPuLwhgWAdlAE\ngZ4soLdZoBFUHSsh0LhEm6jO2DXEKUZY7oLZi1gSZZbPA7PI/5k7OA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3THZibDFMaTZrKzJVWjd3\nd3RScjZVZmNlbitmc2FkRXl0UlVmZlRoM2dJCnlkUW1pa3dHZlNqTklxc3dZTkha\nQUpWck5BcEk4NjRjSXI3TlFzQ0pzVVUKLS0tIFpJdHJzKzVkeGFBcnRvR3pDeFN2\nNFNFaW9kY3JXdzk0Q3hPS29iOFRkK1UKoQSqFLIAeU6aRL+rDQ+oJ9PS6aAtAPeo\n5Kpwoi3KQHVrDDIRBaxvZ2BXObOyU0tBqjzBdrOgtRn+96HmnZ6JDw==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:15Z",
"mac": "ENC[AES256_GCM,data:4T1+t0hZwN0ThdXoCcNeUiQNj2RosRP6HL6SKmsnECn3pmDZqJEt/0xEQWDeDcvfHyZkJAFbU0RfhYsyz2wtYZwuMDL8WEbHURa1GQ4uKNWfUPPeu88eTwnYsbvtUS6TdRZ26eDoQKDNEed1cy9TfI4Bugdt0rrl6O+su2Ikvyo=,iv:aMLJ4r55UY5cIB1rQNns3jN2U/ZjfFN1vl6ZsXpcBbo=,tag:P2Dv7LeGB1fT4TYHwJgLGw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:6bI+k3BUKfpOnYSZx6ucCEKLvPCC2+LVrHzxQyN7MvLKk43LnKsfSSI8lpp5l8CE8Bpcpq9BqWVU3ZhnwmM4RbiHg/+C7R374KqoV0TLXMwq4lkeVM5oh4On2qBbnxsXtBz6NbJmIiJPPTT5YARdDAdIVUZ21jOj7G5jvm1BbAWojVkrKpb5IpOTmuxSA0CPEQRsvP9NHH+5Btm3J8Py4bHcRpgNeKaFfTrJjtXN2te8hTjJb8sGmFvuOr92Tdh/+O5qeA4SjMXqRJEiJiE+Fc9X9ukrxlZkDwsSn2gXQu7ahGhjOZCvQ/Th9j55/ZPiu5zrypfBIltSfGaMzHTqSECSGozfNSqbAAuu/ybCMmp87HDg9RQ3M/2yYqmSs2cRSB1/jN7ha3LTEnPhPKWnpOmnNvQT/rSX0wyfGtdKVk0bIgEc28wIKANyJC+HPPtENdcmdUhx6q2CRi1h+nFz92zkRGzWySgCcl51WawFDD6KRKLvqpq9LWv/bNl4NOQd307hPZguLLB6/Q/yV29j+XgGrZcmlOnn5NfAlVfF+F6ckebPxDKMg0Z3DD/83DPM18iO5SeDcaNVS3c3KoeSpURWMHr5+2n+qtu3wVvKsPPhAsBV8diwGxp5z00Rr84mHXPfnz56Tr+m/rYLfflryrybxTujcuWFCGgzwMdeaYx4tz7lQAY1XtdKSNTYjhWdDfFbqThP3HVGJsRQshCd+8Q4miEfJzDmUgbrUuoF3VjkMApZ4wULV5BCcLrDYIXkwu1lyDt5fk4G0TyZCP8JvBV3zcVxjAPW86tSHNcX0V56Yd090s51u0iJyM/eqNxoNXa/DWBuLd4jpWJRz2c48pUO1XRgqvZHFmUcO5q7SPL/bCvdpjqaopnOqt3501zGB2q+TGfyZUZ3sqtylTPz4C7tv/hrCzmRCTzEFlfsmUOi08DL/6K65ihe6uXjoH2fB7ypjUy4u5L1XcBIvxSitcSacosJHSUtWcFZ6mWIgVyChTjWRfCAF4eB3aVctW+OmVTeiTcdAfbDiPR5FNyDWPTMnnorjBVZkpU=,iv:RGNo6ipQplIsHL7avQpTgEKMDifKnB5W96vYf6X96cQ=,tag:k8oObQ1m3aVnG94MR7dhHw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5NFVRb3cvZEZXdW1xWlBP\nWWNCRjlxYmpPQ1k2aFBmOUR4M0F2ai9Iam5ZCkxzU2dUamZXRmRqV3ZyWTRvdUda\ncTlHa1BuSDQ0em9lMXRwUlc5V2E1WUUKLS0tIDRkamJVZ3NIVlJGRlNrK0EyT09x\nbHVsMzlKVzJGNS8vVVlHRlFJRmUvUlUK1OSBTQi6R3XYEZEQmpGCrMr8m4jCBRiV\nj/G9sCahhTUc0D7E7DTXD3fwfXnZk1bD7buA99f908DT2Bjv4TfeKQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZkJFbWhINkxmK3g4RHFN\neko4VTZHVGZOK1E2dFVqbWdVZktnTEdqT1JJCnBraG1acHVrNzlBWmJhNEMya1Fi\ndUw0NDdKakdLcGE1Z0tsSHVvQ0l1NFkKLS0tIDZvek42eXVEbEdWeG1vSENnakQ4\nQzlGaUNKMVhEM2QwY3ZPTW5nYXNLUkUK2laHy33U+hcQMT4jlUOqtVRCy+hNHyaS\nyuSk1i7Am3VeProaXccREjHjYRHn/l/B1oLRQQQT4cLcComxOArz0w==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:16Z",
"mac": "ENC[AES256_GCM,data:CVyuCoiArDYnoz/GQ0OZ2K0rJ1+Y1xoznp+v4rxAfL22fv3mY29EDw7ByYXxye0ARCD8gBFCpKeUvWbfOPJGfjsfAFT0AxLYFbUONgejYZpVZbnlElfLUSi39ZhaSwcImqe4RLJ2TND/HuJ6jwz0Lb1h8BWOq5/NeF8JJH2tHFo=,iv:n2liiufvBjCjASBIAmOu2Q5IONsX99GFmZw7YDeHJ+4=,tag:0bYMs8k362w7jApCird4yA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -0,0 +1 @@
UHILKRC-BZSMZT7-CA5ANWX-DJTR55R-NBGUBK4-SOZJG5Z-MONIHKP-ZHFGOAL

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:YOzFYRjbmtSqzz+gaimswru/Xl3h2bZ0zkkM+QokycDIGhk5wrdlrGNJiaiQk3xJ1JUEbJewmsBFPz0Xcl8Nh2aAj58s0fMg/H0lb6WfF0n7B4u3TZ6urWir4ViUPdVxcS7oxKhJCU2KoqFNpXS/NjRCmi4G12j42VGBStLNxCFZ8EH4oK/nV0tYpJ++meKA9IlQbuEKmAv6Xt7Ry1isW4THl7jzYwE6y7I1ThqudnDtvqguY+Nl27vMmFed+WmpE4jsXjeW7T/CTynafen81uL7gP6xiy/n+3nZNYgutHCTqa1AonITDIJrzRjAPMmYxwv0k1ebYImBCT0wH7rnmbLF7y4EMNFuA3O5nAvpZ8tkkLzooVf0PCYrtWbwsxOm,iv:T6aRMEHnOezxswMnJXBXoHVkVDFtDqvtCRhqvQHLOWU=,tag:2zTGHZWCt6SQrESyC3f9Rg==,type:str]",
"sops": {
"age": [
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSK2pOU0Zoam52V00vdnFi\nR2hpenpHQ1BDaHlyODZ0cmgvdzlLMTZ6em5VCi9sdG9zdzY0Z1pDQVF3dnNSREdt\nQVVUQ29uaE03SWM3bnA3blRHZWJmUmMKLS0tIGwyNUhVMTJ5SkxudlZaVUJaM0xJ\nSEpoaFZaUlczMkYxWlZhb1RBbUZ3SFUKGmjG+r998QLDJylznGhuqa6magj9x9PM\nly7PlqaoZ1diLuFklqFVExK3cXwvA6xdOScZqd8/P/sEdzAodDuKQw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1t6s4d0sn3jlzlu7zxuux5fx8xw9xjfr0gqfaur0wessfrxsg35hsqttv2t",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnMlpRK043Rm0zbTJxUGlv\nbE5mOUhhZDQ3a2UrNVhHZithUTJMRlo2Q2hZCjQ2aUt6VXpuOVE4dVVyVFc0WjBm\nMWI0SHVqYUNjdkQvSXY5QWdFTDRjdGcKLS0tIDBleURzYVBlZVcwQk85YzhOWE1z\nd2ZwYVRMM0dxM3U0RWI0Z2kxSjlETWsK+/m7xmcoXlnfYkRL3RK1VATGY6RtkmHA\nfg+YeAENLT1Mr9SnJCWcodxFicz8hiN5PinjynjqWgfv/xLMuh2G9Q==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:16Z",
"mac": "ENC[AES256_GCM,data:dKjYJ4ogUztSwuP6gkyXj/gYd3TDztuwivhtRzLYwoJLJ7c4anEeZDA51tslnhdDDXQ101JDEt0tD05wAo1YdLTNg9YR0eAh4wZ/dkJ+U92U7DYEW40YXxy8co/WWW50eYqbD6SnZbZAMjf4SbaH6kqtS2MTqeba2B1G/y8sb94=,iv:Z8tzsvSTRjondcN0Vnc3bcyVlVnpqLYRoaOCe6dLQGQ=,tag:leifNnUovWvO5cqzyN2PNw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:W48JccasfJGc+WJOgGJf+T5OfVYSAfvghuXC7wUqVkkI,iv:Ukhb8+d1FrZPSmerFZUbuL/G2yHpBNSJeRL4nYG2Cm8=,tag:FCf3VI+eBsmrXvbsI62B7w==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbkFSTTBYVlNjVjNyYmIx\nSjl3UUp5VVlJZ2xudW8veS94VUVwbS94NHkwCnZ3LzZocFBnYkN3K0wwelhYalM5\nOVRmN3g1Tk5rRTViZXkwT244b1I3bzgKLS0tIEpKdCtiSTdjREV4OVZDbU1rdDk1\nTnZZTEk5OEZlWFRFTTVITkRCTFVFQXMKV8SdppHqwCEIyRTNUxjG7AtGZyVZuKBr\ndXURED8uLw37i2gvAzlUZLzQieV+F//muVF4fFONucBq5wnRskyBTA==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWb1p5WEM5d3h1RVRpRVVk\nMXArMk9DOTNoYm9yT3pXbWV1YmN4MDFXd2hvClJpUEwwM0pDSUtqZHRvaTI1RlNF\nTkRBYlo4R1lpR0Y1YnRLWGIxNThFMFUKLS0tIGIzeTZ1QVJTZ3VlbjgwN1Z2Y3Bw\nTWxOT2xtVmR0Zm1jeW8wdE1xTEQ0aDAKkMuwZwMPUPO8kGmc7xCElxvpTFfGqag4\nXm6KaoyGNJO5OoWoXgJawynRNyJX2JFGjcI1M1xK4ItYU5NF4Po8ig==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:uOaOVlLb+qLshDC4Y1KxYFgsPpHmNlfDaaPHcBBvNs7brBfsfZOPwVXqPxj5M9IzhD87l7kbYisaeBCqDKPM+IdZV1C3WstRu70a26onLl+JMXYpJSUljQwZKWnL7e48o4MZqcOtY0ghcAajI2if7nnGdKXIF5djdSLwSWHx9e4=,iv:Alb7j8UVmfLHmMzwBpJ9Aor+lffm3P3DAJaipvX6LH4=,tag:L4pTMxjA6dRhK6nl2Kokhw==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:n5rS499IeNmb7or1zApWyDa74HcFhCGv35zn1DsYED8lWGEnmSFMbOUFlKwa3cBWiGRyQ2UjGzdb7lmDmoNLNzd7JB4ARgI5DapVtj+ymn56VIK5GTBOsRzJgtT4lqh3vEu4Spuv290aBHHg+lsPfdTfsJuOgAr4PVgnNY3O6Exz6EZwOSSvbUX5if7G96Y1bemF2Hh1AbdOg5KvB/Uh6mBW17hnUnbqxT7O7fepYmsSLB/qjBJWVivoMtMQWoQL8IkOapmuQYDf8ig+2FMxzcAQ71wTpTlXLGJ/XAuFSx1tJzpLbFCOpvLtGnemoUIB8viBjgfyTqN5iboJvk/WX110609yx0ew/F45CfpBfjQ66iI1shiCidfPm3ZHCO7KSjXzXEmjX5dVFp3xpz58yJH8LwQtrIQB1OQsLKe2ZmrYfjydoDuN9N+A0a6qN6KJUvIGVxNKnQ1rjj8uq4toVhxsSLQrfNkTFPA1Q624rCOu04oz3wd/oPS28war85atfMjmz9JU+Hw5u5qaxgl7UJno1VYknsrrYDIAGPxUwO56t9kHIgOZpNuD6JqDBJAfGJ1oPT+zVIBcNwNg0Z577g0l9rCLNdKInVCVahxZLCVe3LidtxL/bNzvSYHb09RbkJIhdWn7ouYCHwRRWN5kWbUopm2hHVZjRhC2CGJLBZ3+aLkEJzyZGcQCLF6Jc+/659FD8dXdaqfbR/vEKaVhoLL/WGKeo/lZL6c7TaVwDSIIa7dkk/QvGvmWhO+qG8yIs4vciGa06I62P1Y+9DsMrrWu/yqLTSkWtOpKdvIBh/jPOC4t56JEb/ksIvDLKSdrtrtHyRdCkzZ95XQDAM/a2nv+LnH75Yz9q/R0Caex/OSATCFpNcoyeSJwC5sSs6nV/NJ18YynzclSaKmAvMM8XVx8jrtUEp1yIzgBogxTFJ+yTsvEa6y/yCF3i80RrJNIkOKJDR48g7R/RJTufR+YxQj+RjWpgHbkc/Uhb0cR3X5A0ptA2X6Qz+cW9NXr5dA3A4lVsFzkZLzulqPhCxT9+g8OINx2+ayNx/M=,iv:xy7UNpO48flvQE+otJfHUJLhZlOuUueXLP03cDigXFQ=,tag:HeWayOjoZmVa0dr8GPN7Iw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITjNQcHMweFY1ekNkdWFs\nak1wcm5td29BcTI1OFBtMnFXcTg4SEJlUTFZCjc1YlovcSsxOUNFWGIxZWVsRU1L\neS9wNE1rWU05WDdSZEQzbEZib2Yxa3cKLS0tICt4TjVTOG1JUENSNW8xNVlkZHkx\nem1EMEI3Skx5R3BmLzVRYjBkK1BRODQKfX1nexjFXyXxGBvDRlssjw3XcwvtGxGC\narZ2/kmF2nETLoNDVcNIxV5KUPohhTwWdlrmPGksquBrUCjElMOVLw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwMFZ0clBPaE1Xc1NGRTFK\nWlFGTWErdEtoSUhEeG1jWWNycGZsd1h0S0RrClQyQ1dRWGI4VzZRQXlvd3ErU00w\nTjNnQ2VJZEhpZ0RQdUxBUHpONjVNNjAKLS0tIGF1enFGWnhFL3pCM21nSTlhUUxs\naFQreFI2NzJmZVRZeVBxNVBQYVhTWEkKkGUPL0hOBAX2L7Vezq7Sf1V9Yu//X1/x\nNHndFsjTBcaFbDWCxcZpqVvJ0paKm46Nm+AQyhThR0OGCsKbRJhSlQ==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:NieKHtjtSL9NsDsFnIU5MN8pVp55r05Qu7Qf9Pde8W/60xkF1A7kML1zpbUbMfp05VS26aIENps8BOyDZhCJgZG/89NLXccGoJTKFLDUyKDqzwCPuEK+enbRm80VQnPpMLxRSMacCl/Qm3QxpQzLuasQvLH1c18BBhy+jpwuFBY=,iv:NbA65YXGIlkWXu9tmDHdvNL4xxlfp2FFFpjckJ86E80=,tag:XX6T/Hs8W0htQyp7DJhahQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -0,0 +1 @@
LFXPW4H-IIDB5K4-SZ54YJL-PVRW2Q7-52YPHEI-6VZQOQA-PSAVYPV-QF5BCQA

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:QnnXrKYiJMtNQlHBBjwZeosvLjcuMwWH+YjRwy3OeaPQ9gJbj2sAUrkoZSqc5jzi577nTympol92bGIRP51TKJ3k/i0cq7P/UyGIWCjPUi5jmJs3tY8rfztlcyiXr0TPYVYyZDp1ErPfFW96xWjfaSgopjScYGRS/NGkaRedQuP39Hwg4rIPpdrkEbPgVIMCHJdvMcPm2a2RmqX1PzoBj+TEQMCFxyuRg2cTVoqyyYv95w19T+5GddvbVDmB9c8OhG4VS9nqfgdL0IJ3TMUnJAC/S1wfj64YFdhrt7lzR00xriht19ZS+HPhLlBjSruZXa4SrSEtdIMfTAXlVwtw5e/LENhiKumyhmUMqlPRJPANbgZTxa6g1Wq3NmGIOQB6,iv:2I5JcaYbEA4W9zLWTTR4j4I1hREDqb7Nh4Wg9AVEaHE=,tag:WiJhtiPjTMN+CgJU+KNmgw==,type:str]",
"sops": {
"age": [
{
"recipient": "age1mq8u3x9z8v3zdm59qslxyn33zm0rpjzrd9sr9fjzqwp8d66t9czq626xsd",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL1pYVmdUclF3dXhlTjJ5\nRFl1WXRrWUwwYUwxMnNvdXdCbFVZRmp0bDFvCnRNd3p1MzV3WHMxUFlkdUt6SHJj\nTHhSYTkzcGZqNnRWMFBPTTVmNGJaa00KLS0tIHNtWkxzdkEwelVqdzRHM2lqT05H\nN1EvdDgrOG5OUXEvdTlwMDJMekdFa3MKzlxMUKl97RGAcilKQuxC34Pzpr9qPsG9\nS5qhziP2xOn+6rXO21/klVQ1pJDZZcsVI+fCFPHTbwJvXvS6VNuLCg==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXL3ZCelVSd1ZxTXE3TEFs\ndmdEejl2bE5CTXhqVXZnbEVOOUlaYzhNMnlVClJ5K0hmMXVBTzJUVnVybHY0WCtw\na2tnUm5jR0xGN0M0SHVSSzVvejRZQ1EKLS0tIFNnS2EweEs2V3VXVnA5T0hQVWNq\nREFZckQ5cStZamZBdmZ2WHNzUzdQa0kK6774xhCCzaODxymwg2B+3Y6aRoXyBW1G\n7jTYWEsnf9eUCcBZP+jexT3obituZENtque5Ov1zblaGIBYnxi87Fg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:27Z",
"mac": "ENC[AES256_GCM,data:HXcgcj09lBf9NPX5htqHZuU3yLKkKMt+RZtABLPFtzsZzXV6nE/ajlHm6+xI7Fo36u1b3KN9QfQ7i70LP2UKUf1L/JTVZiExkDmcfJCluCaq+/bDpY54ICw1jLZ0ckXgMHSTNwJbvM/UbvqdcB0cCt6MzxRUREHjTW4wk08qpPM=,iv:n24kpaCvODWE6+0cjo5fHCkFYHbz/zSjj9wqbVpd/dg=,tag:9x8wboXQB/YrMUU6L8jXqQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:MMjnYNzHE7QSxrXY7aN8XSFkFwK/lpRhL4qV0vn6cwyn,iv:kB8m6ZhkNta6Y+gsYufvo45B+Q5lHuJBmYN3sk7ALKE=,tag:zbgynC6xhrFGcq+mE2pftA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4S2FEd2R1Z2hUU3BtSGdZ\nS2I3NjRrNHhGaW9SL3Q3bkJuMFNlM25ya0dNCmhpVmk0eUpWWkpkQWErRUJKdks2\nSzUvMnVQTVJVbC9WeWpkd3VHandrNW8KLS0tIFFWOUlIZFlkYi9NSjJpbXNNWE9I\nMEYxeGJEcGNLaHJOSmR2TytaWDBtaVEKhEfTi3ESLw6I2Uu3ejCGWHMv3LmRvjbg\nZ30CoUmktB7z5/RbdwwiFyV+ijuNq0RPxrlBX0VGEkd/+BrIY4BYxQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwSXpGRkIzU0hwaXBaYXdM\nNTdnZmJTSmVzbVVWZHNCd1VSNjQ4OGdzTkFzClh2Vnl1RWlLbnE0YnVsYjNvZ2hs\nOE04a1QxM0pZVUsvMkpPdlA0Tjdpb28KLS0tIEpMVmFsTVpFcVVvcDdRVk5xZFdS\nMXBTdkt4SlV5UnJTcG4wQU11YjZrS1UKcLyVL6PknecAOLEhjYYbZ38+e+g5jXFd\nJcu7fHEs5O464vgM58SKJQ5m77rGzwqRGh6MjqJTfAl8nPbvw0t2cg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:957Dud8YuagJRf8I0pAe4Tpk+En4ac99TAjRVv5eYAQtyDR1iHUwu/NIc0I14wZj7bKP6UqbOntST3jl33xhM2NQGG/FskJppsbS355OirpVfNahqlhcy0Jy5p1L9TikY89SlFgO6kx2eYs67inbk3UXnyeVxAcEIq6qnmh0u60=,iv:rfLaPYlcnBLecIYTIqJbaI2DNERS1MfU1C+7xH51kwY=,tag:5lQ1Ouf40MyB/Q3B6EwMzg==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:65FnMTSkJxWZrik97mTIIZBH7Y+8bxTlV/wIA2jTV96oQebGWN6kI3lkR562e9ZVoiB5xyTQayU+NZhgIFEzyC5xAnXmLnKlZ2cvcjDJ51TlAujoHD5+R36tb6FrsBkuki3UI/v0xeWUhJSKpIoPL3/1Tq4l8EWVRR/JTf8fuC06HxwyiXJeMU3CkLpbt/txL2VGM9nNov+CbEZL+WJ99kCuRQ/QPAVglj0AulttXBPztNnUtPZBnBZzbFpxE4nRZ7ufjweVrRYgqYNj1CT0gdOJnyId9uu7vkHpZvGiwB3CaEeG3VyOGidfocmC1Yq/bQw26djx6npuiKoXPEgJmyaoi9DPuOvrvMwuYMpuueZCM7+54fbk7VTKusRtp0iOddSP1pevg1zM/w3NjRnnzgicahzxRf2EEGBRea9P4/znmPMHdbylXp1mnuqGpYZEteweOyFQFkjEmjIASI/ZI04JSgTGs37D3WrpD+Utjh/nVOtmeFHbeqbhk7vngDf++PW9nNM3xNhsALhd1w313agzUffQjRI96zXGj4SkUoPXkO9v9uPY+bZUxP4VncO4FbgxcEkO9YS57LiLp56YUzC/72Z30zsLrCnl2YZTXQ8nc7fvFVqfl61HmUC7xaEckQ0YXxH1yXQj+YTsk1qOVY/AyKjjq33j96de0i4AxYCEgZGU0pTkbT92MNezs7Atz066YIi1YC9YsMMMOXdtlz0FvQIN0WTzSqo1AtuyAUXo+otBX9IeMzX3hQ++E7CuHDMjR+I+jwwouAlrNCcn1Ean48cj6UkhNFHem/fysDL/2OYUZqeUfB7Qn8r0Ffewo55/87J0o2+8zlxdEOWPYiKOsT91G8Lq77OaadZCGFnNWlBrzqUkt+TuUa7UM9ok8uyZLX4R+f+hvIuvp+T+IAcz6r0R+1ndHai59r5gfgBeFxB5qnTPYTgujcdAiEeqfg7Dx0DPKEBiIj5awgsdd057oVf+eEY4Cu/mXuKb70TlbgEvelNmB5MRDlWTRm6ybrbnb7NfPmt5UYaTcPX6l/kP5ek95Q==,iv:uAE/AVleWSNp6kY/anvkJOeUqlfiuAgtHBgry6IqcJc=,tag:+J8jTvKqEULP8GW3IUqyTQ==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqdmdzU3Y2V1MzaE51a2Fj\nbFBIUWF3eXJrTUhKVW1tYzFSeGpad1BwRDBvCk96Z1lIMDg0ZnMrNnJkUzA4VXJ1\nZEtzbGNJU3ZzVmFYMzRBNmo5TnEybUkKLS0tIHErQ2dYZlVuckdML3Job3dwM0hM\nVEVWek40Y0F0bStzd09XenhWaVFIYVkKToXp2wUPBe9LtuLMlBPfpQRq4WHLbnSt\nueOxJvflzQveUhdpuHi6IbtE9HyCwtn6joeOnBTNFxt12Dy/s247Aw==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3bGpQQ1N0cmRmcURGV3hq\nUGdHOWh0S1VIQkQ2aFFVNXVnZmpFc050S240Ck03Z1NXNDVISTRvNnhkVnFZdzg0\nVFlXZ1g4citlV21PZ2lEbmRhMFhJaUUKLS0tIDIyTXZNTWpIUFhSYXR4VXBUN284\nYTFqWkQza0V3M05lZDlBQzBtWWFlaTAKfJViXG1KULBXTmDC4PLSWRy9LAVchE47\n1rBtT+r/KlbnOTYYEo7zCMagkPhDdPD5WSegmtghUGkXRtqaTZcTBA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:HsnTHpXkyqJMlpbKzHvBH7bxg+ssDwXzjA7JTdpioP74gD8u+oPTHJOlgYI4Qbvr04JbZiRy/cko3BTUXj8C6nCV5gyXhDG3O0rkAGd/v3108ff7WiOWbXpNJ5QhLBnG4Ny1/ITfzu7MQtNkvGFnoktyMtvqHkVeJf5IhOuT6Fo=,iv:T1IYJyi8eXGdVE/JQSvyoyU/Iw/RlpO4TQCx1naZXXI=,tag:Z3J/313RG337U9HdXidrCA==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -0,0 +1 @@
5N6ZTHC-FZKLOY7-DJCORO3-BG3C4Q3-KD3PCMP-2DM7EHQ-2FS25FK-DCOTSAI

View File

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

View File

@@ -0,0 +1,19 @@
{
"data": "ENC[AES256_GCM,data:rDPNgUOmTINCoQnTQMjEXXrkvIYT7Ju6tX56aUA3cjlRGSzGmJb3Vmec58RS2QHmuOPUykydPTuAHxKs+SNZoXbdzDn75gAjS+eGNXGB9H9TFS2+iLpaSC5XTnfSdyzuGVyPRNvceI/C8FIvEauEpJ9dbS2kkTFTD15u2iI6zTmm/Mtvy2C18BwwEtu+m1TWB90oVduX05rrdnxoMjtscBCKR8fz6CbqYrbnpag9AUGUk8Z+cIMYGlSjDRlyaLWkh33aV/TWx/QOzfAWCyCVLMnU500ch0c6l2fg8daKDJJas2jHxo8KnlBIP2b6SLrcEKtlOSaPicizlJ9UDilDqHQfbucEJTgkQ2eEwJWSmXhU3q3ttGm3mxhQgej5FeIW,iv:6P1hFhDmHs4IYeyxVA/lYu2OL0ciaZNPEmMdBudacds=,tag:9MjdSKKjUO3VlTh/QmAPtA==,type:str]",
"sops": {
"age": [
{
"recipient": "age18mp7v3ck9eg0vqy3y83z5hdhum9rc77ntwal42p30udejkzvky2qjluy4x",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUWYyQXVUMUMwaUt4S3RO\nYkNzd3FxU29qYk1wWVlrTUNPK0lKSTU2RFJrCmZaeGg5Nlg5azhaRSs5SzVDZlpV\nKzJuUmlSekYrdzdGQ3JDejNwUmZ3cWMKLS0tIFhGT29pUC9wNlpGaHBMSHROV0lP\nNXNjQytQVDlOQmI1UThUTm5Qd1BmNmsKlvG3ZENmjPUDHRUB2o4RTCueIdtXZjxU\nz3Xg/RWZjaDInneg0z/jXyhKoqLRk1J0h9f/36CHT7RtVHIjtz2KIQ==\n-----END AGE ENCRYPTED FILE-----\n"
},
{
"recipient": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3MWovZlBoVk1HdUhhSXNy\nYXgxZVdIaDhqMTk0dFRCVFptaUE0ZmZ4ZHlBClpDNS85a2U0WE12TXVzOFhDcTRM\nWG1yU0dDaS9pUWVUT3pnWXNxRjlSVmcKLS0tIGxiTWVIREE4SlQ0Mm8xQW0ycnB2\na2Q4WGY2eDl0UGVTdXo1bWhMQjdNNzgKorezVfxaH/Kx3lpX57zF/0gAeCu2NzWX\nAm7RAFeHDJ+w0jIlZa2RM6mXOTKDzaF+h4j27GcLLbXsSRNhlFMqvg==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2025-07-22T13:55:39Z",
"mac": "ENC[AES256_GCM,data:w9GKY762/ikgNU0ypIOzlnXZqnW3tN1Fz+cVCsgPMQhZbf2OIb5zHgQAcZMHJII5+WqPxLE+PThhZ2hstuz0hvvqXF37KvuT5MblcGShCNOCTJiVwLVhhEUQl3c3C9iXKV+A/aG9psagjHAlhxsdv1UKqVPhQ0bgrl92EZZPdwo=,iv:3XuVGnMu2tOLvkjY8G78qk+mJWZShRsgWx7LwU3qaCI=,tag:+tQcEjZttN9cPxAOer4IoQ==,type:str]",
"unencrypted_suffix": "_unencrypted",
"version": "3.10.2"
}
}

View File

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

View File

@@ -134,9 +134,9 @@
systemd.services.zerotier-inventory-autoaccept =
let
machines = uniqueStrings (
(lib.optionals (roles ? moon) (lib.attrNames roles.moon.machines))
++ (lib.optionals (roles ? controller) (lib.attrNames roles.controller.machines))
++ (lib.optionals (roles ? peer) (lib.attrNames roles.peer.machines))
(lib.attrNames roles.moon.machines)
++ (lib.attrNames roles.controller.machines)
++ (lib.attrNames roles.peer.machines)
);
networkIps = builtins.foldl' (
ips: name:

View File

@@ -32,33 +32,6 @@ let
};
};
}).config;
testFlakeNoMoon =
(clanLib.clan {
self = { };
directory = ./vm;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.sara = {
nixpkgs.hostPlatform = "x86_64-linux";
};
machines.bam = {
nixpkgs.hostPlatform = "x86_64-linux";
};
modules.zerotier = module;
inventory.instances = {
zerotier = {
module.name = "zerotier";
module.input = "self";
roles.peer.tags.all = { };
roles.controller.machines.bam = { };
};
};
}).config;
in
{
test_peers = {
@@ -100,30 +73,4 @@ in
networkName = "zerotier";
};
};
test_peers_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.jon.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.jon.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = false;
networkName = "zerotier";
};
};
test_controller_no_moon = {
expr = {
hasNetworkIds = testFlakeNoMoon.nixosConfigurations.bam.config.services.zerotierone.joinNetworks;
isController =
testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.controller.enable;
networkName = testFlakeNoMoon.nixosConfigurations.bam.config.clan.core.networking.zerotier.name;
};
expected = {
hasNetworkIds = [ "0e28cb903344475e" ];
isController = true;
networkName = "zerotier";
};
};
}

View File

@@ -48,80 +48,62 @@ nav:
- Home: index.md
- Guides:
- Getting Started:
- Creating Your First Clan: guides/getting-started/index.md
- Create USB Installer: guides/getting-started/installer.md
- Add Machines: guides/getting-started/add-machines.md
- Add User: guides/getting-started/add-user.md
- Add Services: guides/getting-started/add-services.md
- Deploy Machine: guides/getting-started/deploy.md
- Continuous Integration: guides/getting-started/check.md
- Using Services: guides/clanServices.md
- Backup & Restore: guides/backups.md
- 🚀 Creating Your First Clan: guides/getting-started/index.md
- 📀 Create USB Installer (optional): guides/getting-started/installer.md
- ⚙️ Add Machines: guides/getting-started/add-machines.md
- ⚙️ Add User: guides/getting-started/add-user.md
- ⚙️ Add Services: guides/getting-started/add-services.md
- 🔐 Secrets & Facts: guides/getting-started/secrets.md
- 🚢 Deploy Machine: guides/getting-started/deploy.md
- 🧪 Continuous Integration: guides/getting-started/check.md
- clanServices: guides/clanServices.md
- Disk Encryption: guides/disk-encryption.md
- Age Plugins: guides/age-plugins.md
- Secrets management: guides/secrets.md
- Mesh VPN: guides/mesh-vpn.md
- Backup & Restore: guides/backups.md
- Vars Backend: guides/vars-backend.md
- Facts Backend: guides/secrets.md
- Adding more machines: guides/more-machines.md
- Target Host: guides/target-host.md
- Zerotier VPN: guides/mesh-vpn.md
- Inventory:
- Inventory: guides/inventory.md
- Secure Boot: guides/secure-boot.md
- Flake-parts: guides/flake-parts.md
- macOS: guides/macos.md
- Authoring:
- clanService: guides/authoring/clanServices/index.md
- Disk Template: guides/authoring/templates/disk/disko-templates.md
- clanModule: guides/authoring/clanModules/index.md
- Contributing:
- Contributing: guides/contributing/CONTRIBUTING.md
- Contribute: guides/contributing/CONTRIBUTING.md
- Debugging: guides/contributing/debugging.md
- Testing: guides/contributing/testing.md
- Writing a Service Module: guides/services/community.md
- Writing a Disko Template: guides/disko-templates/community.md
- Migrations:
- Migrate existing Flakes: guides/migrations/migration-guide.md
- Migrate inventory Services: guides/migrations/migrate-inventory-services.md
- Facts Vars Migration: guides/migrations/migration-facts-vars.md
- Disk id: guides/migrations/disk-id.md
- Concepts:
- Inventory: concepts/inventory.md
- Generators: concepts/generators.md
- Autoincludes: concepts/autoincludes.md
- macOS: guides/macos.md
- Reference:
- Overview: reference/index.md
- Clan Options: options.md
- Services:
- Overview:
- reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- API: reference/clanServices/clan-service-author-interface.md
- CLI:
- Overview: reference/cli/index.md
- reference/cli/backups.md
- reference/cli/facts.md
- reference/cli/flakes.md
- reference/cli/flash.md
- reference/cli/machines.md
- reference/cli/select.md
- reference/cli/secrets.md
- reference/cli/show.md
- reference/cli/ssh.md
- reference/cli/state.md
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- Modules (deprecated):
- Overview: reference/clanServices/index.md
- reference/clanServices/admin.md
- reference/clanServices/borgbackup.md
- reference/clanServices/data-mesher.md
- reference/clanServices/emergency-access.md
- reference/clanServices/garage.md
- reference/clanServices/hello-world.md
- reference/clanServices/importer.md
- reference/clanServices/mycelium.md
- reference/clanServices/packages.md
- reference/clanServices/sshd.md
- reference/clanServices/state-version.md
- reference/clanServices/syncthing.md
- reference/clanServices/trusted-nix-caches.md
- reference/clanServices/users.md
- reference/clanServices/wifi.md
- reference/clanServices/zerotier.md
- Interface for making Services: reference/clanServices/clan-service-author-interface.md
- Modules:
- Overview: reference/clanModules/index.md
- reference/clanModules/frontmatter/index.md
# TODO: display the docs of the clan.service modules
@@ -154,7 +136,6 @@ nav:
- reference/clanModules/static-hosts.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing-static-peers.md
- reference/clanModules/syncthing.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
@@ -164,20 +145,38 @@ nav:
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zerotier.md
- reference/clanModules/zt-tcp-relay.md
- CLI:
- Overview: reference/cli/index.md
- clan.core (NixOS Options):
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Developer-api: api.md
- reference/cli/backups.md
- reference/cli/facts.md
- reference/cli/flakes.md
- reference/cli/flash.md
- reference/cli/machines.md
- reference/cli/select.md
- reference/cli/secrets.md
- reference/cli/show.md
- reference/cli/ssh.md
- reference/cli/state.md
- reference/cli/templates.md
- reference/cli/vars.md
- reference/cli/vms.md
- NixOS Modules:
- clan.core:
- Overview: reference/clan.core/index.md
- reference/clan.core/backups.md
- reference/clan.core/deployment.md
- reference/clan.core/facts.md
- reference/clan.core/networking.md
- reference/clan.core/settings.md
- reference/clan.core/sops.md
- reference/clan.core/state.md
- reference/clan.core/vars.md
- Nix API:
- clan: reference/nix-api/clan.md
- Inventory: reference/nix-api/inventory.md
- Glossary: reference/glossary.md
- Decisions:
- Architecture Decisions: decisions/README.md
- 01-clanModules: decisions/01-ClanModules.md
@@ -186,7 +185,10 @@ nav:
- 04-fetching-nix-from-python: decisions/04-fetching-nix-from-python.md
- 05-deployment-parameters: decisions/05-deployment-parameters.md
- Template: decisions/_template.md
- Glossary: reference/glossary.md
- Options: options.md
- Developer:
- Introduction: intern/index.md
- API: intern/api.md
docs_dir: site
site_dir: out
@@ -197,7 +199,6 @@ theme:
favicon: https://clan.lol/favicon.svg
name: material
features:
- navigation.footer
- navigation.instant
- navigation.tabs
- navigation.tabs.sticky
@@ -245,6 +246,3 @@ plugins:
- search
- macros
- redoc-tag
- redirects:
redirect_maps:
guides/getting-started/secrets.md: concepts/generators.md

View File

@@ -40,7 +40,6 @@ pkgs.stdenv.mkDerivation {
mkdocs-material
mkdocs-macros
mkdocs-redoc-tag
mkdocs-redirects
]);
configurePhase = ''
pushd docs
@@ -55,7 +54,6 @@ pkgs.stdenv.mkDerivation {
chmod -R +w ./site/reference
echo "Generated API documentation in './site/reference/' "
rm -r ./site/options-page || true
cp -r ${docs-options} ./site/options-page
chmod -R +w ./site/options-page

View File

@@ -114,6 +114,9 @@
in
{
options = {
_ = mkOption {
type = types.raw;
};
instances.${name} = lib.mkOption {
inherit description;
type = types.submodule {
@@ -146,29 +149,20 @@
};
};
docModules = [
{
inherit self;
}
self.modules.clan.default
{
options.inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
{ noInstanceOptions = true; }
] ++ mapAttrsToList fakeInstanceOptions serviceModules;
};
};
}
];
mkScope = name: modules: {
inherit name;
modules = [
{
_module.args = { inherit clanLib; };
_file = "docs mkScope";
}
{ noInstanceOptions = true; }
../../../lib/modules/inventoryClass/interface.nix
] ++ mapAttrsToList fakeInstanceOptions modules;
urlPrefix = "https://github.com/nix-community/dream2nix/blob/main/";
};
in
{
# Uncomment for debugging
# legacyPackages.docModules = lib.evalModules {
# modules = docModules;
# };
packages = lib.optionalAttrs ((privateInputs ? nuschtos) || (inputs ? nuschtos)) {
docs-options =
(privateInputs.nuschtos or inputs.nuschtos)
@@ -177,13 +171,7 @@
inherit baseHref;
title = "Clan Options";
# scopes = mapAttrsToList mkScope serviceModules;
scopes = [
{
name = "Clan";
modules = docModules;
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
}
];
scopes = [ (mkScope "Clan Inventory" serviceModules) ];
};
};
};

View File

@@ -193,7 +193,7 @@ def module_header(module_name: str, has_inventory_feature: bool = False) -> str:
def module_nix_usage(module_name: str) -> str:
return f"""## Usage via Nix
**This module can be also imported directly in your nixos configuration. Although it is recommended to use the [inventory](../../concepts/inventory.md) interface if available.**
**This module can be also imported directly in your nixos configuration. Although it is recommended to use the [inventory](../../reference/nix-api/inventory.md) interface if available.**
Some modules are considered 'low-level' or 'expert modules' and are not available via the inventory interface.
@@ -373,7 +373,7 @@ This module can be used via predefined roles
"""
Every role has its own configuration options, which are each listed below.
For more information, see the [inventory guide](../../concepts/inventory.md).
For more information, see the [inventory guide](../../guides/inventory.md).
??? Example
For example the `admin` module adds the following options globally to all machines where it is used.
@@ -402,7 +402,7 @@ certain option types restricted to enable configuration through a graphical
interface.
!!! note "🔹"
Modules with this indicator support the [inventory](../../concepts/inventory.md) feature.
Modules with this indicator support the [inventory](../../guides/inventory.md) feature.
"""
@@ -679,6 +679,86 @@ def build_option_card(module_name: str, frontmatter: Frontmatter) -> str:
return f"{to_md_li(module_name, frontmatter)}\n\n"
def produce_build_clan_docs() -> None:
if not BUILD_CLAN_PATH:
msg = f"Environment variables are not set correctly: BUILD_CLAN_PATH={BUILD_CLAN_PATH}. Expected a path to the optionsJSON"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
output = """# Clan
This provides an overview of the available arguments of the `clan` interface.
Each attribute is documented below
- **clan-core.lib.clan**: A function that takes an attribute set.
??? example "clan Example"
```nix
clan {
self = self;
machines = {
jon = { };
sara = { };
};
};
```
- **clan with flake-parts**: Import the FlakeModule
After importing the FlakeModule you can define your `clan` as a flake attribute
All attribute can be defined via `clan.*`
Further information see: [flake-parts](../../guides/flake-parts.md) guide.
??? example "flake-parts Example"
```nix
flake-parts.lib.mkFlake { inherit inputs; } ({
systems = [];
imports = [
clan-core.flakeModules.default
];
clan = {
machines = {
jon = { };
sara = { };
};
};
});
```
"""
with Path(BUILD_CLAN_PATH).open() as f:
options: dict[str, dict[str, Any]] = json.load(f)
split = split_options_by_root(options)
for option_name, options in split.items():
# Skip underscore options
if option_name.startswith("_"):
continue
# Skip inventory sub options
# Inventory model has its own chapter
if option_name.startswith("inventory."):
continue
print(f"[build_clan_docs] Rendering option of {option_name}...")
root = options_to_tree(options)
for option in root.suboptions:
output += options_docs_from_tree(option, init_level=2)
outfile = Path(OUT) / "nix-api/clan.md"
outfile.parent.mkdir(parents=True, exist_ok=True)
with Path.open(outfile, "w") as of:
of.write(output)
def split_options_by_root(options: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""
Split the flat dictionary of options into a dict of which each entry will construct complete option trees.
@@ -725,7 +805,7 @@ Typically needed by module authors to define roles, behavior and metadata for di
!!! Note
This is not a user-facing documentation, but rather meant as a reference for *module authors*
See: [clanService Authoring Guide](../../guides/services/community.md)
See: [clanService Authoring Guide](../../guides/authoring/clanServices/index.md)
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model
@@ -754,6 +834,48 @@ class Option:
suboptions: list["Option"] = field(default_factory=list)
def produce_inventory_docs() -> None:
if not BUILD_CLAN_PATH:
msg = f"Environment variables are not set correctly: BUILD_CLAN_PATH={BUILD_CLAN_PATH}. Expected a path to the optionsJSON"
raise ClanError(msg)
if not OUT:
msg = f"Environment variables are not set correctly: $out={OUT}"
raise ClanError(msg)
output = """# Inventory
This provides an overview of the available attributes of the `inventory` model.
It can be set via the `inventory` attribute of the [`clan`](./clan.md#inventory) function, or via the [`clan.inventory`](./clan.md#inventory) attribute of flake-parts.
"""
# Inventory options are already included under the clan attribute
# We just omitted them in the clan docs, because we want a separate output for the inventory model
with Path(BUILD_CLAN_PATH).open() as f:
options: dict[str, dict[str, Any]] = json.load(f)
clan_root_option = options_to_tree(options)
# Find the inventory options
inventory_opt: None | Option = None
for opt in clan_root_option.suboptions:
if opt.name == "inventory":
inventory_opt = opt
break
if not inventory_opt:
print("No inventory options found.")
exit(1)
# Render the inventory options
# This for loop excludes the root node
for option in inventory_opt.suboptions:
output += options_docs_from_tree(option, init_level=2)
outfile = Path(OUT) / "nix-api/inventory.md"
outfile.parent.mkdir(parents=True, exist_ok=True)
with Path.open(outfile, "w") as of:
of.write(output)
def option_short_name(option_name: str) -> str:
parts = option_name.split(".")
short_name = ""
@@ -862,6 +984,9 @@ def options_docs_from_tree(
if __name__ == "__main__": #
produce_clan_core_docs()
produce_build_clan_docs()
produce_inventory_docs()
produce_clan_service_author_docs()
produce_clan_modules_docs()

View File

@@ -1,15 +0,0 @@
Clan automatically imports the following files from a directory and registers them.
## Machine registration
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
!!! info "Automatically loaded files"
The following files are loaded automatically for each Clan machine:
- [x] `machines/{machineName}/configuration.nix`
- [x] `machines/{machineName}/hardware-configuration.nix`
- [x] `machines/{machineName}/facter.json` Automatically configured, for further information see [nixos-facter](https://clan.lol/blog/nixos-facter/)
- [x] `machines/{machineName}/disko.nix` Automatically loaded, for further information see the [disko docs](https://github.com/nix-community/disko/blob/master/docs/quickstart.md).

View File

@@ -6,8 +6,6 @@ Accepted
## Context
Current state as of writing:
To define a service in Clan, you need to define two things:
- `clanModule` - defined by module authors

View File

@@ -1,59 +0,0 @@
## Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -0,0 +1,229 @@
# Authoring a clanModule
!!! Danger "Will get deprecated soon"
Please consider twice creating new modules in this format
[`clan.service` module](../clanServices/index.md) will be the new standard soon.
This site will guide you through authoring your first module. Explaining which conventions must be followed, such that others will have an enjoyable experience and the module can be used with minimal effort.
!!! Tip
External ClanModules can be ad-hoc loaded via [`clan.inventory.modules`](../../../reference/nix-api/inventory.md#inventory.modules)
## Bootstrapping the `clanModule`
A ClanModule is a specific subset of a [NixOS Module](https://nix.dev/tutorials/module-system/index.html), but it has some constraints and might be used via the [Inventory](../../../guides/inventory.md) interface.
In fact a `ClanModule` can be thought of as a layer of abstraction on-top of NixOS and/or other ClanModules. It may configure sane defaults and provide an ergonomic interface that is easy to use and can also be used via a UI that is under development currently.
Because ClanModules should be configurable via `json`/`API` all of its interface (`options`) must be serializable.
!!! Tip
ClanModules interface can be checked by running the json schema converter as follows.
`nix build .#legacyPackages.x86_64-linux.schemas.inventory`
If the build succeeds the module is compatible.
## Directory structure
Each module SHOULD be a directory of the following format:
```sh
# Example: borgbackup
clanModules/borgbackup
├── README.md
└── roles
├── client.nix
└── server.nix
```
!!! Tip
`README.md` is always required. See section [Readme](#readme) for further details.
The `roles` folder is strictly required for `features = [ "inventory" ]`.
## Registering the module
=== "User module"
If the module should be ad-hoc loaded.
It can be made available in any project via the [`clan.inventory.modules`](../../../reference/nix-api/inventory.md#inventory.modules) attribute.
```nix title="flake.nix"
# ...
# Sometimes this attribute set is defined in clan.nix
clan-core.lib.clan {
# 1. Add the module to the available clanModules with inventory support
inventory.modules = {
custom-module = ./modules/my_module;
};
# 2. Use the module in the inventory
inventory.services = {
custom-module.instance_1 = {
roles.default.machines = [ "machineA" ];
};
};
};
```
=== "Upstream module"
If the module will be contributed to [`clan-core`](https://git.clan.lol/clan-core)
The clanModule must be registered within the `clanModules` attribute in `clan-core`
```nix title="clanModules/flake-module.nix"
--8<-- "clanModules/flake-module.nix:0:5"
# Register our new module here
# ...
```
## Readme
The `README.md` is a required file for all modules. It MUST contain frontmatter in [`toml`](https://toml.io) format.
```markdown
---
description = "Module A"
---
This is the example module that does xyz.
```
See the [Full Frontmatter reference](../../../reference/clanModules/frontmatter/index.md) further details and all supported attributes.
## Roles
If the module declares to implement `features = [ "inventory" ]` then it MUST contain a roles directory.
Each `.nix` file in the `roles` directory is added as a role to the inventory service.
Other files can also be placed alongside the `.nix` files
```sh
└── roles
├── client.nix
└── server.nix
```
Adds the roles: `client` and `server`
??? Tip "Good to know"
Sometimes a `ClanModule` should be usable via both clan's `inventory` concept but also natively as a NixOS module.
> In the long term, we want most modules to implement support for the inventory,
> but we are also aware that there are certain low-level modules that always serve as a backend for other higher-level `clanModules` with inventory support.
> These modules may not want to implement inventory interfaces as they are always used directly by other modules.
This can be achieved by placing an additional `default.nix` into the root of the ClanModules directory as shown:
```sh
# ModuleA
├── README.md
├── default.nix
└── roles
└── default.nix
```
```nix title="default.nix"
{...}:{
imports = [ ./roles/default.nix ];
}
```
By utilizing this pattern the module (`moduleA`) can then be imported into any regular NixOS module via:
```nix
{...}:{
imports = [ clanModules.moduleA ];
}
```
## Adding configuration options
While we recommend to keep the interface as minimal as possible and deriving all required information from the `roles` model it might sometimes be required or convenient to expose customization options beyond `roles`.
The following shows how to add options to your module.
**It is important to understand that every module has its own namespace where it should declare options**
**`clan.{moduleName}`**
???+ Example
The following example shows how to register options in the module interface
and how it can be set via the inventory
```nix title="/default.nix"
custom-module = ./modules/custom-module;
```
Since the module is called `custom-module` all of its exposed options should be added to `options.clan.custom-module.*...*`
```nix title="custom-module/roles/default.nix"
{
options = {
clan.custom-module.foo = mkOption {
type = types.str;
default = "bar";
};
};
}
```
If the module is [registered](#registering-the-module).
Configuration can be set as follows.
```nix title="flake.nix"
# Sometimes this attribute set is defined in clan.nix
clan-core.lib.clan {
inventory.services = {
custom-module.instance_1 = {
roles.default.machines = [ "machineA" ];
roles.default.config = {
# All configuration here is scoped to `clan.custom-module`
foo = "foobar";
};
};
};
}
```
## Organizing the ClanModule
Each `{role}.nix` is included into the machine if the machine is declared to have the role.
For example
```nix
roles.client.machines = ["MachineA"];
```
Then `roles/client.nix` will be added to the machine `MachineA`.
This behavior makes it possible to split the interface and common code paths when using multiple roles.
In the concrete example of `borgbackup` this allows a `server` to declare a different interface than the corresponding `client`.
The client offers configuration option, to exclude certain local directories from being backed up:
```nix title="roles/client.nix"
# Example client interface
options.clan.borgbackup.exclude = ...
```
The server doesn't offer any configuration option. Because everything is set-up automatically.
```nix title="roles/server.nix"
# Example server interface
options.clan.borgbackup = {};
```
Assuming that there is a common code path or a common interface between `server` and `client` this can be structured as:
```nix title="roles/server.nix, roles/client.nix"
{...}: {
# ...
imports = [ ../common.nix ];
}
```

View File

@@ -1,16 +1,16 @@
# Authoring a 'clan.service' module
!!! Tip
This is the successor format to the older [clanModules](../../reference/clanModules/index.md)
This is the successor format to the older [clanModules](../clanModules/index.md)
While some features might still be missing we recommend to adapt this format early and give feedback.
## Service Module Specification
This section explains how to author a clan service module.
We discussed the initial architecture in [01-clan-service-modules](../../decisions/01-ClanModules.md) and decided to rework the format.
We discussed the initial architecture in [01-clan-service-modules](../../../decisions/01-ClanModules.md) and decided to rework the format.
For the full specification and current state see: **[Service Author Reference](../../reference/clanServices/clan-service-author-interface.md)**
For the full specification and current state see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
### A Minimal module
@@ -52,7 +52,7 @@ The imported module file must fulfill at least the following requirements:
}
```
For more attributes see: **[Service Author Reference](../../reference/clanServices/clan-service-author-interface.md)**
For more attributes see: **[Service Author Reference](../../../reference/clanServices/clan-service-author-interface.md)**
### Adding functionality to the module
@@ -266,6 +266,6 @@ The benefit of this approach is that downstream users can override the value of
## Further
- [Reference Documentation for Service Authors](../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../guides/migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../decisions/01-ClanModules.md)
- [Reference Documentation for Service Authors](../../../reference/clanServices/clan-service-author-interface.md)
- [Migration Guide from ClanModules to ClanServices](../../migrations/migrate-inventory-services.md)
- [Decision that lead to ClanServices](../../../decisions/01-ClanModules.md)

View File

@@ -1,199 +1,167 @@
# Introduction to Backups
This guide explains how to set up and manage
[BorgBackup](https://borgbackup.readthedocs.io/) for secure, efficient backups
in a clan network. BorgBackup provides:
When you're managing your own services, creating regular backups is crucial to ensure your data's safety.
This guide introduces you to Clan's built-in backup functionalities.
Clan supports backing up your data to both local storage devices (like USB drives) and remote servers, using well-known tools like borgbackup and rsnapshot.
We might add more options in the future, but for now, let's dive into how you can secure your data.
- Space efficient storage of backups with deduplication
- Secure, authenticated encryption
- Compression: lz4, zstd, zlib, lzma or none
- Mountable backups with FUSE
- Easy installation on multiple platforms: Linux, macOS, BSD, …
- Free software (BSD license).
- Backed by a large and active open-source community.
## Backing Up Locally with Localbackup
## Borgbackup Example
Localbackup lets you backup your data onto physical storage devices connected to your computer,
such as USB hard drives or network-attached storage. It uses a tool called rsnapshot for this purpose.
### Setting Up Localbackup
1. **Identify Your Backup Device:**
First, figure out which device you'll use for backups. You can see all connected devices by running this command in your terminal:
```bash
lsblk --output NAME,PTUUID,FSTYPE,SIZE,MOUNTPOINT
```
Look for the device you intend to use for backups and note its details.
2. **Configure Your Backup Device:**
Once you've identified your device, you'll need to add it to your configuration.
Here's an example NixOS configuration for a device located at `/dev/sda2` with an `ext4` filesystem:
```nix
inventory.instances = {
borgbackup = {
module = {
name = "borgbackup";
input = "clan";
};
roles.client.machines."jon".settings = {
destinations."storagebox" = {
repo = "username@$hostname:/./borgbackup";
rsh = ''ssh -oPort=23 -i /run/secrets/vars/borgbackup/borgbackup.ssh'';
};
};
roles.server.machines = { };
{
fileSystems."/mnt/hdd" = {
device = "/dev/sda2";
fsType = "ext4";
options = [ "defaults" "noauto" ];
};
};
}
```
The input should be named according to your flake input. Jon is configured as a
client machine with a destination pointing to a Hetzner Storage Box.
Replace `/dev/sda2` with your device and `/mnt/hdd` with your preferred mount point.
To see a list of all possible options go to [borgbackup clan service](../reference/clanServices/borgbackup.md)
3. **Set Backup Targets:** Next, define where on your device you'd like the backups to be stored:
## Roles
```nix
{
clan.localbackup.targets.hdd = {
directory = "/mnt/hdd/backup";
mountpoint = "/mnt/hdd";
};
}
```
A Clan Service can have multiple roles, each role applies different nix config to the machine.
Change `/mnt/hdd` to the actual mount point you're using.
### 1. Client
4. **Create Backups:** To create a backup, run:
Clients are machines that create and send backups to various destinations. Each
client can have multiple backup destinations configured.
```bash
clan backups create mymachine
```
### 2. Server
This command saves snapshots of your data onto the backup device.
Servers act as backup repositories, receiving and storing backups from client
machines. They can be dedicated backup servers within your clan network.
5. **Listing Backups:** To see available backups, run:
## Backup destinations
```bash
clan backups list mymachine
```
This service allows you to perform backups to multiple `destinations`.
Destinations can be:
## Remote Backups with Borgbackup
- **Local**: Local disk storage
- **Server**: Your own borgbackup server (using the `server` role)
- **Third-party services**: Such as Hetzner's Storage Box
### Overview of Borgbackup
## State management
Borgbackup splits the backup process into two parts: a backup client that sends data to a backup server.
The server stores the backups.
Backups are based on [states](../reference/clan.core/state.md). A state
defines which files should be backed up and how these files are obtained through
pre/post backup and restore scripts.
### Setting Up the Borgbackup Client
Here's an example for a user application `linkding`:
1. **Specify Backup Server:**
In this example:
- `/data/podman/linkding` is the application's data directory
- `/var/backup/linkding` is the staging directory where data is copied for
backup
Start by indicating where your backup data should be sent. Replace `hostname` with your server's address:
```nix
clan.core.state.linkding = {
folders = [ "/var/backup/linkding" ];
preBackupScript = ''
export PATH=${
lib.makeBinPath [
config.systemd.package
pkgs.coreutils
pkgs.rsync
]
}
service_status=$(systemctl is-active podman-linkding)
if [ "$service_status" = "active" ]; then
systemctl stop podman-linkding
rsync -avH --delete --numeric-ids "/data/podman/linkding/" /var/backup/linkding/
systemctl start podman-linkding
fi
'';
postRestoreScript = ''
export PATH=${
lib.makeBinPath [
config.systemd.package
pkgs.coreutils
pkgs.rsync
]
}
service_status="$(systemctl is-active podman-linkding)"
if [ "$service_status" = "active" ]; then
systemctl stop podman-linkding
# Backup locally current linkding data
cp -rp "/data/podman/linkding" "/data/podman/linkding.bak"
# Restore from borgbackup
rsync -avH --delete --numeric-ids /var/backup/linkding/ "/data/podman/linkding/"
systemctl start podman-linkding
fi
'';
};
{
clan.borgbackup.destinations = {
myhostname = {
repo = "borg@backuphost:/var/lib/borgbackup/myhostname";
};
};
}
```
## Managing backups
2. **Select Folders to Backup:**
In this section we go over how to manage your collection of backups with the clan command.
Decide which folders you want to back up. For example, to backup your home and root directories:
### Listing states
```nix
{ clan.core.state.userdata.folders = [ "/home" "/root" ]; }
```
To see which files (`states`) will be backed up on a specific machine, use:
3. **Generate Backup Credentials:**
Run `clan facts generate <yourmachine>` to prepare your machine for backup, creating necessary SSH keys and credentials.
### Setting Up the Borgbackup Server
1. **Configure Backup Repository:**
On the server where backups will be stored, enable the SSH daemon and set up a repository for each client:
```nix
{
services.borgbackup.repos.myhostname = {
path = "/var/lib/borgbackup/myhostname";
authorizedKeys = [
(builtins.readFile (config.clan.core.settings.directory + "/machines/myhostname/facts/borgbackup.ssh.pub"))
];
};
}
```
Ensure the path to the public key is correct.
2. **Update Your Systems:** Apply your changes by running `clan machines update` to both the server and your client
### Managing Backups
- **Scheduled Backups:**
Backups are automatically performed nightly. To check the next scheduled backup, use:
```bash
systemctl list-timers | grep -E 'NEXT|borg'
```
- **Listing Backups:** To see available backups, run:
```bash
clan backups list mymachine
```
- **Manual Backups:** You can also initiate a backup manually:
```bash
clan backups create mymachine
```
- **Restoring Backups:** To restore a backup that has been listed by the list command (NAME):
```bash
clan backups restore [MACHINE] [PROVIDER] [NAME]
```
Example (Restoring a machine called `client` with the backup provider `borgbackup`):
```bash
clan backups restore client borgbackup [NAME]
```
The `backups` command is service aware and allows optional specification of the `--service` flag.
To only restore the service called `zerotier` on a machine called `controller` through the backup provider `borgbackup` use the following command:
```bash
clan state list jon
clan backups restore client borgbackup [NAME] --service zerotier
```
This will show all configured states for the machine `jon`, for example:
```text
· service: linkding
folders:
- /var/backup/linkding
preBackupCommand: pre-backup-linkding
postRestoreCommand: post-restore-linkding
· service: zerotier
folders:
- /var/lib/zerotier-one
```
### Creating backups
To create a backup of a machine (e.g., `jon`), run:
```bash
clan backups create jon
```
This will backup all configured states (`zerotier` and `linkding` in this
example) from the machine `jon`.
### Listing available backups
To see all available backups, use:
```bash
clan backups list
```
This will display all backups with their timestamps:
```text
storagebox::username@username.your-storagebox.de:/./borgbackup::jon-jon-2025-07-22T19:40:10
storagebox::username@username.your-storagebox.de:/./borgbackup::jon-jon-2025-07-23T01:00:00
storagebox::username@username.your-storagebox.de:/./borgbackup::jon-storagebox-2025-07-24T01:00:00
storagebox::username@username.your-storagebox.de:/./borgbackup::jon-storagebox-2025-07-24T06:02:35
```
### Restoring backups
For restoring a backup you have two options.
#### Full restoration
To restore all services from a backup:
```bash
clan backups restore jon borgbackup storagebox::u444061@u444061.your-storagebox.de:/./borgbackup::jon-storagebox-2025-07-24T06:02:35
```
#### Partial restoration
To restore only a specific service (e.g., `linkding`):
```bash
clan backups restore --service linkding jon borgbackup storagebox::u444061@u444061.your-storagebox.de:/./borgbackup::jon-storagebox-2025-07-24T06:02:35
```

View File

@@ -138,7 +138,7 @@ You can use services exposed by Clans core module library, `clan-core`.
You can also author your own `clanService` modules.
🔗 Learn how to write your own service: [Authoring a service](../guides/services/community.md)
🔗 Learn how to write your own service: [Authoring a clanService](../guides/authoring/clanServices/index.md)
You might expose your service module from your flake — this makes it easy for other people to also use your module in their clan.
@@ -154,6 +154,6 @@ You might expose your service module from your flake — this makes it easy for
## Whats Next?
* [Author your own clanService →](../guides/services/community.md)
* [Author your own clanService →](../guides/authoring/clanServices/index.md)
* [Migrate from clanModules →](../guides/migrations/migrate-inventory-services.md)
<!-- TODO: * [Understand the architecture →](../explanation/clan-architecture.md) -->

View File

@@ -27,7 +27,7 @@ inputs = {
## Import the Clan flake-parts Module
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](../options.md) available within `mkFlake`.
After updating your flake inputs, the next step is to import the Clan flake-parts module. This will make the [Clan options](../reference/nix-api/clan.md) available within `mkFlake`.
```nix
{

View File

@@ -6,7 +6,7 @@ Machines can be added using the following methods
- Editing machines/`machine_name`/configuration.nix (automatically included if it exists)
- `clan machines create` (imperative)
See the complete [list](../../concepts/autoincludes.md) of auto-loaded files.
See the complete [list](../../guides/more-machines.md#automatic-registration) of auto-loaded files.
## Create a machine

View File

@@ -41,7 +41,7 @@ To learn more: [Guide about clanService](../clanServices.md)
```
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
Or read [authoring/clanServices](../../guides/services/community.md) if you want to bring your own
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.

View File

@@ -57,7 +57,7 @@ For more information see [clanService/users](../../reference/clanServices/users.
Some people like to define a `users` folder in their repository root.
That allows to bind all user specific logic to a single place (`default.nix`)
Which can be imported into individual machines to make the user available on that machine.
Which can be imported into individual machines to make the user avilable on that machine.
```bash
.
@@ -107,7 +107,7 @@ We can use this property of clan services to bind a nixosModule to the user, whi
}
```
1. Type `path` or `string`: Must point to a separate file. Inlining a module is not possible
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
!!! Note "This is inspiration"
Our community might come up with better solutions soon.

View File

@@ -8,6 +8,7 @@ Now that you have created a machines, added some services and setup secrets. Thi
- [x] RAM > 2GB
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
## Physical Hardware
@@ -17,7 +18,7 @@ Steps:
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
- Note down a reachable ip address (*ipv4*, *ipv6* or *tor*)
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
---
@@ -168,7 +169,7 @@ Re-run the command with the correct disk:
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
```
Should now be successful
Should now be succesfull
```shellSession
Applied disk template 'single-disk' to machine 'jon'

View File

@@ -59,7 +59,7 @@ Enter a *name*, confirm with *enter*. A directory with that name will be created
## Explore the Project Structure
Take a look at all project files:
Take a lookg at all project files:
```bash
cd my-clan
@@ -125,10 +125,11 @@ To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix
You can continue with **any** of the following steps at your own pace:
- [x] [Install Nix & Clan CLI](./index.md)
- [x] [Initialize Clan](./index.md#add-clan-cli-to-your-shell)
- [x] [Initialize Clan](./index.md#initialize-your-project)
- [ ] [Create USB Installer (optional)](./installer.md)
- [ ] [Add Machines](./add-machines.md)
- [ ] [Add a User](./add-user.md)
- [ ] [Add Services](./add-services.md)
- [ ] [Configure Secrets](./secrets.md)
- [ ] [Deploy](./deploy.md) - Requires configured secrets
- [ ] [Setup CI (optional)](./check.md)

View File

@@ -0,0 +1,179 @@
Setting up secrets is **Required** for any *machine deployments* or *vm runs* - You need to complete the steps: [Create Admin Keypair](#create-your-admin-keypair) and [Add Your Public Key(s)](#add-your-public-keys)
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
This guide will walk you through:
- **Creating a Keypair for Your User**: Learn how to generate a keypair for `$USER` to securely control all secrets.
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
### Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
### Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Further: Using Age Plugins
If you wish to use a key generated using an [age plugin] as your admin key, extra care is needed.
You must **precede your secret key with a comment that contains its corresponding recipient**.
This is usually output as part of the generation process
and is only required because there is no unified mechanism for recovering a recipient from a plugin secret key.
Here is an example:
```title="~/.config/sops/age/keys.txt"
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
```
!!! note
The comment that precedes the plugin secret key need only contain the recipient.
Any other text is ignored.
In the example above, you can specify `# recipient: age1zdy...`, `# public: age1zdy....` or even
just `# age1zdy....`
You will need to add an entry into your `flake.nix` to ensure that the necessary `age` plugins
are loaded when using Clan:
```nix title="flake.nix"
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
# Sometimes this attribute set is defined in clan.nix
clan = clan-core.lib.clan {
inherit self;
meta.name = "myclan";
# Add Yubikey and FIDO2 HMAC plugins
# Note: the plugins listed here must be available in nixpkgs.
secrets.age.plugins = [
"age-plugin-yubikey"
"age-plugin-fido2-hmac"
];
machines = {
# elided for brevity
};
};
in
{
inherit (clan) nixosConfigurations nixosModules clanInternals;
# elided for brevity
};
}
```

View File

@@ -9,6 +9,8 @@ The inventory logic will automatically derive the modules and configurations to
The following tutorial will walk through setting up a Backup service where the terms `Service` and `Role` will become more clear.
See also: [Inventory API Documentation](../reference/nix-api/inventory.md)
!!! example "Experimental status"
The inventory implementation is not considered stable yet.
We are actively soliciting feedback from users.
@@ -17,7 +19,7 @@ The following tutorial will walk through setting up a Backup service where the t
## Prerequisites
- [x] [Add some machines](../guides/getting-started/add-machines.md) to your Clan.
- [x] [Add multiple machines](./more-machines.md) to your Clan.
## Services

View File

@@ -7,7 +7,7 @@ This guide explains how to manage macOS machines using Clan.
Currently, Clan supports the following features for macOS:
- `clan machines update` for existing [nix-darwin](https://github.com/nix-darwin/nix-darwin) installations
- Support for [vars](../concepts/generators.md)
- Support for [vars](../guides/vars-backend.md)
## Add Your Machine to Your Clan Flake

View File

@@ -1,7 +1,7 @@
# Migrating from using `clanModules` to `clanServices`
**Audience**: This is a guide for **people using `clanModules`**.
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../../guides/services/community.md)
If you are a **module author** and need to migrate your modules please consult our **new** [clanServices authoring guide](../authoring/clanServices/index.md)
## What's Changing?
@@ -35,37 +35,6 @@ services = {
};
```
### Complex Example: Multi-service Setup
```nix
# Old format
services = {
borgbackup.production = {
roles.server.machines = [ "backup-server" ];
roles.server.config = {
directory = "/var/backup/borg";
};
roles.client.tags = [ "backup" ];
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
};
zerotier.company-network = {
roles.controller.machines = [ "network-controller" ];
roles.moon.machines = [ "moon-1" "moon-2" ];
roles.peer.tags = [ "nixos" ];
};
sshd.internal = {
roles.server.tags = [ "nixos" ];
roles.client.tags = [ "nixos" ];
config.certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
```
---
## ✅ After: New `instances` Definition with `clanServices`
@@ -101,56 +70,6 @@ instances = {
};
```
### Complex Example Migrated
```nix
# New format
instances = {
borgbackup-production = {
module = {
name = "borgbackup";
input = "clan-core";
};
roles.server.machines."backup-server" = { };
roles.server.settings = {
directory = "/var/backup/borg";
};
roles.client.tags.backup = { };
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
};
zerotier-company-network = {
module = {
name = "zerotier";
input = "clan-core";
};
roles.controller.machines."network-controller" = { };
roles.moon.machines."moon-1".settings = {
stableEndpoints = [ "10.0.0.1" "2001:db8::1" ];
};
roles.moon.machines."moon-2".settings = {
stableEndpoints = [ "10.0.0.2" "2001:db8::2" ];
};
roles.peer.tags.nixos = { };
};
sshd-internal = {
module = {
name = "sshd";
input = "clan-core";
};
roles.server.tags.nixos = { };
roles.client.tags.nixos = { };
roles.client.settings = {
certificate.searchDomains = [
"internal.example.com"
"vpn.example.com"
];
};
};
};
```
---
## Steps to Migrate
@@ -212,33 +131,6 @@ roles.default.machines."test-inventory-machine".settings = {
};
```
### Important Type Changes
The new `instances` format uses **attribute sets** instead of **lists** for tags and machines:
```nix
# ❌ Old format (lists)
roles.client.tags = [ "backup" ];
roles.server.machines = [ "blob64" ];
# ✅ New format (attribute sets)
roles.client.tags.backup = { };
roles.server.machines.blob64 = { };
```
### Handling Multiple Machines/Tags
When you need to assign multiple machines or tags to a role:
```nix
# ❌ Old format
roles.moon.machines = [ "eva" "eve" ];
# ✅ New format - each machine gets its own attribute
roles.moon.machines.eva = { };
roles.moon.machines.eve = { };
```
---
!!! Warning
@@ -246,89 +138,8 @@ roles.moon.machines.eve = { };
* `inventory.services` is no longer recommended; use `inventory.instances` instead.
* Module authors should begin exporting service modules under the `clan.modules` attribute of their flake.
## Troubleshooting Common Migration Errors
### Error: "not of type `attribute set of (submodule)`"
This error occurs when using lists instead of attribute sets for tags or machines:
```
error: A definition for option `flake.clan.inventory.instances.borgbackup-blob64.roles.client.tags' is not of type `attribute set of (submodule)'.
```
**Solution**: Convert lists to attribute sets as shown in the "Important Type Changes" section above.
### Error: "unsupported attribute `module`"
This error indicates the module structure is incorrect:
```
error: Module ':anon-4:anon-1' has an unsupported attribute `module'.
```
**Solution**: Ensure the `module` attribute has exactly two fields: `name` and `input`.
### Error: "attribute 'pkgs' missing"
This suggests the instance configuration is trying to use imports incorrectly:
```
error: attribute 'pkgs' missing
```
**Solution**: Use the `module = { name = "..."; input = "..."; }` format instead of `imports`.
### Removed Features
The following features from the old `services` format are no longer supported in `instances`:
- Top-level `config` attribute (use `roles.<role>.settings` instead)
- Direct module imports (use the `module` declaration instead)
### extraModules Support
The `extraModules` attribute is still supported in the new instances format! The key change is how modules are specified:
**Old format (string paths relative to clan root):**
```nix
roles.client.extraModules = [ "nixosModules/borgbackup.nix" ];
```
**New format (NixOS modules):**
```nix
# Direct module reference
roles.client.extraModules = [ ../nixosModules/borgbackup.nix ];
# Or using self
roles.client.extraModules = [ self.nixosModules.borgbackup ];
# Or inline module definition
roles.client.extraModules = [
{ config, ... }: {
# Your module configuration here
}
];
```
The `extraModules` now expects actual **NixOS modules** rather than string paths. This provides better type checking and more flexibility in how modules are specified.
**Alternative: Using @clan/importer**
For scenarios where you need to import modules with specific tag-based targeting, you can also use the dedicated `@clan/importer` service:
```nix
instances = {
my-importer = {
module.name = "@clan/importer";
module.input = "clan-core";
roles.default.tags.my-tag = { };
roles.default.extraModules = [ self.nixosModules.myModule ];
};
};
```
## Further reference
* [Inventory Concept](../../concepts/inventory.md)
* [Authoring a 'clan.service' module](../../guides/services/community.md)
* [Authoring a 'clan.service' module](../authoring/clanServices/index.md)
* [ClanServices](../clanServices.md)
* [Inventory Reference](../../reference/nix-api/inventory.md)

View File

@@ -3,7 +3,7 @@
For a high level overview about `vars` see our [blog post](https://clan.lol/blog/vars/).
This guide will help you migrate your modules that still use our [`facts`](../../guides/secrets.md) backend
to the [`vars`](../../concepts/generators.md) backend.
to the [`vars`](../../guides/vars-backend.md) backend.
The `vars` [module](../../reference/clan.core/vars.md) and the clan [command](../../reference/cli/vars.md) work in tandem, they should ideally be kept in sync.

View File

@@ -0,0 +1,50 @@
Clan has two general methods of adding machines:
- **Automatic**: Detects every folder in the `machines` folder.
- **Declarative**: Explicit declarations in Nix.
## Automatic registration
Every folder `machines/{machineName}` will be registered automatically as a Clan machine.
!!! info "Automatically loaded files"
The following files are loaded automatically for each Clan machine:
- [x] `machines/{machineName}/configuration.nix`
- [x] `machines/{machineName}/hardware-configuration.nix`
- [x] `machines/{machineName}/facter.json` Automatically configured, for further information see [nixos-facter](https://clan.lol/blog/nixos-facter/)
- [x] `machines/{machineName}/disko.nix` Automatically loaded, for further information see the [disko docs](https://github.com/nix-community/disko/blob/master/docs/quickstart.md).
## Manual declaration
Machines can be added via [`clan.inventory.machines`](../guides/inventory.md) or in `clan.machines`, which allows for defining NixOS options.
=== "**Individual Machine Configuration**"
```{.nix}
clan-core.lib.clan {
machines = {
"jon" = {
# Any valid nixos config
};
};
}
```
=== "**Inventory Configuration**"
```{.nix}
clan-core.lib.clan {
inventory = {
machines = {
"jon" = {
# Inventory can set tags and other metadata
tags = [ "zone1" ];
deploy.targetHost = "root@jon";
};
};
};
}
```

View File

@@ -1,141 +1,25 @@
This article provides an overview over the underlying secrets system which is used by [Vars](../concepts/generators.md).
Under most circumstances you should use [Vars](../concepts/generators.md) directly instead.
If you want to know more about how to save and share passwords in your clan read further!
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
Manually interacting with secrets via `clan secrets [set|remove]`, etc may break the integrity of your `Vars` state.
---
Clan enables encryption of secrets (such as passwords & keys) ensuring security and ease-of-use among users.
By default, Clan uses the [sops](https://github.com/getsops/sops) format
and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
Clan can also be configured to be used with other secret store [backends](../reference/clan.core/vars.md#clan.core.vars.settings.secretStore).
## Create Your Admin Keypair
To get started, you'll need to create **your admin keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
```bash
clan secrets key generate
```
**Output**:
```{.console, .no-copy}
Public key: age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7
Generated age private key at '/home/joerg/.config/sops/age/keys.txt' for your user. Please back it up on a secure location or you will lose access to your secrets.
Also add your age public key to the repository with 'clan secrets users add YOUR_USER age1wkth7uhpkl555g40t8hjsysr20drq286netu8zptw50lmqz7j95sw2t3l7' (replace YOUR_USER with your actual username)
```
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If you already have an [age] secret key and want to use that instead, you can simply edit `~/.config/sops/age/keys.txt`:
```title="~/.config/sops/age/keys.txt"
AGE-SECRET-KEY-13GWMK0KNNKXPTJ8KQ9LPSQZU7G3KU8LZDW474NX3D956GGVFAZRQTAE3F4
```
Alternatively, you can provide your [age] secret key as an environment variable `SOPS_AGE_KEY`, or in a different file
using `SOPS_AGE_KEY_FILE`.
For more information see the [SOPS] guide on [encrypting with age].
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
## Add Your Public Key(s)
```console
clan secrets users add $USER --age-key <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
Once run this will create the following files:
```{.console, .no-copy}
sops/
└── users/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
!!! note
You can add multiple age keys for a user by providing multiple `--age-key <your_public_key>` flags:
```console
clan secrets users add $USER \
--age-key <your_public_key_1> \
--age-key <your_public_key_2> \
...
```
## Manage Your Public Key(s)
You can list keys for your user with `clan secrets users get $USER`:
```console
clan secrets users get alice
[
{
"publickey": "age1hrrcspp645qtlj29krjpq66pqg990ejaq0djcms6y6evnmgglv5sq0gewu",
"type": "age",
"username": "alice"
},
{
"publickey": "age13kh4083t3g4x3ktr52nav6h7sy8ynrnky2x58pyp96c5s5nvqytqgmrt79",
"type": "age",
"username": "alice"
}
]
```
To add a new key to your user:
```console
clan secrets users add-key $USER --age-key <your_public_key>
```
To remove a key from your user:
```console
clan secrets users remove-key $USER --age-key <your_public_key>
```
[age]: https://github.com/FiloSottile/age
[age plugin]: https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins
[sops]: https://github.com/getsops/sops
[encrypting with age]: https://github.com/getsops/sops?tab=readme-ov-file#encrypting-using-age
## Adding a Secret
### Adding a Secret
```shellSession
clan secrets set mysecret
Paste your secret:
```
## Retrieving a Stored Secret
### Retrieving a Stored Secret
```bash
clan secrets get mysecret
```
## List all Secrets
### List all Secrets
```bash
clan secrets list
```
## NixOS integration
### NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
@@ -153,7 +37,7 @@ In your nixos configuration you can get a path to secrets like this `config.sops
}
```
## Assigning Access
### Assigning Access
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.

View File

@@ -1,4 +1,7 @@
# Generators
!!! Note
Vars is the new secret backend that will soon replace the Facts backend
Defining a linux user's password via the nixos configuration previously required running `mkpasswd ...` and then copying the hash back into the nix configuration.
@@ -8,7 +11,7 @@ For a more general explanation of what clan vars are and how it works, see the i
This guide assumes
- Clan is set up already (see [Getting Started](../guides/getting-started/index.md))
- a machine has been added to the clan (see [Adding Machines](../guides/getting-started/add-machines.md))
- a machine has been added to the clan (see [Adding Machines](./more-machines.md))
This section will walk you through the following steps:
@@ -20,7 +23,7 @@ This section will walk you through the following steps:
6. share the root password between machines
7. change the password
## Declare a generator
## Declare the generator
In this example, a `vars` `generator` is used to:

View File

@@ -4,72 +4,87 @@ hide:
- toc
---
# :material-home: What is Clan?
[Clan](https://clan.lol/) is a peer-to-peer computer management framework that
empowers you to **selfhost in a reliable and scalable way**.
Built on NixOS, Clan provides a **declarative interface for managing machines** with automated [secret management](./guides/secrets.md), easy [mesh VPN
connectivity](./guides/mesh-vpn.md), and [automated backups](./guides/backups.md).
Whether you're running a homelab or maintaining critical computing infrastructure,
Clan will help **reduce maintenance burden** by allowing a **git repository to define your whole network** of computers.
In combination with [sops-nix](https://github.com/Mic92/sops-nix), [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) and [disko](https://github.com/nix-community/disko), Clan makes it possible to have **collaborative infrastructure**.
At the heart of Clan are [Clan Services](./reference/clanServices/index.md) - the core
concept that enables you to add functionality across multiple machines in your
network. While Clan ships with essential core services, you can [create custom
services](./guides/clanServices.md) tailored to your specific needs.
# :material-home: Welcome to **Clan**'s documentation
[Getting Started](./guides/getting-started/index.md){ .md-button }
## :material-book: Guides
How-to Guides for achieving a certain goal or solving a specific issue.
**How-to Guides for achieving a certain goal or solving a specific issue.**
<div class="grid cards" markdown>
- [:material-clock-fast: Getting Started](./guides/getting-started/index.md)
- [Adding more machines](./guides/more-machines.md)
---
Get started in less than 20 minutes!
Learn how Clan automatically includes machines and Nix files.
- [Mac OS](./guides/macos.md)
- [Vars Backend](./guides/vars-backend.md)
---
How to manage Mac OS machines with Clan
Learn how to manage secrets with vars.
- [Inventory](./guides/inventory.md)
---
Clan's declaration format for running **services** on one or multiple **machines**.
- [Flake-parts](./guides/flake-parts.md)
---
Use Clan with [https://flake.parts/]()
- [Contribute](./guides/contributing/CONTRIBUTING.md)
---
How to set up a development environment
Discover how to set up a development environment to contribute to Clan!
- [macOS machines](./guides/macos.md)
---
Manage macOS machines with nix-darwin
</div>
## Concepts
## API Reference
Explore the underlying principles of Clan
**Reference API Documentation**
<div class="grid cards" markdown>
- [Generators](./concepts/generators.md)
- [CLI Reference](./reference/cli/index.md)
---
Learn about Generators, our way to secret management
The `clan` CLI command
- [Inventory](./concepts/inventory.md)
- [Service Modules](./reference/clanServices/index.md)
---
Learn about the Inventory, a multi machine Nix interface
An overview of available service modules
- [Core](./reference/clan.core/index.md)
---
The clan core nix module.
This is imported when using clan and is the basis of the extra functionality
that can be provided.
- [(Legacy) Modules](./reference/clanModules/index.md)
---
An overview of available clanModules
!!! Example "These will be deprecated soon"
</div>
## Blog
Visit our [Clan Blog](https://clan.lol/blog/) for the latest updates, tutorials, and community stories.

25
docs/site/intern/index.md Normal file
View File

@@ -0,0 +1,25 @@
# Developer Documentation
!!! Danger
This documentation is **not** intended for external users. It may contain low-level details and internal-only interfaces.*
Welcome to the internal developer documentation.
This section is intended for contributors, engineers, and internal stakeholders working directly with our system, tooling, and APIs. It provides a technical overview of core components, internal APIs, conventions, and patterns that support the platform.
Our goal is to make the internal workings of the system **transparent, discoverable, and consistent** — helping you contribute confidently, troubleshoot effectively, and build faster.
## What's Here?
!!! note "docs migration ongoing"
- [ ] **API Reference**: 🚧🚧🚧 Detailed documentation of internal API functions, inputs, and expected outputs. 🚧🚧🚧
- [ ] **System Concepts**: Architectural overviews and domain-specific guides.
- [ ] **Development Guides**: How to test, extend, or integrate with key components.
- [ ] **Design Notes**: Rationales behind major design decisions or patterns.
## Who is This For?
* Developers contributing to the platform
* Engineers debugging or extending internal systems
* Anyone needing to understand **how** and **why** things work under the hood

View File

@@ -4,10 +4,10 @@ This section of the site provides an overview of available options and commands
---
- [Clan Configuration Option](../options.md) - for defining a Clan
- Learn how to use the [Clan CLI](./cli/index.md)
- Explore available services and application [modules](./clanModules/index.md)
- [NixOS Configuration Options](./clan.core/index.md) - Additional options avilable on a NixOS machine.
- Discover [configuration options](./clan.core/index.md) that manage essential features
- Find descriptions of the [Nix interfaces](./nix-api/clan.md) for defining a Clan
---

View File

@@ -2,7 +2,6 @@
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format("truetype");
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format("truetype");
@@ -21,9 +20,3 @@
.md-nav__item.md-nav__item--section > label > span {
color: var(--md-typeset-a-color);
}
.md-typeset h4 {
margin: 3em 0 0.5em;
font-weight: bold;
color: #7ebae4;
}

12
flake.lock generated
View File

@@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1753140376,
"narHash": "sha256-7lrVrE0jSvZHrxEzvnfHFE/Wkk9DDqb+mYCodI5uuB8=",
"lastModified": 1752718651,
"narHash": "sha256-PkaR0qmyP9q/MDN3uYa+RLeBA0PjvEQiM0rTDDBXkL8=",
"owner": "nix-community",
"repo": "disko",
"rev": "545aba02960caa78a31bd9a8709a0ad4b6320a5c",
"rev": "d5ad4485e6f2edcc06751df65c5e16572877db88",
"type": "github"
},
"original": {
@@ -51,11 +51,11 @@
]
},
"locked": {
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {

View File

@@ -229,8 +229,8 @@ in
};
inventory = lib.mkOption {
type = types.submoduleWith {
modules = [
type = types.submodule {
imports = [
{
_module.args = { inherit clanLib; };
_file = "clan interface";

View File

@@ -247,7 +247,7 @@ in
{
distributedServices = clanLib.inventory.mapInstances {
inherit (clanConfig) inventory exportsModule;
inherit flakeInputs directory;
inherit flakeInputs;
clanCoreModules = clan-core.clan.modules;
prefix = [ "distributedServices" ];
};

View File

@@ -1,8 +1,4 @@
# Wraps all services in one fixed point module
{
# TODO: consume directly from clan.config
directory,
}:
{
lib,
config,
@@ -33,8 +29,6 @@ in
{
_module.args._ctx = [ name ];
_module.args.exports' = config.exports;
_module.args.directory = directory;
}
)
./service-module.nix
@@ -77,5 +71,8 @@ in
};
default = { };
};
debug = mkOption {
default = lib.mapAttrsToList (_: service: service.exports) config.mappedServices;
};
};
}

View File

@@ -24,7 +24,6 @@ in
flakeInputs,
# The clan inventory
inventory,
directory,
clanCoreModules,
prefix ? [ ],
exportsModule,
@@ -129,7 +128,7 @@ in
_ctx = prefix;
};
modules = [
(import ./all-services-wrapper.nix { inherit directory; })
./all-services-wrapper.nix
] ++ modules;
};

View File

@@ -2,7 +2,6 @@
lib,
config,
_ctx,
directory,
...
}:
let
@@ -213,7 +212,7 @@ in
options.extraModules = lib.mkOption {
default = [ ];
type = types.listOf (types.either types.deferredModule types.str);
type = types.listOf (types.deferredModule);
};
})
];
@@ -756,14 +755,10 @@ in
instanceRes
// {
nixosModule = {
imports =
[
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
]
++ (map (
s: if builtins.typeOf s == "string" then "${directory}/${s}" else s
) instanceCfg.roles.${roleName}.extraModules);
imports = [
# Result of the applied 'perInstance = {...}: { nixosModule = { ... }; }'
instanceRes.nixosModule
] ++ instanceCfg.roles.${roleName}.extraModules;
};
}

View File

@@ -45,7 +45,6 @@ let
};
in
clanLib.inventory.mapInstances {
directory = ./.;
clanCoreModules = { };
flakeInputs = flakeInputsFixture;
inherit inventory;
@@ -53,7 +52,6 @@ let
};
in
{
extraModules = import ./extraModules.nix { inherit clanLib; };
exports = import ./exports.nix { inherit lib clanLib; };
resolve_module_spec = import ./import_module_spec.nix { inherit lib callInventoryAdapter; };
test_simple =

View File

@@ -1,33 +0,0 @@
{ clanLib }:
let
clan = clanLib.clan {
self = { };
directory = ./.;
machines.jon = {
nixpkgs.hostPlatform = "x86_64-linux";
};
# A module that adds exports perMachine
modules.A = {
manifest.name = "A";
roles.peer = { };
};
inventory = {
instances.A = {
module.input = "self";
roles.peer.tags.all = { };
roles.peer.extraModules = [ ./oneOption.nix ];
};
};
};
in
{
test_1 = {
inherit clan;
expr = clan.config.nixosConfigurations.jon.config.testDebug;
expected = 42;
};
}

View File

@@ -1,6 +0,0 @@
{ lib, ... }:
{
options.testDebug = lib.mkOption {
default = 42;
};
}

View File

@@ -142,7 +142,7 @@ in
- The module MUST have at least `features = [ "inventory" ]` in the frontmatter section.
- The module MUST have a subfolder `roles` with at least one `{roleName}.nix` file.
For further information see: [Module Authoring Guide](../../guides/services/community.md).
For further information see: [Module Authoring Guide](../../guides/authoring/clanServices/index.md).
???+ example
```nix
@@ -179,7 +179,8 @@ in
map (m: "'${m}'") (lib.attrNames (lib.filterAttrs (n: _v: !builtins.elem n allowedNames) moduleSet))
)}
See: https://docs.clan.lol/guides/services/community/
See: https://docs.clan.lol/guides/clanServices/
And: https://docs.clan.lol/guides/authoring/clanServices/
'' moduleSet;
};

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