Compare commits
1 Commits
simplify_h
...
yggdrasil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f197ebd861 |
@@ -30,7 +30,7 @@ In the Clan ecosystem, security is paramount. Learn how to handle secrets effect
|
||||
|
||||
The Clan project thrives on community contributions. We welcome everyone to contribute and collaborate:
|
||||
|
||||
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/guides/contributing/CONTRIBUTING/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
|
||||
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/contributing/contributing/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
|
||||
|
||||
## Join the revolution
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ inventory.instances = {
|
||||
borgbackup = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.client.machines."jon".settings = {
|
||||
destinations."storagebox" = {
|
||||
|
||||
@@ -8,7 +8,7 @@ The service consists of two roles:
|
||||
- A `server` role: This is the DNS-server that will be queried when trying to
|
||||
resolve clan-internal services. It defines the top-level domain.
|
||||
- A `default` role: This does two things. First, it sets up the nameservers so
|
||||
that clan-internal queries are resolved via the `server` machine, while
|
||||
thatclan-internal queries are resolved via the `server` machine, while
|
||||
external queries are resolved as normal via DHCP. Second, it allows exposing
|
||||
services (see example below).
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ inventory.instances = {
|
||||
clan-cache = {
|
||||
module = {
|
||||
name = "trusted-nix-caches";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.machines.draper = { };
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
user-alice = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.tags.all = { };
|
||||
roles.default.settings = {
|
||||
@@ -35,7 +35,7 @@
|
||||
user-bob = {
|
||||
module = {
|
||||
name = "users";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.default.machines.bobs-laptop = { };
|
||||
roles.default.settings.user = "bob";
|
||||
|
||||
108
clanServices/yggdrasil/default.nix
Normal file
108
clanServices/yggdrasil/default.nix
Normal file
@@ -0,0 +1,108 @@
|
||||
# Example clan service. See https://docs.clan.lol/guides/services/community/
|
||||
# for more details
|
||||
|
||||
# The test for this module in ./tests/vm/default.nix shows an example of how
|
||||
# the service is used.
|
||||
|
||||
{ packages }:
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "clan-core/yggdrasil";
|
||||
manifest.description = "Yggdrasil VPN";
|
||||
|
||||
roles.default = {
|
||||
# interface =
|
||||
# { lib, ... }:
|
||||
# {
|
||||
# # Here we define the settings for this role. They will be accessible
|
||||
# # via `roles.morning.settings` in the role
|
||||
#
|
||||
# options.greeting = lib.mkOption {
|
||||
# type = lib.types.str;
|
||||
# default = "Good morning";
|
||||
# description = "The greeting to use";
|
||||
# };
|
||||
# };
|
||||
# Maps over all instances and produces one result per instance.
|
||||
perInstance =
|
||||
{
|
||||
# Role settings for this machine/instance
|
||||
settings,
|
||||
|
||||
# The name of this instance of the service
|
||||
instanceName,
|
||||
|
||||
# The current machine
|
||||
machine,
|
||||
|
||||
# All roles of this service, with their assigned machines
|
||||
roles,
|
||||
...
|
||||
}:
|
||||
{
|
||||
# Analog to 'perSystem' of flake-parts.
|
||||
# For every instance of this service we will add a nixosModule to a morning-machine
|
||||
nixosModule =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
|
||||
clan.core.vars.generators.yggdrasil = {
|
||||
|
||||
files.privateKey = { };
|
||||
|
||||
runtimeInputs = with pkgs; [
|
||||
yggdrasil
|
||||
jq
|
||||
];
|
||||
|
||||
script = ''
|
||||
yggdrasil -genconf -json | jq 'to_entries|map(select(.key|endswith("Key")))|from_entries' > $out/privateKey
|
||||
'';
|
||||
};
|
||||
|
||||
services.yggdrasil = {
|
||||
persistentKeys = true;
|
||||
enable = true;
|
||||
};
|
||||
|
||||
systemd.services.yggdrasil.serviceConfig.BindReadOnlyPaths = [
|
||||
"${config.clan.core.vars.generators.yggdrasil.files.privateKey.path}:/var/lib/yggdrasil/keys.json"
|
||||
];
|
||||
|
||||
# Interaction examples what you could do here:
|
||||
# - Get some settings of this machine
|
||||
# settings.ipRanges
|
||||
#
|
||||
# - Get all evening names:
|
||||
# allEveningNames = lib.attrNames roles.evening.machines
|
||||
#
|
||||
# - Get all roles of the machine:
|
||||
# machine.roles
|
||||
#
|
||||
# - Get the settings that where applied to a specific evening machine:
|
||||
# roles.evening.machines.peer1.settings
|
||||
# environment.etc.hello.text = "${settings.greeting} World!";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# This part gets applied to all machines, regardless of their role.
|
||||
# perMachine =
|
||||
# { machine, ... }:
|
||||
# {
|
||||
# nixosModule =
|
||||
# { pkgs, ... }:
|
||||
# {
|
||||
# environment.systemPackages = [
|
||||
# (pkgs.writeShellScriptBin "greet-world" ''
|
||||
# #!${pkgs.bash}/bin/bash
|
||||
# set -euo pipefail
|
||||
#
|
||||
# cat /etc/hello
|
||||
# echo " I'm ${machine.name}"
|
||||
# '')
|
||||
# ];
|
||||
# };
|
||||
# };
|
||||
}
|
||||
25
clanServices/yggdrasil/flake-module.nix
Normal file
25
clanServices/yggdrasil/flake-module.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
self,
|
||||
inputs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix {
|
||||
inherit (self) packages;
|
||||
};
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
yggdrasil = module;
|
||||
};
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.yggdrasil = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules.yggdrasil = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
41
clanServices/yggdrasil/tests/vm/default.nix
Normal file
41
clanServices/yggdrasil/tests/vm/default.nix
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
name = "yggdrasil";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
inventory = {
|
||||
|
||||
machines.peer1 = { };
|
||||
|
||||
# machines.peer2 = { };
|
||||
|
||||
instances."yggdrasil" = {
|
||||
module.name = "yggdrasil";
|
||||
module.input = "self";
|
||||
|
||||
# Assign the roles to the two machines
|
||||
roles.default.machines.peer1 = { };
|
||||
|
||||
# roles.evening.machines.peer2 = {
|
||||
# # Set roles settings for the peers, where we want to differ from
|
||||
# # the role defaults
|
||||
# settings = {
|
||||
# greeting = "Good night";
|
||||
# };
|
||||
# };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
# value = peer1.succeed("greet-world")
|
||||
# assert value.strip() == "Good morning World! I'm peer1", value
|
||||
#
|
||||
# value = peer2.succeed("greet-world")
|
||||
# assert value.strip() == "Good night World! I'm peer2", value
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -5,7 +5,7 @@ inventory.instances = {
|
||||
zerotier = {
|
||||
module = {
|
||||
name = "zerotier";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.peer.tags.all = { };
|
||||
roles.controller.machines.jon = { };
|
||||
@@ -18,6 +18,7 @@ All machines will be peers and connected to the zerotier network.
|
||||
Jon is the controller machine, which will will accept other machines into the network.
|
||||
Sara is a moon and sets the `stableEndpoint` setting with a publicly reachable IP, the moon is optional.
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to set up and manage a [ZeroTier VPN](https://zerotier.com) for a clan network. Each VPN requires a single controller and can support multiple peers and optional moons for better connectivity.
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
It will be reachable under the given stable endpoints.
|
||||
'';
|
||||
example = ''
|
||||
[ "1.2.3.4" "10.0.0.3/9993" "2001:abcd:abcd::3/9993" ]
|
||||
[ 1.2.3.4" "10.0.0.3/9993" "2001:abcd:abcd::3/9993" ]
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
12
devFlake/flake.lock
generated
12
devFlake/flake.lock
generated
@@ -84,11 +84,11 @@
|
||||
},
|
||||
"nixpkgs-dev": {
|
||||
"locked": {
|
||||
"lastModified": 1757924820,
|
||||
"narHash": "sha256-to/hwbY9/jsRaejPa5oJmPUFZsJfFCB3WReKhD0+/+E=",
|
||||
"lastModified": 1757195359,
|
||||
"narHash": "sha256-Uf/d5NGvq+Q6ct+n5xRr76N1ZGV0vkfsJ6iVTciPkY0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "aa54acd34af0e86f49d55ea52823031e2da399df",
|
||||
"rev": "f4cefbe0160ba99567be386a043824549ccd5cb7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -107,11 +107,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757885130,
|
||||
"narHash": "sha256-56CMb5W/pgjKLh0bx2ekhn5rde/YmgR63HAqrY9/BCw=",
|
||||
"lastModified": 1756738487,
|
||||
"narHash": "sha256-8QX7Ab5CcICp7zktL47VQVS+QeaU4YDNAjzty7l7TQE=",
|
||||
"owner": "NuschtOS",
|
||||
"repo": "search",
|
||||
"rev": "fae3c59a646e00c4b1d359c50b27458a0713d2fd",
|
||||
"rev": "5feeaeefb571e6ca2700888b944f436f7c05149b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -61,16 +61,9 @@ nav:
|
||||
- Continuous Integration: guides/getting-started/flake-check.md
|
||||
- Convert Existing NixOS Config: guides/getting-started/convert-flake.md
|
||||
- ClanServices: guides/clanServices.md
|
||||
- Vars:
|
||||
- Overview: guides/vars/vars-overview.md
|
||||
- Getting Started: guides/vars/vars-backend.md
|
||||
- Concepts: guides/vars/vars-concepts.md
|
||||
- Sops Backend:
|
||||
- Yubikeys & Age Plugins: guides/vars/sops/age-plugins.md
|
||||
- Advanced Examples: guides/vars/vars-advanced-examples.md
|
||||
- Troubleshooting: guides/vars/vars-troubleshooting.md
|
||||
- Backup & Restore: guides/backups.md
|
||||
- Disk Encryption: guides/disk-encryption.md
|
||||
- Age Plugins: guides/age-plugins.md
|
||||
- Secrets management: guides/secrets.md
|
||||
- Networking: guides/networking.md
|
||||
- Zerotier VPN: guides/mesh-vpn.md
|
||||
@@ -90,6 +83,7 @@ nav:
|
||||
- Disk id: guides/migrations/disk-id.md
|
||||
- Concepts:
|
||||
- Inventory: concepts/inventory.md
|
||||
- Generators: concepts/generators.md
|
||||
- Autoincludes: concepts/autoincludes.md
|
||||
- Templates: concepts/templates.md
|
||||
- Reference:
|
||||
@@ -224,4 +218,4 @@ plugins:
|
||||
- redoc-tag
|
||||
- redirects:
|
||||
redirect_maps:
|
||||
guides/getting-started/secrets.md: guides/vars/vars-overview.md
|
||||
guides/getting-started/secrets.md: concepts/generators.md
|
||||
|
||||
@@ -205,31 +205,25 @@
|
||||
# };
|
||||
|
||||
packages = {
|
||||
docs-options =
|
||||
if privateInputs ? nuschtos then
|
||||
privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
|
||||
docs-options = privateInputs.nuschtos.packages.${pkgs.stdenv.hostPlatform.system}.mkMultiSearch {
|
||||
inherit baseHref;
|
||||
title = "Clan Options";
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [
|
||||
{
|
||||
inherit baseHref;
|
||||
title = "Clan Options";
|
||||
# scopes = mapAttrsToList mkScope serviceModules;
|
||||
scopes = [
|
||||
{
|
||||
inherit baseHref;
|
||||
name = "Flake Options (clan.nix file)";
|
||||
modules = docModules;
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
}
|
||||
{
|
||||
name = "Machine Options (clan.core NixOS options)";
|
||||
optionsJSON = "${coreOptions}/share/doc/nixos/options.json";
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
}
|
||||
];
|
||||
name = "Flake Options (clan.nix file)";
|
||||
modules = docModules;
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
}
|
||||
else
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "empty";
|
||||
buildCommand = "echo 'This is an empty derivation' > $out";
|
||||
};
|
||||
{
|
||||
name = "Machine Options (clan.core NixOS options)";
|
||||
optionsJSON = "${coreOptions}/share/doc/nixos/options.json";
|
||||
urlPrefix = "https://git.clan.lol/clan/clan-core/src/branch/main/";
|
||||
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
The `clan vars` subcommand is a powerful tool for managing machine-specific variables in a declarative and reproducible way. This guide will walk you through its usage, from setting up a generator to sharing and updating variables across machines.
|
||||
# Generators
|
||||
|
||||
For a detailed API reference, see the [vars module documentation](../../reference/clan.core/vars.md).
|
||||
Defining a linux user's password via the nixos configuration previously required running `mkpasswd ...` and then copying the hash back into the nix configuration.
|
||||
|
||||
In this guide, you will learn how to:
|
||||
In this example, we will guide you through automating that interaction using clan `vars`.
|
||||
|
||||
1. Declare a `generator` in the machine's NixOS configuration.
|
||||
2. Inspect the status of variables using the Clan CLI.
|
||||
3. Generate variables interactively.
|
||||
4. Observe the changes made to your repository.
|
||||
5. Update the machine configuration.
|
||||
6. Share the root password between multiple machines.
|
||||
7. Change the root password when needed.
|
||||
For a more general explanation of what clan vars are and how it works, see the intro of the [Reference Documentation for vars](../reference/clan.core/vars.md)
|
||||
|
||||
By the end of this guide, you will have a clear understanding of how to use `clan vars` to manage sensitive data, such as passwords, in a secure and efficient manner.
|
||||
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))
|
||||
|
||||
This section will walk you through the following steps:
|
||||
|
||||
## Declare the generator
|
||||
1. declare a `generator` in the machine's nixos configuration
|
||||
2. inspect the status via the Clan CLI
|
||||
3. generate the vars
|
||||
4. observe the changes
|
||||
5. update the machine
|
||||
6. share the root password between machines
|
||||
7. change the password
|
||||
|
||||
## Declare a generator
|
||||
|
||||
In this example, a `vars` `generator` is used to:
|
||||
|
||||
@@ -109,7 +114,7 @@ If we just imported the `root-password.nix` from above into more machines, clan
|
||||
If the root password instead should only be entered once and shared across all machines, the generator defined above needs to be declared as `shared`, by adding `share = true` to it:
|
||||
```nix
|
||||
{config, pkgs, ...}: {
|
||||
clan.core.vars.generators.root-password = {
|
||||
clan.vars.generators.root-password = {
|
||||
share = true;
|
||||
# ...
|
||||
}
|
||||
@@ -136,3 +141,8 @@ Updated var root-password/password-hash
|
||||
new: $6$OyoQtDVzeemgh8EQ$zRK...
|
||||
```
|
||||
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Reference Documentation for `clan.core.vars` NixOS options](../reference/clan.core/vars.md)
|
||||
- [Reference Documentation for the `clan vars` CLI command](../reference/cli/vars.md)
|
||||
@@ -21,7 +21,7 @@ The following tutorial will walk through setting up a Backup service where the t
|
||||
|
||||
## Services
|
||||
|
||||
The inventory defines `instances` of clan services. Membership of `machines` is defined via `roles` exclusively.
|
||||
The inventory defines `services`. Membership of `machines` is defined via `roles` exclusively.
|
||||
|
||||
See each [modules documentation](../reference/clanServices/index.md) for its available roles.
|
||||
|
||||
@@ -31,8 +31,9 @@ A service can be added to one or multiple machines via `Roles`. Clan's `Role` in
|
||||
|
||||
Each service can still be customized and configured according to the modules options.
|
||||
|
||||
- Per role configuration via `inventory.instances.<instanceName>.roles.<roleName>.settings`
|
||||
- Per machine configuration via `inventory.instances.<instanceName>.roles.<roleName>.machines.<machineName>.settings`
|
||||
- Per instance configuration via `services.<serviceName>.<instanceName>.config`
|
||||
- Per role configuration via `services.<serviceName>.<instanceName>.roles.<roleName>.config`
|
||||
- Per machine configuration via `services.<serviceName>.<instanceName>.machines.<machineName>.config`
|
||||
|
||||
### Setting up the Backup Service
|
||||
|
||||
@@ -43,17 +44,16 @@ Each service can still be customized and configured according to the modules opt
|
||||
|
||||
See also: [Multiple Service Instances](#multiple-service-instances)
|
||||
|
||||
```{.nix hl_lines="9-10"}
|
||||
{
|
||||
inventory.instances.instance_1 = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
```{.nix hl_lines="6-7"}
|
||||
clan-core.lib.clan {
|
||||
inventory = {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
# Machines can be added here.
|
||||
roles.client.machines = [ "jon" ];
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
};
|
||||
};
|
||||
|
||||
# Machines can be added here.
|
||||
roles.client.machines."jon" {};
|
||||
roles.server.machines."backup_server" = {};
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -66,8 +66,8 @@ It is possible to add services to multiple machines via tags as shown
|
||||
|
||||
!!! Example "Tags Example"
|
||||
|
||||
```{.nix hl_lines="5 8 18"}
|
||||
{
|
||||
```{.nix hl_lines="5 8 14"}
|
||||
clan-core.lib.clan {
|
||||
inventory = {
|
||||
machines = {
|
||||
"jon" = {
|
||||
@@ -76,16 +76,13 @@ It is possible to add services to multiple machines via tags as shown
|
||||
"sara" = {
|
||||
tags = [ "backup" ];
|
||||
};
|
||||
# ...
|
||||
};
|
||||
|
||||
instances.instance_1 = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.tags = [ "backup" ];
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
};
|
||||
|
||||
roles.client.tags = [ "backup" ];
|
||||
roles.server.machines."backup_server" = {};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -101,34 +98,22 @@ It is possible to add services to multiple machines via tags as shown
|
||||
|
||||
In this example `backup_server` has role `client` and `server` in different instances.
|
||||
|
||||
```{.nix hl_lines="17 26"}
|
||||
{
|
||||
```{.nix hl_lines="11 14"}
|
||||
clan-core.lib.clan {
|
||||
inventory = {
|
||||
machines = {
|
||||
"jon" = {};
|
||||
"backup_server" = {};
|
||||
"backup_backup_server" = {};
|
||||
"backup_backup_server" = {}
|
||||
};
|
||||
|
||||
instances = {
|
||||
instance_1 = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
};
|
||||
|
||||
roles.client.machines."jon" = {};
|
||||
roles.server.machines."backup_server" = {};
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.machines = [ "jon" ];
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
};
|
||||
|
||||
instance_2 = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
};
|
||||
|
||||
roles.client.machines."backup_server" = {};
|
||||
roles.server.machines."backup_backup_server" = {};
|
||||
borgbackup.instance_2 = {
|
||||
roles.client.machines = [ "backup_server" ];
|
||||
roles.server.machines = [ "backup_backup_server" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
59
docs/site/guides/age-plugins.md
Normal file
59
docs/site/guides/age-plugins.md
Normal file
@@ -0,0 +1,59 @@
|
||||
## 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
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
This guide explains how to set up and manage
|
||||
[BorgBackup](https://borgbackup.readthedocs.io/) for secure, efficient backups
|
||||
in a clan network. BorgBackup provides:
|
||||
@@ -17,7 +18,7 @@ inventory.instances = {
|
||||
borgbackup = {
|
||||
module = {
|
||||
name = "borgbackup";
|
||||
input = "clan-core";
|
||||
input = "clan";
|
||||
};
|
||||
roles.client.machines."jon".settings = {
|
||||
destinations."storagebox" = {
|
||||
@@ -176,7 +177,7 @@ storagebox::username@username.your-storagebox.de:/./borgbackup::jon-storagebox-2
|
||||
|
||||
### Restoring backups
|
||||
|
||||
For restoring a backup you have two options.
|
||||
For restoring a backup you have two options.
|
||||
|
||||
#### Full restoration
|
||||
|
||||
@@ -193,3 +194,6 @@ 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
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ This guide provides an example setup for a single-disk ZFS system with native en
|
||||
!!! Warning
|
||||
This configuration only applies to `systemd-boot` enabled systems and **requires** UEFI booting.
|
||||
|
||||
!!! Info "Secure Boot"
|
||||
This guide is compatible with systems that have [secure boot disabled](../guides/secure-boot.md). If you encounter boot issues, check if secure boot needs to be disabled in your UEFI settings.
|
||||
|
||||
Replace the highlighted lines with your own disk-id.
|
||||
You can find our your disk-id by executing:
|
||||
|
||||
@@ -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](../guides/vars/vars-overview.md)
|
||||
- Support for [vars](../concepts/generators.md)
|
||||
|
||||
## Add Your Machine to Your Clan Flake
|
||||
|
||||
|
||||
@@ -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`](../../guides/vars/vars-overview.md) backend.
|
||||
to the [`vars`](../../concepts/generators.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.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
This article provides an overview over the underlying secrets system which is used by [Vars](../guides/vars/vars-overview.md).
|
||||
Under most circumstances you should use [Vars](../guides/vars/vars-overview.md) directly instead.
|
||||
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.
|
||||
|
||||
Consider using `clan secrets` only for managing admin users and groups, as well as a debugging tool.
|
||||
|
||||
@@ -292,14 +292,15 @@ The following diagrams illustrates how a user can provide a secret (i.e. a Passw
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
|
||||
actor "User" as user
|
||||
database "Secret" as secret
|
||||
rectangle "Machine" as machine
|
||||
Person(user, "User", "Someone who manages secrets")
|
||||
ContainerDb(secret, "Secret")
|
||||
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
|
||||
|
||||
user -right-> secret : Encrypt\n(Pubkeys: User, Machine)
|
||||
secret -left-> user : Decrypt\n(user privkey)
|
||||
secret -right-> machine : Decrypt\n(machine privkey)
|
||||
Rel_R(user, secret, "Encrypt", "", "Pubkeys: User, Machine")
|
||||
Rel_L(secret, user, "Decrypt", "", "user privkey")
|
||||
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
|
||||
|
||||
@enduml
|
||||
```
|
||||
@@ -315,18 +316,19 @@ Common use cases:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
|
||||
rectangle "Group" {
|
||||
actor "User A" as user1
|
||||
actor "User B" as user2
|
||||
System_Boundary(c1, "Group") {
|
||||
Person(user1, "User A", "has access")
|
||||
Person(user2, "User B", "has access")
|
||||
}
|
||||
|
||||
database "Secret" as secret
|
||||
rectangle "Machine" as machine
|
||||
ContainerDb(secret, "Secret")
|
||||
Container(machine, "Machine", "A Machine. i.e. Needs the Secret for a given Service." )
|
||||
|
||||
Rel_R(c1, secret, "Encrypt", "", "Pubkeys: User A, User B, Machine")
|
||||
Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
|
||||
|
||||
user1 -right-> secret : Encrypt
|
||||
user2 -right-> secret : (Pubkeys: User A, User B, Machine)
|
||||
secret -right-> machine : Decrypt\n(machine privkey)
|
||||
|
||||
@enduml
|
||||
```
|
||||
@@ -345,17 +347,19 @@ Common use cases:
|
||||
|
||||
```plantuml
|
||||
@startuml
|
||||
!include C4_Container.puml
|
||||
!include C4_Deployment.puml
|
||||
|
||||
actor "User" as user
|
||||
database "Secret" as secret
|
||||
rectangle "Group" {
|
||||
rectangle "Machine A" as machine1
|
||||
rectangle "Machine B" as machine2
|
||||
Person(user, "User", "Someone who manages secrets")
|
||||
ContainerDb(secret, "Secret")
|
||||
System_Boundary(c1, "Group") {
|
||||
Container(machine1, "Machine A", "Both machines need the same secret" )
|
||||
Container(machine2, "Machine B", "Both machines need the same secret" )
|
||||
}
|
||||
|
||||
user -right-> secret : Encrypt\n(Pubkeys: machine A, machine B, User)
|
||||
secret -down-> machine1 : Decrypt
|
||||
secret -down-> machine2 : (Both machines can decrypt\nusing their private key)
|
||||
Rel_R(user, secret, "Encrypt", "", "Pubkeys: machine A, machine B, User")
|
||||
Rel(secret, c1, "Decrypt", "", "Both machine A or B can decrypt using their private key" )
|
||||
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
# Using Age Plugins with Clan Vars
|
||||
|
||||
This guide explains how to set up YubiKey and other plugins for `clan vars` secrets.
|
||||
|
||||
By default the `clan vars` subcommand uses the `age` encryption tool, which supports various plugins.
|
||||
|
||||
---
|
||||
|
||||
## Supported Age Plugins
|
||||
|
||||
Below is a [list of popular `age` plugins](https://github.com/FiloSottile/awesome-age?tab=readme-ov-file#plugins) you can use with Clan. (Last updated: **September 12, 2025**)
|
||||
|
||||
- ⭐️ [**age-plugin-yubikey**](https://github.com/str4d/age-plugin-yubikey): YubiKey (and other PIV tokens) plugin.
|
||||
- [**age-plugin-se**](https://github.com/remko/age-plugin-se): Apple Secure Enclave plugin.
|
||||
- 🧪 [**age-plugin-tpm**](https://github.com/Foxboron/age-plugin-tpm): TPM 2.0 plugin.
|
||||
- 🧪 [**age-plugin-tkey**](https://github.com/quite/age-plugin-tkey): Tillitis TKey plugin.
|
||||
[**age-plugin-trezor**](https://github.com/romanz/trezor-agent/blob/master/doc/README-age.md): Hardware wallet plugin (TREZOR, Ledger, etc.).
|
||||
- 🧪 [**age-plugin-sntrup761x25519**](https://github.com/keisentraut/age-plugin-sntrup761x25519): Post-quantum hybrid plugin (NTRU Prime + X25519).
|
||||
- 🧪 [**age-plugin-fido**](https://github.com/riastradh/age-plugin-fido): Prototype symmetric encryption plugin for FIDO2 keys.
|
||||
- 🧪 [**age-plugin-fido2-hmac**](https://github.com/olastor/age-plugin-fido2-hmac): FIDO2 plugin with PIN support.
|
||||
- 🧪 [**age-plugin-sss**](https://github.com/olastor/age-plugin-sss): Shamir's Secret Sharing (SSS) plugin.
|
||||
- 🧪 [**age-plugin-amnesia**](https://github.com/cedws/amnesia/blob/master/README.md#age-plugin-experimental): Adds Q&A-based identity wrapping.
|
||||
|
||||
> **Note:** Plugins marked with 🧪 are experimental. Plugins marked with ⭐️ are official.
|
||||
|
||||
---
|
||||
|
||||
## Using Plugin-Generated Keys
|
||||
|
||||
If you want to use `fido2 tokens` to encrypt your secret instead of the normal age secret key then you need to prefix your age secret key with the corresponding plugin name. In our case we want to use the `age-plugin-fido2-hmac` plugin so we replace `AGE-SECRET-KEY` with `AGE-PLUGIN-FIDO2-HMAC`.
|
||||
|
||||
??? tip
|
||||
- On Linux the age secret key is located at `~/.config/sops/age/keys.txt`
|
||||
- On macOS it is located at `/Users/admin/Library/Application Support/sops/age/keys.txt`
|
||||
|
||||
**Before**:
|
||||
```hl_lines="2"
|
||||
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||
AGE-SECRET-KEY-1QQPQZRFR7ZZ2WCV...
|
||||
```
|
||||
|
||||
**After**:
|
||||
```hl_lines="2"
|
||||
# public key: age1zdy49ek6z60q9r34vf5mmzkx6u43pr9haqdh5lqdg7fh5tpwlfwqea356l
|
||||
AGE-PLUGIN-FIDO2-HMAC-1QQPQZRFR7ZZ2WCV...
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Configuring Plugins in `flake.nix`
|
||||
|
||||
To use `age` plugins with Clan, you need to configure them in your `flake.nix` file. Here’s an example:
|
||||
|
||||
```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
|
||||
# Define Clan configuration
|
||||
clan = clan-core.lib.clan {
|
||||
inherit self;
|
||||
|
||||
meta.name = "myclan";
|
||||
|
||||
# Add YubiKey and FIDO2 HMAC plugins
|
||||
# Note: Plugins must be available in nixpkgs.
|
||||
secrets.age.plugins = [
|
||||
"age-plugin-yubikey"
|
||||
"age-plugin-fido2-hmac"
|
||||
];
|
||||
|
||||
machines = {
|
||||
# Machine configurations (elided for brevity)
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit (clan) nixosConfigurations nixosModules clanInternals;
|
||||
|
||||
# Additional configurations (elided for brevity)
|
||||
};
|
||||
}
|
||||
```
|
||||
@@ -1,290 +0,0 @@
|
||||
# Advanced Vars Examples
|
||||
|
||||
This guide demonstrates complex, real-world patterns for the vars system.
|
||||
|
||||
|
||||
## Certificate Authority with Intermediate Certificates
|
||||
|
||||
This example shows how to create a complete certificate authority with root and intermediate certificates using dependencies.
|
||||
|
||||
```nix
|
||||
{
|
||||
# Generate root CA (not deployed to machines)
|
||||
clan.core.vars.generators.root-ca = {
|
||||
files."ca.key" = {
|
||||
secret = true;
|
||||
deploy = false; # Keep root key offline
|
||||
};
|
||||
files."ca.crt".secret = false;
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
step certificate create "My Root CA" \
|
||||
$out/ca.crt $out/ca.key \
|
||||
--profile root-ca \
|
||||
--no-password \
|
||||
--not-after 87600h
|
||||
'';
|
||||
};
|
||||
|
||||
# Generate intermediate key
|
||||
clan.core.vars.generators.intermediate-key = {
|
||||
files."intermediate.key" = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
};
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
step crypto keypair \
|
||||
$out/intermediate.pub \
|
||||
$out/intermediate.key \
|
||||
--no-password
|
||||
'';
|
||||
};
|
||||
|
||||
# Generate intermediate certificate signed by root
|
||||
clan.core.vars.generators.intermediate-cert = {
|
||||
files."intermediate.crt".secret = false;
|
||||
dependencies = [
|
||||
"root-ca"
|
||||
"intermediate-key"
|
||||
];
|
||||
runtimeInputs = [ pkgs.step-cli ];
|
||||
script = ''
|
||||
step certificate create "My Intermediate CA" \
|
||||
$out/intermediate.crt \
|
||||
$in/intermediate-key/intermediate.key \
|
||||
--ca $in/root-ca/ca.crt \
|
||||
--ca-key $in/root-ca/ca.key \
|
||||
--profile intermediate-ca \
|
||||
--not-after 8760h \
|
||||
--no-password
|
||||
'';
|
||||
};
|
||||
|
||||
# Use the certificates in services
|
||||
services.nginx.virtualHosts."example.com" = {
|
||||
sslCertificate = config.clan.core.vars.generators.intermediate-cert.files."intermediate.crt".value;
|
||||
sslCertificateKey = config.clan.core.vars.generators.intermediate-key.files."intermediate.key".path;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Service Secret Sharing
|
||||
|
||||
Generate secrets that multiple services can use:
|
||||
|
||||
```nix
|
||||
{
|
||||
# Generate database credentials
|
||||
clan.core.vars.generators.database = {
|
||||
share = true; # Share across machines
|
||||
files."password" = { };
|
||||
files."connection-string" = { };
|
||||
prompts.dbname = {
|
||||
description = "Database name";
|
||||
type = "line";
|
||||
};
|
||||
script = ''
|
||||
# Generate password
|
||||
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 32 > $out/password
|
||||
|
||||
# Create connection string
|
||||
echo "postgresql://app:$(cat $out/password)@localhost/$prompts/dbname" \
|
||||
> $out/connection-string
|
||||
'';
|
||||
};
|
||||
|
||||
# PostgreSQL uses the password
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
initialScript = pkgs.writeText "init.sql" ''
|
||||
CREATE USER app WITH PASSWORD '${
|
||||
builtins.readFile config.clan.core.vars.generators.database.files."password".path
|
||||
}';
|
||||
'';
|
||||
};
|
||||
|
||||
# Application uses the connection string
|
||||
systemd.services.myapp = {
|
||||
serviceConfig.EnvironmentFile =
|
||||
config.clan.core.vars.generators.database.files."connection-string".path;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## SSH Host Keys with Certificates
|
||||
|
||||
Generate SSH host keys and sign them with a CA:
|
||||
|
||||
```nix
|
||||
{
|
||||
# SSH Certificate Authority (shared)
|
||||
clan.core.vars.generators.ssh-ca = {
|
||||
share = true;
|
||||
files."ca" = { secret = true; deploy = false; };
|
||||
files."ca.pub" = { secret = false; };
|
||||
runtimeInputs = [ pkgs.openssh ];
|
||||
script = ''
|
||||
ssh-keygen -t ed25519 -N "" -f $out/ca
|
||||
mv $out/ca.pub $out/ca.pub
|
||||
'';
|
||||
};
|
||||
|
||||
# Host-specific SSH keys
|
||||
clan.core.vars.generators.ssh-host = {
|
||||
files."ssh_host_ed25519_key" = {
|
||||
secret = true;
|
||||
owner = "root";
|
||||
group = "root";
|
||||
mode = "0600";
|
||||
};
|
||||
files."ssh_host_ed25519_key.pub" = { secret = false; };
|
||||
files."ssh_host_ed25519_key-cert.pub" = { secret = false; };
|
||||
dependencies = [ "ssh-ca" ];
|
||||
runtimeInputs = [ pkgs.openssh ];
|
||||
script = ''
|
||||
# Generate host key
|
||||
ssh-keygen -t ed25519 -N "" -f $out/ssh_host_ed25519_key
|
||||
|
||||
# Sign with CA
|
||||
ssh-keygen -s $in/ssh-ca/ca \
|
||||
-I "host:${config.networking.hostName}" \
|
||||
-h \
|
||||
-V -5m:+365d \
|
||||
$out/ssh_host_ed25519_key.pub
|
||||
'';
|
||||
};
|
||||
|
||||
# Configure SSH to use the generated keys
|
||||
services.openssh = {
|
||||
hostKeys = [{
|
||||
path = config.clan.core.vars.generators.ssh-host.files."ssh_host_ed25519_key".path;
|
||||
type = "ed25519";
|
||||
}];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## WireGuard Mesh Network
|
||||
|
||||
Create a WireGuard configuration with pre-shared keys:
|
||||
|
||||
```nix
|
||||
{
|
||||
# Generate WireGuard keys for this host
|
||||
clan.core.vars.generators.wireguard = {
|
||||
files."privatekey" = {
|
||||
secret = true;
|
||||
owner = "systemd-network";
|
||||
mode = "0400";
|
||||
};
|
||||
files."publickey" = { secret = false; };
|
||||
files."preshared" = { secret = true; };
|
||||
runtimeInputs = [ pkgs.wireguard-tools ];
|
||||
script = ''
|
||||
# Generate key pair
|
||||
wg genkey > $out/privatekey
|
||||
wg pubkey < $out/privatekey > $out/publickey
|
||||
|
||||
# Generate pre-shared key
|
||||
wg genpsk > $out/preshared
|
||||
'';
|
||||
};
|
||||
|
||||
# Configure WireGuard
|
||||
networking.wireguard.interfaces.wg0 = {
|
||||
privateKeyFile = config.clan.core.vars.generators.wireguard.files."privatekey".path;
|
||||
|
||||
peers = [{
|
||||
publicKey = "peer-public-key-here";
|
||||
presharedKeyFile = config.clan.core.vars.generators.wireguard.files."preshared".path;
|
||||
allowedIPs = [ "10.0.0.2/32" ];
|
||||
}];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Generation Based on Machine Role
|
||||
|
||||
Generate different secrets based on machine configuration:
|
||||
|
||||
```nix
|
||||
{
|
||||
clan.core.vars.generators = lib.mkMerge [
|
||||
# All machines get basic auth
|
||||
{
|
||||
basic-auth = {
|
||||
files."htpasswd" = { };
|
||||
prompts.username = {
|
||||
description = "Username for basic auth";
|
||||
type = "line";
|
||||
};
|
||||
prompts.password = {
|
||||
description = "Password for basic auth";
|
||||
type = "hidden";
|
||||
};
|
||||
runtimeInputs = [ pkgs.apacheHttpd ];
|
||||
script = ''
|
||||
htpasswd -nbB "$prompts/username" "$prompts/password" > $out/htpasswd
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
||||
# Only servers get API tokens
|
||||
(lib.mkIf config.services.myapi.enable {
|
||||
api-tokens = {
|
||||
files."admin-token" = { };
|
||||
files."readonly-token" = { };
|
||||
runtimeInputs = [ pkgs.openssl ];
|
||||
script = ''
|
||||
openssl rand -hex 32 > $out/admin-token
|
||||
openssl rand -hex 16 > $out/readonly-token
|
||||
'';
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## Backup Encryption Keys
|
||||
|
||||
Generate and manage backup encryption keys:
|
||||
|
||||
```nix
|
||||
{
|
||||
clan.core.vars.generators.backup = {
|
||||
share = true; # Same key for all backup sources
|
||||
files."encryption.key" = {
|
||||
secret = true;
|
||||
deploy = true;
|
||||
};
|
||||
files."encryption.pub" = { secret = false; };
|
||||
runtimeInputs = [ pkgs.age ];
|
||||
script = ''
|
||||
# Generate age key pair
|
||||
age-keygen -o $out/encryption.key 2>/dev/null
|
||||
|
||||
# Extract public key
|
||||
grep "public key:" $out/encryption.key | cut -d: -f2 | tr -d ' ' \
|
||||
> $out/encryption.pub
|
||||
'';
|
||||
};
|
||||
|
||||
# Use in backup service
|
||||
services.borgbackup.jobs.system = {
|
||||
encryption = {
|
||||
mode = "repokey-blake2";
|
||||
passCommand = "cat ${config.clan.core.vars.generators.backup.files."encryption.key".path}";
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Tips and Best Practices
|
||||
|
||||
1. **Use dependencies** to build complex multi-stage generations
|
||||
2. **Share generators** when the same secret is needed across machines
|
||||
3. **Set appropriate permissions** for service-specific secrets
|
||||
4. **Use prompts** for user-specific values that shouldn't be generated
|
||||
5. **Combine secret and non-secret files** in the same generator when they're related
|
||||
6. **Use conditional generation** with `lib.mkIf` for role-specific secrets
|
||||
@@ -1,123 +0,0 @@
|
||||
# Understanding Clan Vars - Concepts & Architecture
|
||||
|
||||
This guide explains the architecture and design principles behind the vars system.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The vars system provides a declarative, reproducible way to manage generated files (especially secrets) in NixOS configurations.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Generator Script] --> B[Output Files]
|
||||
C[User Prompts] --> A
|
||||
D[Dependencies] --> A
|
||||
B --> E[Secret Storage<br/>sops/password-store]
|
||||
B --> F[Nix Store<br/>public files]
|
||||
E --> G[Machine Deployment]
|
||||
F --> G
|
||||
```
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
### 1. Declarative Generation
|
||||
|
||||
Unlike imperative secret management, vars are declared in your NixOS configuration and generated deterministically. This ensures reproducibility across deployments.
|
||||
|
||||
### 2. Separation of Concerns
|
||||
|
||||
- **Generation logic**: Defined in generator scripts
|
||||
- **Storage**: Handled by pluggable backends (sops, password-store, etc.)
|
||||
- **Deployment**: Managed by NixOS activation scripts
|
||||
- **Access control**: Enforced through file permissions and ownership
|
||||
|
||||
### 3. Composability Through Dependencies
|
||||
|
||||
Generators can depend on outputs from other generators, enabling complex workflows:
|
||||
|
||||
```nix
|
||||
# Dependencies create a directed acyclic graph (DAG)
|
||||
A → B → C
|
||||
↓
|
||||
D
|
||||
```
|
||||
|
||||
This allows building sophisticated systems like certificate authorities where intermediate certificates depend on root certificates.
|
||||
|
||||
### 4. Type Safety
|
||||
|
||||
The vars system distinguishes between:
|
||||
- **Secret files**: Only accessible via `.path`, deployed to `/run/secrets/`
|
||||
- **Public files**: Accessible via `.value`, stored in nix store
|
||||
|
||||
This prevents accidental exposure of secrets in the nix store.
|
||||
|
||||
## Storage Backend Architecture
|
||||
|
||||
The vars system uses pluggable storage backends:
|
||||
|
||||
- **sops** (default): Integrates with clan's existing sops encryption
|
||||
- **password-store**: For users already using pass
|
||||
|
||||
Each backend handles encryption/decryption transparently, allowing the same generator definitions to work across different security models.
|
||||
|
||||
## Timing and Lifecycle
|
||||
|
||||
### Generation Phases
|
||||
|
||||
1. **Pre-deployment**: `clan vars generate` creates vars before deployment
|
||||
2. **During deployment**: Missing vars are generated automatically
|
||||
3. **Regeneration**: Explicit regeneration with `--regenerate` flag
|
||||
|
||||
### The `neededFor` Option
|
||||
|
||||
Control when vars are available during system activation:
|
||||
|
||||
```nix
|
||||
files."early-secret" = {
|
||||
secret = true;
|
||||
neededFor = [ "users" "groups" ]; # Available early in activation
|
||||
};
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Multi-Machine Coordination
|
||||
|
||||
The `share` option enables cross-machine secret sharing:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Shared Generator] --> B[Machine 1]
|
||||
A --> C[Machine 2]
|
||||
A --> D[Machine 3]
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
- Shared certificate authorities
|
||||
- Mesh VPN pre-shared keys
|
||||
- Cluster join tokens
|
||||
|
||||
### Generator Composition
|
||||
|
||||
Complex systems can be built by composing simple generators:
|
||||
|
||||
```
|
||||
root-ca → intermediate-ca → service-cert
|
||||
↓
|
||||
ocsp-responder
|
||||
```
|
||||
|
||||
Each generator focuses on one task, making the system modular and testable.
|
||||
|
||||
## Key Advantages
|
||||
|
||||
Compared to manual secret management, vars provides:
|
||||
|
||||
- **Declarative configuration**: Define once, generate consistently
|
||||
- **Dependency management**: Build complex systems with generator dependencies
|
||||
- **Type safety**: Separate handling of secret and public files
|
||||
- **User prompts**: Gather input when needed
|
||||
- **Easy regeneration**: Update secrets with a single command
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
# Vars System Overview
|
||||
|
||||
The vars system is clan's declarative solution for managing generated files, secrets, and dynamic configuration in your NixOS deployments. It eliminates the manual steps of generating credentials, certificates, and other dynamic values by automating these processes within your infrastructure-as-code workflow.
|
||||
|
||||
## What Problems Does Vars Solve?
|
||||
|
||||
### Before Vars: Manual Secret Management
|
||||
|
||||
Traditional NixOS deployments require manual steps for secrets and generated files:
|
||||
|
||||
```bash
|
||||
# Generate password hash manually
|
||||
mkpasswd -m sha-512 > /tmp/root-password-hash
|
||||
# Copy hash into configuration
|
||||
users.users.root.hashedPasswordFile = "/tmp/root-password-hash";
|
||||
```
|
||||
|
||||
This approach has several problems:
|
||||
|
||||
- **Not reproducible**: Manual steps vary between team members
|
||||
|
||||
- **Hard to maintain**: Updating secrets requires remembering manual commands
|
||||
|
||||
- **Deployment friction**: Secrets must be managed outside of your configuration
|
||||
|
||||
- **Team collaboration issues**: Sharing credentials securely is complex
|
||||
|
||||
### After Vars: Declarative Generation
|
||||
|
||||
With vars, the same process becomes declarative and automated:
|
||||
|
||||
```nix
|
||||
clan.core.vars.generators.root-password = {
|
||||
prompts.password.description = "Root password";
|
||||
prompts.password.type = "hidden";
|
||||
files.hash.secret = false;
|
||||
script = "mkpasswd -m sha-512 < $prompts/password > $out/hash";
|
||||
runtimeInputs = [ pkgs.mkpasswd ];
|
||||
};
|
||||
|
||||
users.users.root.hashedPasswordFile =
|
||||
config.clan.core.vars.generators.root-password.files.hash.path;
|
||||
```
|
||||
|
||||
## Core Benefits
|
||||
|
||||
- **🔄 Reproducible**: Same inputs always produce the same outputs
|
||||
- **📝 Declarative**: Defined alongside your NixOS configuration
|
||||
- **🔐 Secure**: Automatic secret storage and encrypted deployment
|
||||
- **👥 Collaborative**: Built-in sharing for team environments
|
||||
- **🚀 Automated**: No manual intervention required for deployments
|
||||
- **🔗 Integrated**: Works seamlessly with clan's deployment workflow
|
||||
|
||||
## How It Works
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Generator Declaration] --> B[clan vars generate]
|
||||
B --> C{Prompts User}
|
||||
C --> D[Execute Script]
|
||||
D --> E[Output Files]
|
||||
E --> F{Secret?}
|
||||
F -->|Yes| G[Encrypted Storage]
|
||||
F -->|No| H[Git Repository]
|
||||
G --> I[Deploy to Machine]
|
||||
H --> I
|
||||
I --> J[Available in NixOS]
|
||||
```
|
||||
|
||||
1. **Declare generators** in your NixOS configuration
|
||||
2. **Generate values** using `clan vars generate` (or automatically during deployment)
|
||||
3. **Store securely** in encrypted backends or version control
|
||||
4. **Deploy seamlessly** to your machines where they're accessible as file paths
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
| Use Case | What Gets Generated | Benefits |
|
||||
|----------|-------------------|----------|
|
||||
| **User passwords** | Password hashes | No plaintext in config |
|
||||
| **SSH keys** | Host/user keypairs | Automated key rotation |
|
||||
| **TLS certificates** | Certificates + private keys | Automated PKI |
|
||||
| **Database credentials** | Passwords + connection strings | Secure service communication |
|
||||
| **API tokens** | Random tokens | Service authentication |
|
||||
| **Configuration files** | Complex configs with secrets | Dynamic config generation |
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The vars system has three main components:
|
||||
|
||||
### 1. **Generators**
|
||||
Define how to create files from inputs:
|
||||
|
||||
- **Prompts**: Values requested from users
|
||||
|
||||
- **Scripts**: Generation logic
|
||||
|
||||
- **Dependencies**: Other generators this depends on
|
||||
|
||||
- **Outputs**: Files that get created
|
||||
|
||||
### 2. **Storage Backends**
|
||||
Handle secret storage and deployment:
|
||||
|
||||
- **sops**: Encrypted files in git (recommended)
|
||||
|
||||
- **password-store**: GPG/age-based secret storage
|
||||
|
||||
## Quick Start Example
|
||||
|
||||
Here's a complete example showing password generation and usage:
|
||||
|
||||
```nix
|
||||
# generator.nix
|
||||
{ config, pkgs, ... }: {
|
||||
clan.core.vars.generators.user-password = {
|
||||
prompts.password = {
|
||||
description = "User password";
|
||||
type = "hidden";
|
||||
};
|
||||
files.hash = { secret = false; };
|
||||
script = ''
|
||||
mkpasswd -m sha-512 < $prompts/password > $out/hash
|
||||
'';
|
||||
runtimeInputs = [ pkgs.mkpasswd ];
|
||||
};
|
||||
|
||||
users.users.myuser = {
|
||||
hashedPasswordFile =
|
||||
config.clan.core.vars.generators.user-password.files.hash.path;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Generate the password
|
||||
clan vars generate my-machine
|
||||
|
||||
# Deploy to machine
|
||||
clan machines update my-machine
|
||||
```
|
||||
|
||||
## Migration from Facts
|
||||
|
||||
If you're currently using the legacy facts system, see our [Migration Guide](../migrations/migration-facts-vars.md) for step-by-step instructions on upgrading to vars.
|
||||
|
||||
@@ -1,272 +0,0 @@
|
||||
# Troubleshooting Vars
|
||||
|
||||
Quick reference for diagnosing and fixing vars issues.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Generator Script Fails
|
||||
|
||||
**Symptom**: Error during `clan vars generate` or deployment
|
||||
|
||||
**Possible causes and solutions**:
|
||||
|
||||
1. **Missing runtime inputs**
|
||||
```nix
|
||||
# Wrong - missing required tool
|
||||
runtimeInputs = [ ];
|
||||
script = ''
|
||||
openssl rand -hex 32 > $out/secret # openssl not found!
|
||||
'';
|
||||
|
||||
# Correct
|
||||
runtimeInputs = [ pkgs.openssl ];
|
||||
```
|
||||
|
||||
2. **Wrong output path**
|
||||
```nix
|
||||
# Wrong - must use $out
|
||||
script = ''
|
||||
echo "secret" > ./myfile
|
||||
'';
|
||||
|
||||
# Correct
|
||||
script = ''
|
||||
echo "secret" > $out/myfile
|
||||
'';
|
||||
```
|
||||
|
||||
3. **Missing declared files**
|
||||
```nix
|
||||
files."config" = { };
|
||||
files."key" = { };
|
||||
script = ''
|
||||
# Wrong - only generates one file
|
||||
echo "data" > $out/config
|
||||
'';
|
||||
|
||||
# Correct - must generate all declared files
|
||||
script = ''
|
||||
echo "data" > $out/config
|
||||
echo "key" > $out/key
|
||||
'';
|
||||
```
|
||||
|
||||
### Cannot Access Generated Files
|
||||
|
||||
**Symptom**: "attribute 'value' missing" or file not found
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Secret files don't have `.value`**
|
||||
```nix
|
||||
# Wrong - secret files can't use .value
|
||||
files."secret" = { secret = true; };
|
||||
# ...
|
||||
environment.etc."app.conf".text =
|
||||
config.clan.core.vars.generators.app.files."secret".value;
|
||||
|
||||
# Correct - use .path for secrets
|
||||
environment.etc."app.conf".source =
|
||||
config.clan.core.vars.generators.app.files."secret".path;
|
||||
```
|
||||
|
||||
2. **Public files should use `.value`**
|
||||
```nix
|
||||
# Better for non-secrets
|
||||
files."cert.pem" = { secret = false; };
|
||||
# ...
|
||||
sslCertificate =
|
||||
config.clan.core.vars.generators.ca.files."cert.pem".value;
|
||||
```
|
||||
|
||||
### Dependencies Not Available
|
||||
|
||||
**Symptom**: "No such file or directory" when accessing `$in/...`
|
||||
|
||||
**Solution**: Declare dependencies correctly
|
||||
```nix
|
||||
clan.core.vars.generators.child = {
|
||||
# Wrong - missing dependency
|
||||
script = ''
|
||||
cat $in/parent/file > $out/newfile
|
||||
'';
|
||||
|
||||
# Correct
|
||||
dependencies = [ "parent" ];
|
||||
script = ''
|
||||
cat $in/parent/file > $out/newfile
|
||||
'';
|
||||
};
|
||||
```
|
||||
|
||||
### Permission Denied
|
||||
|
||||
**Symptom**: Service cannot read generated secret file
|
||||
|
||||
**Solution**: Set correct ownership and permissions
|
||||
```nix
|
||||
files."service.key" = {
|
||||
secret = true;
|
||||
owner = "myservice"; # Match service user
|
||||
group = "myservice";
|
||||
mode = "0400"; # Read-only for owner
|
||||
};
|
||||
```
|
||||
|
||||
### Vars Not Regenerating
|
||||
|
||||
**Symptom**: Changes to generator script don't trigger regeneration
|
||||
|
||||
**Solution**: Use `--regenerate` flag
|
||||
```bash
|
||||
clan vars generate my-machine --generator my-generator --regenerate
|
||||
```
|
||||
|
||||
### Prompts Not Working
|
||||
|
||||
**Symptom**: Script fails with "No such file or directory" for prompts
|
||||
|
||||
**Solution**: Access prompts correctly
|
||||
```nix
|
||||
# Wrong
|
||||
script = ''
|
||||
echo $password > $out/file
|
||||
'';
|
||||
|
||||
# Correct
|
||||
prompts.password.type = "hidden";
|
||||
script = ''
|
||||
cat $prompts/password > $out/file
|
||||
'';
|
||||
```
|
||||
|
||||
## Debugging Techniques
|
||||
|
||||
### 1. Check Generator Status
|
||||
|
||||
See what vars are set:
|
||||
```bash
|
||||
clan vars list my-machine
|
||||
```
|
||||
|
||||
### 2. Inspect Generated Files
|
||||
|
||||
For shared vars:
|
||||
```bash
|
||||
ls -la vars/shared/my-generator/
|
||||
```
|
||||
|
||||
For per-machine vars:
|
||||
```bash
|
||||
ls -la vars/per-machine/my-machine/my-generator/
|
||||
```
|
||||
|
||||
### 3. Test Generators Locally
|
||||
|
||||
Create a test script to debug:
|
||||
```nix
|
||||
# test-generator.nix
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
name = "test-generator";
|
||||
buildInputs = [ pkgs.openssl ]; # Your runtime inputs
|
||||
buildCommand = ''
|
||||
# Your generator script here
|
||||
mkdir -p $out
|
||||
openssl rand -hex 32 > $out/secret
|
||||
ls -la $out/
|
||||
'';
|
||||
}
|
||||
```
|
||||
|
||||
Run with:
|
||||
```bash
|
||||
nix-build test-generator.nix
|
||||
```
|
||||
|
||||
### 4. Enable Debug Logging
|
||||
|
||||
Set debug mode:
|
||||
```bash
|
||||
clan --debug vars generate my-machine
|
||||
```
|
||||
|
||||
### 5. Check File Permissions
|
||||
|
||||
Verify generated secret permissions:
|
||||
```bash
|
||||
# On the target machine
|
||||
ls -la /run/secrets/
|
||||
```
|
||||
|
||||
## Recovery Procedures
|
||||
|
||||
### Regenerate All Vars
|
||||
|
||||
If vars are corrupted or need refresh:
|
||||
```bash
|
||||
# Regenerate all for a machine
|
||||
clan vars generate my-machine --regenerate
|
||||
|
||||
# Regenerate specific generator
|
||||
clan vars generate my-machine --generator my-generator --regenerate
|
||||
```
|
||||
|
||||
### Manual Secret Injection
|
||||
|
||||
For recovery or testing:
|
||||
```bash
|
||||
# Set a var manually (bypass generator)
|
||||
echo "temporary-secret" | clan vars set my-machine my-generator/my-file
|
||||
```
|
||||
|
||||
### Restore from Backup
|
||||
|
||||
Vars are stored in the repository:
|
||||
```bash
|
||||
# Restore previous version
|
||||
git checkout HEAD~1 -- vars/
|
||||
|
||||
# Check and regenerate if needed
|
||||
clan vars list my-machine
|
||||
```
|
||||
|
||||
## Storage Backend Issues
|
||||
|
||||
### SOPS Decryption Fails
|
||||
|
||||
**Symptom**: "Failed to decrypt" or permission errors
|
||||
|
||||
**Solution**: Ensure your user/machine has the correct age keys configured. Clan manages encryption keys automatically based on the configured users and machines in your flake.
|
||||
|
||||
Check that:
|
||||
|
||||
1. Your machine is properly configured in the flake
|
||||
|
||||
2. Your user has access to the machine's secrets
|
||||
|
||||
3. The age key is available in the expected location
|
||||
|
||||
### Password Store Issues
|
||||
|
||||
**Symptom**: "pass: store not initialized"
|
||||
|
||||
**Solution**: Initialize password store:
|
||||
```bash
|
||||
export PASSWORD_STORE_DIR=/path/to/clan/vars
|
||||
pass init your-gpg-key
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If these solutions don't resolve your issue:
|
||||
|
||||
1. Check the [clan-core issue tracker](https://git.clan.lol/clan/clan-core/issues)
|
||||
2. Ask in the Clan community channels
|
||||
3. Provide:
|
||||
|
||||
- The generator configuration
|
||||
|
||||
- The exact error message
|
||||
|
||||
- Output of `clan --debug vars generate`
|
||||
@@ -55,7 +55,7 @@ Explore the underlying principles of Clan
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- [Vars](./guides/vars/vars-overview.md)
|
||||
- [Generators](./concepts/generators.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
26
flake.lock
generated
26
flake.lock
generated
@@ -13,11 +13,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757905600,
|
||||
"narHash": "sha256-Yd7buL9N7N7IaDVViItqP9HsECfnlDFykxvvNgMYcKk=",
|
||||
"rev": "c10c4002bdc5aef040fcbb814d5f482e82dc8345",
|
||||
"lastModified": 1756695982,
|
||||
"narHash": "sha256-dyLhOSDzxZtRgi5aj/OuaZJUsuvo+8sZ9CU/qieZ15c=",
|
||||
"rev": "cc8f26e7e6c2dc985526ba59b286ae5a83168cdb",
|
||||
"type": "tarball",
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c10c4002bdc5aef040fcbb814d5f482e82dc8345.tar.gz"
|
||||
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/cc8f26e7e6c2dc985526ba59b286ae5a83168cdb.tar.gz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
@@ -31,11 +31,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757508292,
|
||||
"narHash": "sha256-7lVWL5bC6xBIMWWDal41LlGAG+9u2zUorqo3QCUL4p4=",
|
||||
"lastModified": 1756733629,
|
||||
"narHash": "sha256-dwWGlDhcO5SMIvMSTB4mjQ5Pvo2vtxvpIknhVnSz2I8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "146f45bee02b8bd88812cfce6ffc0f933788875a",
|
||||
"rev": "a5c4f2ab72e3d1ab43e3e65aa421c6f2bd2e12a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -71,11 +71,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757430124,
|
||||
"narHash": "sha256-MhDltfXesGH8VkGv3hmJ1QEKl1ChTIj9wmGAFfWj/Wk=",
|
||||
"lastModified": 1757130842,
|
||||
"narHash": "sha256-4i7KKuXesSZGUv0cLPLfxbmF1S72Gf/3aSypgvVkwuA=",
|
||||
"owner": "nix-darwin",
|
||||
"repo": "nix-darwin",
|
||||
"rev": "830b3f0b50045cf0bcfd4dab65fad05bf882e196",
|
||||
"rev": "15f067638e2887c58c4b6ba1bdb65a0b61dc58c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -146,11 +146,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1757449901,
|
||||
"narHash": "sha256-qwN8nYdSRnmmyyi+uR6m4gXnVktmy5smG1MOrSFD8PI=",
|
||||
"lastModified": 1754988908,
|
||||
"narHash": "sha256-t+voe2961vCgrzPFtZxha0/kmFSHFobzF00sT8p9h0U=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "3b4a369df9dd6ee171a7ea4448b50e2528faf850",
|
||||
"rev": "3223c7a92724b5d804e9988c6b447a0d09017d48",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
;
|
||||
|
||||
privateInputs =
|
||||
if builtins.pathExists (./. + "/.skip-private-inputs") then
|
||||
if builtins.pathExists (./. + ".skip-private-inputs") then
|
||||
{ }
|
||||
else
|
||||
(import ./devFlake/flake-compat.nix {
|
||||
|
||||
@@ -189,12 +189,8 @@ in
|
||||
clan.core.vars.generators.zerotier = {
|
||||
migrateFact = "zerotier";
|
||||
files.zerotier-ip.secret = false;
|
||||
files.zerotier-ip.restartUnits = [ "zerotierone.service" ];
|
||||
files.zerotier-network-id.secret = false;
|
||||
files.zerotier-network-id.restartUnits = [ "zerotierone.service" ];
|
||||
files.zerotier-identity-secret = {
|
||||
restartUnits = [ "zerotierone.service" ];
|
||||
};
|
||||
files.zerotier-identity-secret = { };
|
||||
runtimeInputs = [
|
||||
config.services.zerotierone.package
|
||||
pkgs.python3
|
||||
@@ -215,10 +211,7 @@ in
|
||||
clan.core.vars.generators.zerotier = {
|
||||
migrateFact = "zerotier";
|
||||
files.zerotier-ip.secret = false;
|
||||
files.zerotier-ip.restartUnits = [ "zerotierone.service" ];
|
||||
files.zerotier-identity-secret = {
|
||||
restartUnits = [ "zerotierone.service" ];
|
||||
};
|
||||
files.zerotier-identity-secret = { };
|
||||
runtimeInputs = [
|
||||
config.services.zerotierone.package
|
||||
pkgs.python3
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import logging
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import ExitStack
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from clan_lib.api import ApiError, ApiResponse, ErrorDataClass
|
||||
from clan_lib.api.tasks import WebThread
|
||||
from clan_lib.async_run import set_current_thread_opkey, set_should_cancel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_app.middleware.base import Middleware
|
||||
from .middleware import Middleware
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,17 +30,20 @@ class BackendResponse:
|
||||
_op_key: str
|
||||
|
||||
|
||||
class ApiBridge(Protocol):
|
||||
@dataclass
|
||||
class ApiBridge(ABC):
|
||||
"""Generic interface for API bridges that can handle method calls from different sources."""
|
||||
|
||||
middleware_chain: tuple["Middleware", ...]
|
||||
threads: dict[str, WebThread]
|
||||
threads: dict[str, WebThread] = field(default_factory=dict)
|
||||
|
||||
def send_api_response(self, response: BackendResponse) -> None: ...
|
||||
@abstractmethod
|
||||
def send_api_response(self, response: BackendResponse) -> None:
|
||||
"""Send response back to the client."""
|
||||
|
||||
def process_request(self, request: BackendRequest) -> None:
|
||||
"""Process an API request through the middleware chain."""
|
||||
from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
|
||||
from .middleware import MiddlewareContext # noqa: PLC0415
|
||||
|
||||
with ExitStack() as stack:
|
||||
context = MiddlewareContext(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
"""Compatibility wrapper for relocated middleware components.
|
||||
|
||||
This module preserves the legacy import path ``clan_app.api.middleware`` while
|
||||
the actual middleware implementations now live in ``clan_app.middleware``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from warnings import warn
|
||||
|
||||
import clan_app.middleware as _middleware
|
||||
from clan_app.middleware import * # noqa: F403
|
||||
|
||||
warn(
|
||||
"clan_app.api.middleware is deprecated; use clan_app.middleware instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
__all__ = _middleware.__all__
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Middleware components shared by API bridge implementations."""
|
||||
"""Middleware components for the webview API bridge."""
|
||||
|
||||
from .argument_parsing import ArgumentParsingMiddleware
|
||||
from .base import Middleware, MiddlewareContext
|
||||
@@ -12,13 +12,13 @@ from clan_lib.log_manager import LogGroupConfig, LogManager
|
||||
from clan_lib.log_manager import api as log_manager_api
|
||||
|
||||
from clan_app.api.file_gtk import get_clan_folder, get_system_file
|
||||
from clan_app.deps.http.http_server import HttpApiServer
|
||||
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
||||
from clan_app.middleware import (
|
||||
from clan_app.api.middleware import (
|
||||
ArgumentParsingMiddleware,
|
||||
LoggingMiddleware,
|
||||
MethodExecutionMiddleware,
|
||||
)
|
||||
from clan_app.deps.http.http_server import HttpApiServer
|
||||
from clan_app.deps.webview.webview import Size, SizeHint, Webview
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from clan_lib.async_run import (
|
||||
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_app.middleware.base import Middleware
|
||||
from clan_app.api.middleware import Middleware
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from clan_lib.api import MethodRegistry
|
||||
from clan_lib.api.tasks import WebThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_app.middleware.base import Middleware
|
||||
from clan_app.api.middleware import Middleware
|
||||
|
||||
from .http_bridge import HttpBridge
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import pytest
|
||||
from clan_lib.api import MethodRegistry, tasks
|
||||
from clan_lib.async_run import is_async_cancelled
|
||||
|
||||
from clan_app.deps.http.http_server import HttpApiServer
|
||||
from clan_app.middleware import (
|
||||
from clan_app.api.middleware import (
|
||||
ArgumentParsingMiddleware,
|
||||
MethodExecutionMiddleware,
|
||||
)
|
||||
from clan_app.deps.http.http_server import HttpApiServer
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from ._webview_ffi import (
|
||||
from .webview_bridge import WebviewBridge
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_app.middleware.base import Middleware
|
||||
from clan_app.api.middleware import Middleware
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from clan_lib.api import dataclass_to_dict
|
||||
@@ -9,8 +9,6 @@ from clan_lib.api.tasks import WebThread
|
||||
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clan_app.middleware.base import Middleware
|
||||
|
||||
from .webview import Webview
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -21,8 +19,7 @@ class WebviewBridge(ApiBridge):
|
||||
"""Webview-specific implementation of the API bridge."""
|
||||
|
||||
webview: "Webview"
|
||||
middleware_chain: tuple["Middleware", ...]
|
||||
threads: dict[str, WebThread] = field(default_factory=dict)
|
||||
threads: dict[str, WebThread] # Inherited from ApiBridge
|
||||
|
||||
def send_api_response(self, response: BackendResponse) -> None:
|
||||
"""Send response back to the webview client."""
|
||||
|
||||
@@ -38,7 +38,7 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
|
||||
assets.forEach((asset) => {
|
||||
// console.log(asset);
|
||||
if (asset.src === "index.html") {
|
||||
asset.css?.forEach((cssEntry) => {
|
||||
asset.css.forEach((cssEntry) => {
|
||||
// css to be processed
|
||||
|
||||
const css = fs.readFileSync(`dist/${cssEntry}`, "utf8");
|
||||
|
||||
@@ -19,7 +19,6 @@ import { LoadingBar } from "@/src/components/LoadingBar/LoadingBar";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { useClanURI } from "@/src/hooks/clan";
|
||||
import { AlertProps } from "@/src/components/Alert/Alert";
|
||||
import usbLogo from "@/logos/usb-stick-min.png?url";
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface UpdateStepperProps {
|
||||
@@ -136,7 +135,11 @@ const UpdateProgress = () => {
|
||||
|
||||
return (
|
||||
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
|
||||
<img src={usbLogo} alt="usb logo" class="absolute top-2 z-0" />
|
||||
<img
|
||||
src="/logos/usb-stick-min.png"
|
||||
alt="usb logo"
|
||||
class="absolute top-2 z-0"
|
||||
/>
|
||||
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useSystemStorageOptions } from "@/src/hooks/queries";
|
||||
import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { onMount } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import usbLogo from "@/logos/usb-stick-min.png?url";
|
||||
|
||||
const Prose = () => (
|
||||
<StepLayout
|
||||
@@ -336,7 +335,11 @@ const FlashProgress = () => {
|
||||
"relative flex size-full flex-col items-center justify-end bg-inv-4",
|
||||
)}
|
||||
>
|
||||
<img src={usbLogo} alt="usb logo" class="absolute top-4 z-0" />
|
||||
<img
|
||||
src="/logos/usb-stick-min.png"
|
||||
alt="usb logo"
|
||||
class="absolute top-4 z-0"
|
||||
/>
|
||||
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -36,7 +36,6 @@ import { useApiClient } from "@/src/hooks/ApiClient";
|
||||
import { ProcessMessage, useNotifyOrigin } from "@/src/hooks/notify";
|
||||
import { Loader } from "@/src/components/Loader/Loader";
|
||||
import { Button as KButton } from "@kobalte/core/button";
|
||||
import usbLogo from "@/logos/usb-stick-min.png?url";
|
||||
|
||||
export const InstallHeader = (props: { machineName: string }) => {
|
||||
return (
|
||||
@@ -830,7 +829,11 @@ const InstallProgress = () => {
|
||||
|
||||
return (
|
||||
<div class="relative flex size-full flex-col items-center justify-end bg-inv-4">
|
||||
<img src={usbLogo} alt="usb logo" class="absolute top-2 z-0" />
|
||||
<img
|
||||
src="/logos/usb-stick-min.png"
|
||||
alt="usb logo"
|
||||
class="absolute top-2 z-0"
|
||||
/>
|
||||
<div class="z-10 mb-6 flex w-full max-w-md flex-col items-center gap-2 fg-inv-1">
|
||||
<Typography
|
||||
hierarchy="title"
|
||||
|
||||
@@ -30,7 +30,6 @@ export default defineConfig({
|
||||
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
optimizeDeps: {
|
||||
include: ["debug", "extend"],
|
||||
},
|
||||
@@ -49,18 +48,7 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
target: "safari11",
|
||||
modulePreload: false,
|
||||
// assetsDi
|
||||
manifest: true,
|
||||
// Inline everything: TODO
|
||||
// Detect file:///assets requests and point to the correct directory in webview
|
||||
rollupOptions: {
|
||||
output: {
|
||||
format: "iife",
|
||||
// entryFileName: ""
|
||||
// inlineDynamicImports: true,
|
||||
},
|
||||
},
|
||||
// assetsInlineLimit: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@ clangStdenv.mkDerivation {
|
||||
domain = "git.clan.lol";
|
||||
owner = "clan";
|
||||
repo = "webview";
|
||||
rev = "d83c3ebffb76f25b3a9e37d59237c5d8e94060a2";
|
||||
hash = "sha256-YAfH1KCw4r2WPvBQho2ypAVH+/c/a05SsEDUYKGadFI=";
|
||||
rev = "c27041cb50f79c197080a3f4fa2bad4557ef3234";
|
||||
hash = "sha256-xNkX7O+GFMbv3YnXPrtO6vw+BUqCbVeFd8FjgPKfEG0=";
|
||||
};
|
||||
|
||||
outputs = [
|
||||
|
||||
@@ -198,7 +198,7 @@ This subcommand provides an interface to templates provided by clan.
|
||||
Examples:
|
||||
|
||||
$ clan templates list
|
||||
List all available templates
|
||||
List all the machines managed by Clan.
|
||||
|
||||
Usage differs based on the template type
|
||||
|
||||
@@ -227,16 +227,6 @@ Disk templates
|
||||
|
||||
Real world example
|
||||
$ clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
|
||||
|
||||
---
|
||||
|
||||
Machine templates
|
||||
|
||||
$ clan templates apply machine [TEMPLATE] [MACHINE_NAME]
|
||||
Will create a new machine [MACHINE_NAME] from the specified [TEMPLATE]
|
||||
|
||||
Real world example
|
||||
$ clan templates apply machine flash-installer my-installer
|
||||
"""
|
||||
),
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
|
||||
@@ -303,27 +303,6 @@ def complete_templates_clan(
|
||||
return []
|
||||
|
||||
|
||||
def complete_templates_machine(
|
||||
_prefix: str,
|
||||
parsed_args: argparse.Namespace,
|
||||
**_kwargs: Any,
|
||||
) -> Iterable[str]:
|
||||
"""Provides completion functionality for machine templates"""
|
||||
flake = (
|
||||
clan_dir_result
|
||||
if (clan_dir_result := clan_dir(getattr(parsed_args, "flake", None)))
|
||||
is not None
|
||||
else "."
|
||||
)
|
||||
|
||||
list_all_templates = list_templates(Flake(flake))
|
||||
machine_template_list = list_all_templates.builtins.get("machine")
|
||||
if machine_template_list:
|
||||
machine_templates = list(machine_template_list)
|
||||
return dict.fromkeys(machine_templates, "machine")
|
||||
return []
|
||||
|
||||
|
||||
def complete_vars_for_machine(
|
||||
_prefix: str,
|
||||
parsed_args: argparse.Namespace,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import argparse
|
||||
|
||||
from .apply_disk import register_apply_disk_template_parser
|
||||
from .apply_machine import register_apply_machine_template_parser
|
||||
|
||||
|
||||
def register_apply_parser(parser: argparse.ArgumentParser) -> None:
|
||||
@@ -12,7 +11,5 @@ def register_apply_parser(parser: argparse.ArgumentParser) -> None:
|
||||
required=True,
|
||||
)
|
||||
disk_parser = subparser.add_parser("disk", help="Apply a disk template")
|
||||
machine_parser = subparser.add_parser("machine", help="Apply a machine template")
|
||||
|
||||
register_apply_disk_template_parser(disk_parser)
|
||||
register_apply_machine_template_parser(machine_parser)
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from clan_lib.nix_models.clan import InventoryMachine
|
||||
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
|
||||
|
||||
from clan_cli.machines.create import CreateOptions, create_machine
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def apply_command(args: argparse.Namespace) -> None:
|
||||
"""Apply a machine template - actually an alias for machines create --template."""
|
||||
# Create machine using the create_machine API directly
|
||||
machine = InventoryMachine(
|
||||
name=args.machine,
|
||||
tags=[],
|
||||
deploy=MachineDeploy(targetHost=None),
|
||||
)
|
||||
|
||||
opts = CreateOptions(
|
||||
clan_dir=args.flake,
|
||||
machine=machine,
|
||||
template=args.template,
|
||||
)
|
||||
|
||||
create_machine(opts)
|
||||
|
||||
|
||||
def register_apply_machine_template_parser(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument(
|
||||
"template",
|
||||
type=str,
|
||||
help="The name of the machine template to apply",
|
||||
)
|
||||
parser.add_argument(
|
||||
"machine",
|
||||
type=str,
|
||||
help="The name of the machine to create from the template",
|
||||
)
|
||||
|
||||
parser.set_defaults(func=apply_command)
|
||||
@@ -1,79 +0,0 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from clan_lib.errors import ClanError
|
||||
from clan_lib.flake import Flake
|
||||
from clan_lib.machines.machines import Machine
|
||||
from clan_lib.templates.disk import set_machine_disk_schema
|
||||
|
||||
from clan_cli.tests.fixtures_flakes import FlakeForTest
|
||||
from clan_cli.tests.helpers import cli
|
||||
|
||||
|
||||
@pytest.mark.with_core
|
||||
def test_templates_apply_machine_and_disk(
|
||||
test_flake_with_core: FlakeForTest,
|
||||
) -> None:
|
||||
"""Test both machine template creation and disk template application."""
|
||||
flake_path = str(test_flake_with_core.path)
|
||||
|
||||
cli.run(
|
||||
[
|
||||
"templates",
|
||||
"apply",
|
||||
"machine",
|
||||
"new-machine",
|
||||
"test-apply-machine",
|
||||
"--flake",
|
||||
flake_path,
|
||||
]
|
||||
)
|
||||
|
||||
# Verify machine was created
|
||||
machine_dir = test_flake_with_core.path / "machines" / "test-apply-machine"
|
||||
assert machine_dir.exists(), "Machine directory should be created"
|
||||
assert (machine_dir / "configuration.nix").exists(), (
|
||||
"Configuration file should exist"
|
||||
)
|
||||
|
||||
facter_content = {
|
||||
"disks": [
|
||||
{
|
||||
"name": "test-disk",
|
||||
"path": "/dev/sda",
|
||||
"size": 107374182400,
|
||||
"type": "disk",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
facter_path = machine_dir / "facter.json"
|
||||
facter_path.write_text(json.dumps(facter_content, indent=2))
|
||||
|
||||
machine = Machine(name="test-apply-machine", flake=Flake(flake_path))
|
||||
set_machine_disk_schema(
|
||||
machine,
|
||||
"single-disk",
|
||||
{"mainDisk": "/dev/sda"},
|
||||
force=False,
|
||||
check_hw=False, # Skip hardware validation for test
|
||||
)
|
||||
|
||||
# Verify disk template was applied by checking that disko.nix exists or was updated
|
||||
disko_file = machine_dir / "disko.nix"
|
||||
assert disko_file.exists(), "Disko configuration should be created"
|
||||
|
||||
# Verify error handling - try to create duplicate machine
|
||||
# Since apply machine now uses machines create, it raises ClanError for duplicates
|
||||
with pytest.raises(ClanError, match="already exists"):
|
||||
cli.run(
|
||||
[
|
||||
"templates",
|
||||
"apply",
|
||||
"machine",
|
||||
"new-machine",
|
||||
"test-apply-machine", # Same name as existing
|
||||
"--flake",
|
||||
flake_path,
|
||||
]
|
||||
)
|
||||
@@ -8,7 +8,6 @@ HostKeyCheck = Literal[
|
||||
"strict", # Strictly check ssh host keys, prompt for unknown ones
|
||||
"ask", # Ask for confirmation on first use
|
||||
"accept-new", # Trust on ssh keys on first use
|
||||
"tofu", # Trust on ssh keys on first use
|
||||
"none", # Do not check ssh host keys
|
||||
]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# The lines below will define a zerotier network and add all machines as 'peer' to it.
|
||||
# !!! Manual steps required:
|
||||
# - Define a controller machine for the zerotier network.
|
||||
# - Deploy the controller machine first to initialize the network.
|
||||
# - Deploy the controller machine first to initilize the network.
|
||||
zerotier = {
|
||||
# Replace with the name (string) of your machine that you will use as zerotier-controller
|
||||
# See: https://docs.zerotier.com/controller/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{ inputs, ... }:
|
||||
{
|
||||
imports = [
|
||||
inputs.clan-core.flakeModules.default
|
||||
inputs.clan.flakeModules.default
|
||||
];
|
||||
clan = {
|
||||
meta.name = "__CHANGE_ME__";
|
||||
|
||||
@@ -9,7 +9,7 @@ _: {
|
||||
devShells = {
|
||||
default = pkgs.mkShellNoCC {
|
||||
packages = [
|
||||
inputs'.clan-core.packages.default
|
||||
inputs'.clan.packages.default
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# The lines below will define a zerotier network and add all machines as 'peer' to it.
|
||||
# !!! Manual steps required:
|
||||
# - Define a controller machine for the zerotier network.
|
||||
# - Deploy the controller machine first to initialize the network.
|
||||
# - Deploy the controller machine first to initilize the network.
|
||||
zerotier = {
|
||||
# Replace with the name (string) of your machine that you will use as zerotier-controller
|
||||
# See: https://docs.zerotier.com/controller/
|
||||
|
||||
Reference in New Issue
Block a user