{
description = "My cluster configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
fleet = {
url = "github:CertainLach/fleet";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-parts.url = "github:hercules-ci/flake-parts";
lanzaboote = {
url = "github:nix-community/lanzaboote/v0.3.0";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
imports = [inputs.fleet.flakeModules.default];
perSystem = {
inputs',
pkgs,
system,
...
}: {
formatter = pkgs.alejandra;
devShells.default =
pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};
};
# Single flake may contain multiple fleet configurations, default one is called... `default`
fleetConfigurations.default = {
# nixos option section of fleet config declares module, which is used for all configured nixos hosts.
nixos = {
imports = [inputs.lanzaboote.nixosModules.lanzaboote];
# Make `nix shell nixpkgs#thing` use the same nixpkgs, as used to build the system.
nix.registry.nixpkgs = {
from = {
id = "nixpkgs";
type = "indirect";
};
flake = inputs.nixpkgs;
exact = false;
};
};
# Those modules are used to configure all the machines in cluster at the same time, good example of global modules
# Is I.e wiring up the mesh VPN, or deploying kubernetes, or other things.
#
# Modules use the same semantics as standard nixos module system, they are just configuring all the hosts at once.
imports = [
./wireguard
# Multi-instancible modules example
(import ./kubernetes {hosts = ["a" "b"];})
(import ./kubernetes {hosts = ["c" "d"];})
];
# Hosts attribute (may also be defined/extended using modules attribute) configures hosts...
hosts.controlplane-1 = {
# Every host has some system, for which the system configuration needs to be built
system = "x86_64-linux";
nixos = {
# And nixos modules
imports = [
./controlplane-1/hardware-configuration.nix
./controlplane-1/configuration.nix
];
# Configuration may also be specified inline, as in any nixos config.
services.ray = {
gpus = 4;
cpus = 128;
};
};
};
};
};
}
difftreelog
docs prepare for delta.rocks
Some of the docs were generated by Claude Opus, but they still should be correct. Wireguard example and explanation is based on mesh example written by me:
16 files changed
README.adocdiffbeforeafterbothAn NixOS cluster deployment tool.
Advantages over existing configuration systems (NixOps/Morph)
-
Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)
-
Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.
-
Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)
Flake example
Secret generator example
- TODO
-
This section should into some kind of fleet documentation… But as there is none, it is just left here as-is.
Quickly run securely setup gitlab
{config, ...}: {
secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {
gitlab-initial-root = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-secret = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-otp = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-db = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-jws = {
generator = {mkRsa}: mkRsa {};
} // ownership;
};
services.gitlab = let secrets = config.secrets; in {
enable = true;
initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;
secrets = {
secretFile = secrets.gitlab-secret.secretPath;
otpFile = secrets.gitlab-otp.secretPath;
dbFile = secrets.gitlab-db.secretPath;
jwsFile = secrets.gitlab-jws.secretPath;
};
};
}
Securely initialize kubernetes secrets
In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it. This setup should probably split into multiple steps, where I allow target machine to generate CSR, then copy it to the HSM machine, and then sign it there… But this is just the plan. I want to build ansible-like script execution in fleet for this kind of tasks.
{...}: {
# First I define required secret generators:
nixpkgs.overlays = [
(final: prev: let
lib = final.lib;
in {
readKubernetesCa = {impureOn}:
final.mkImpureSecretGenerator ''
cd ~/ca
cert=kubernetes-intermediateCA.crt
expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
echo -n $expires_at > $out/expires_at
cat $cert > $out/public
''
impureOn;
mkKubernetesCert = {
subj,
sans ? [],
impureOn,
}:
final.mkImpureSecretGenerator ''
cd ~/ca
params=$(sudo mktemp)
csr=$(sudo mktemp)
cert=$(sudo mktemp)
sudo openssl ecparam -genkey -name secp384r1 -out $params
sudo openssl req -new -key $params \
-subj "${lib.strings.concatStringsSep""(lib.attrsets.mapAttrsToList(kv"/${k}=${v}")subj)}" \
${lib.optionalString(sans!=[])"-addext \"subjectAltName = ${lib.strings.concatStringsSep","sans}\""} \
-out $csr
sudo hsms x509 -req -days 365 -in $csr -CA kubernetes-intermediateCA.crt -CAkey "pkcs11:object=[CENSORED] Kubernetes Intermediate CA;type=private" -CAcreateserial -copy_extensions copy -out $cert
expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
echo -n $expires_at > $out/expires_at
sudo cat $params | encrypt > $out/secret
sudo cat $cert > $out/public
''
impureOn;
})
];
# Those secret generators are impure, thus they are run in system environment.
# Probably there needs to be a dedicated user for that kind of tasks, but this is my current setup, don't judge.
# I write a couple of scripts for executing openssl with HSM.
environment.systemPackages = [
pkgs.openssl.bin
(pkgs.writeShellApplication {
name = "hsms";
text = ''
set -eu
export OPENSSL_CONF=${openssl-conf}
# Yay, using secrets to generate secrets!
HSM_PIN=cat ${config.secrets.hsm-pin.secretPath})
exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"
'';
})
(pkgs.writeShellApplication {
name = "hsmt";
text = ''
set -eu
HSM_PIN=cat ${config.secrets.hsm-pin.secretPath})
exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"
'';
})
];
# And finally, I have secrets, which are shared between machines.
# Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
secrets = {
"ca.pem" = {
# This is just the public key, no need to regenerate it to change owner list
regenerateOnOwnerAdded = false;
# For secret regeneration/reencryption, we need to specify which machines SHOULD have it.
expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];
generator = {readKubernetesCa}:
readKubernetesCa {
impureOn = "[CENSORED]";
};
};
"kube-admin.pem" = {
regenerateOnOwnerAdded = false;
expectedOwners = ["cluster-admin"];
generator = {mkKubernetesCert}:
mkKubernetesCert {
subj = {
CN = "admin";
O = "system:masters";
};
impureOn = "[CENSORED]";
};
};
"kube-apiserver.pem" = {
# This secret depends on machine SANS, so if owner list has been changed, then we need to regenerate it.
# However, SANS dependency is in fact handled by secret seed, and secret is regenerated if the seed is changed...
#
# In this case regeneration is added as a half-assed security measure, as if apiserver is removed, we don't
# want for it to be able to pretend like it is a valid server.
#
# However, certificate revokation is complicated in my setup, and I can't show it here.
regenerateOnOwnerAdded = true;
expectedOwners = ["controlplane-1" "controlplane-2"];
generator = {mkKubernetesCert}:
mkKubernetesCert {
inherit sans;
subj.CN = "kubernetes";
impureOn = "[CENSORED]";
};
};
}
An NixOS cluster deployment tool.
Advantages over existing configuration systems (NixOps/Morph)
-
Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)
-
Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.
-
Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)
Flake example
{
description = "My cluster configuration";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
fleet = {
url = "github:CertainLach/fleet";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-parts.url = "github:hercules-ci/flake-parts";
lanzaboote = {
url = "github:nix-community/lanzaboote/v0.3.0";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs:
inputs.flake-parts.lib.mkFlake {inherit inputs;} {
imports = [inputs.fleet.flakeModules.default];
perSystem = {
inputs',
pkgs,
system,
...
}: {
formatter = pkgs.alejandra;
devShells.default =
pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};
};
# Single flake may contain multiple fleet configurations, default one is called... `default`
fleetConfigurations.default = {
# nixos option section of fleet config declares module, which is used for all configured nixos hosts.
nixos = {
imports = [inputs.lanzaboote.nixosModules.lanzaboote];
# Make `nix shell nixpkgs#thing` use the same nixpkgs, as used to build the system.
nix.registry.nixpkgs = {
from = {
id = "nixpkgs";
type = "indirect";
};
flake = inputs.nixpkgs;
exact = false;
};
};
# Those modules are used to configure all the machines in cluster at the same time, good example of global modules
# Is I.e wiring up the mesh VPN, or deploying kubernetes, or other things.
#
# Modules use the same semantics as standard nixos module system, they are just configuring all the hosts at once.
imports = [
./wireguard
# Multi-instancible modules example
(import ./kubernetes {hosts = ["a" "b"];})
(import ./kubernetes {hosts = ["c" "d"];})
];
# Hosts attribute (may also be defined/extended using modules attribute) configures hosts...
hosts.controlplane-1 = {
# Every host has some system, for which the system configuration needs to be built
system = "x86_64-linux";
nixos = {
# And nixos modules
imports = [
./controlplane-1/hardware-configuration.nix
./controlplane-1/configuration.nix
];
# Configuration may also be specified inline, as in any nixos config.
services.ray = {
gpus = 4;
cpus = 128;
};
};
};
};
};
}
Secret generator example
- TODO
-
This section should into some kind of fleet documentation… But as there is none, it is just left here as-is.
Quickly run securely setup gitlab
{config, ...}: {
secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {
gitlab-initial-root = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-secret = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-otp = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-db = {
generator = {mkPassword}: mkPassword {};
} // ownership;
gitlab-jws = {
generator = {mkRsa}: mkRsa {};
} // ownership;
};
services.gitlab = let secrets = config.secrets; in {
enable = true;
initialRootPasswordFile = secrets.gitlab-initial-root.secret.path;
secrets = {
secretFile = secrets.gitlab-secret.secret.path;
otpFile = secrets.gitlab-otp.secret.path;
dbFile = secrets.gitlab-db.secret.path;
jwsFile = secrets.gitlab-jws.secret.path;
};
};
}
Securely initialize kubernetes secrets
In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it. This setup should probably split into multiple steps, where I allow target machine to generate CSR, then copy it to the HSM machine, and then sign it there… But this is just the plan. I want to build ansible-like script execution in fleet for this kind of tasks.
{...}: {
# First I define required secret generators:
nixpkgs.overlays = [
(final: prev: let
lib = final.lib;
in {
readKubernetesCa = {impureOn}:
final.mkImpureSecretGenerator ''
cd ~/ca
cert=kubernetes-intermediateCA.crt
expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
echo -n $expires_at > $out/expires_at
cat $cert > $out/public
''
impureOn;
mkKubernetesCert = {
subj,
sans ? [],
impureOn,
}:
final.mkImpureSecretGenerator ''
cd ~/ca
params=$(sudo mktemp)
csr=$(sudo mktemp)
cert=$(sudo mktemp)
sudo openssl ecparam -genkey -name secp384r1 -out $params
sudo openssl req -new -key $params \
-subj "${lib.strings.concatStringsSep""(lib.attrsets.mapAttrsToList(kv"/${k}=${v}")subj)}" \
${lib.optionalString(sans!=[])"-addext \"subjectAltName = ${lib.strings.concatStringsSep","sans}\""} \
-out $csr
sudo hsms x509 -req -days 365 -in $csr -CA kubernetes-intermediateCA.crt -CAkey "pkcs11:object=[CENSORED] Kubernetes Intermediate CA;type=private" -CAcreateserial -copy_extensions copy -out $cert
expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")
echo -n $expires_at > $out/expires_at
sudo cat $params | encrypt > $out/secret
sudo cat $cert > $out/public
''
impureOn;
})
];
# Those secret generators are impure, thus they are run in system environment.
# Probably there needs to be a dedicated user for that kind of tasks, but this is my current setup, don't judge.
# I write a couple of scripts for executing openssl with HSM.
environment.systemPackages = [
pkgs.openssl.bin
(pkgs.writeShellApplication {
name = "hsms";
text = ''
set -eu
export OPENSSL_CONF=${openssl-conf}
# Yay, using secrets to generate secrets!
HSM_PIN=cat ${config.secrets.hsm-pin.secret.path})
exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"
'';
})
(pkgs.writeShellApplication {
name = "hsmt";
text = ''
set -eu
HSM_PIN=cat ${config.secrets.hsm-pin.secret.path})
exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"
'';
})
];
# And finally, I have secrets, which are shared between machines.
# Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
secrets = {
"ca.pem" = {
# This is just the public key, no need to regenerate it to change owner list
regenerateOnOwnerAdded = false;
# For secret regeneration/reencryption, we need to specify which machines SHOULD have it.
expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];
generator = {readKubernetesCa}:
readKubernetesCa {
impureOn = "[CENSORED]";
};
};
"kube-admin.pem" = {
regenerateOnOwnerAdded = false;
expectedOwners = ["cluster-admin"];
generator = {mkKubernetesCert}:
mkKubernetesCert {
subj = {
CN = "admin";
O = "system:masters";
};
impureOn = "[CENSORED]";
};
};
"kube-apiserver.pem" = {
# This secret depends on machine SANS, so if owner list has been changed, then we need to regenerate it.
# However, SANS dependency is in fact handled by secret seed, and secret is regenerated if the seed is changed...
#
# In this case regeneration is added as a half-assed security measure, as if apiserver is removed, we don't
# want for it to be able to pretend like it is a valid server.
#
# However, certificate revokation is complicated in my setup, and I can't show it here.
regenerateOnOwnerAdded = true;
expectedOwners = ["controlplane-1" "controlplane-2"];
generator = {mkKubernetesCert}:
mkKubernetesCert {
inherit sans;
subj.CN = "kubernetes";
impureOn = "[CENSORED]";
};
};
}
docs/examples/_section.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/examples/_section.adoc
@@ -0,0 +1,2 @@
+= Examples
+:order: 2
docs/examples/wireguard.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/examples/wireguard.adoc
@@ -0,0 +1,282 @@
+= WireGuard Mesh Network
+:order: 0
+
+Full-mesh WireGuard VPN with automatic key generation, cross-host peer wiring, and hash-derived IPv6 ULA addresses.
+
+== Directory Structure
+
+----
+.
+├── flake.nix
+└── wireguard/
+ └── default.nix
+----
+
+== flake.nix
+
+[source,nix]
+----
+{...}: {
+ outputs =
+ inputs:
+ inputs.flake-parts.lib.mkFlake { inherit inputs; } {
+ imports = [
+ inputs.fleet.flakeModule
+ ];
+ fleetConfigurations.default = {
+ imports = [
+ (import ./wireguard { iface = "testwg"; })
+ ];
+ cluster.wireguard.testwg = {
+ hosts = [
+ "bernard"
+ "edgeworth"
+ ];
+ port = 51825;
+ };
+ hosts.bernard = {
+ system = "x86_64-linux";
+ network.externalIps = [
+ "78.11.11.11"
+ ];
+ };
+ hosts.edgeworth = {
+ system = "aarch64-linux";
+ };
+ };
+ };
+}
+----
+
+Module is imported with a parameterized interface name (`testwg`), then configured declaratively via `cluster.wireguard.testwg`.
+Hosts without `network.externalIps` (like `edgeworth`) are treated as NAT/roaming — no endpoint is set for them, peers connect outbound only.
+
+== wireguard/default.nix
+
+[source,nix]
+----
+{ iface }:
+{ config, lib, fleetLib, ... }:
+let
+ inherit (lib.attrsets) genAttrs optionalAttrs;
+ inherit (lib.strings) substring;
+ inherit (lib.lists)
+ filter
+ sort
+ unique
+ length
+ elemAt
+ ;
+ inherit (lib.options) mkOption;
+ inherit (lib.types) listOf str int;
+ inherit (lib) hashString lessThan;
+
+ cfg = config.cluster.wireguard.${iface};
+ hostNames = sort lessThan cfg.hosts;
+ hostHash = name: hashString "sha256" (name + iface);
+ hostSubnet =
+ name:
+ let
+ h = hostHash name;
+ in
+ "fd00:${substring 0 4 h}:${substring 4 4 h}:${substring 8 4 h}";
+ hostIPv6 = name: "${hostSubnet name}::1";
+in
+{
+ options.cluster.wireguard.${iface} = {
+ hosts = mkOption {
+ description = "Hostnames participating in this wireguard mesh";
+ type = listOf str;
+ };
+ port = mkOption {
+ description = "WireGuard listen port";
+ type = int;
+ default = 51824;
+ };
+ };
+
+ config = {
+ assertions = [
+ {
+ assertion = length (unique (map hostSubnet hostNames)) == length hostNames;
+ message = "${iface}: hostname-derived IPv6 subnet collision detected";
+ }
+ ];
+ secrets."${iface}-psk" = {
+ expectedOwners = hostNames;
+ generator = fleetLib.mkBase64Bytes { count = 32; };
+ };
+
+ hosts = genAttrs hostNames (name: {
+ nixos =
+ {
+ config,
+ nixosHosts,
+ hosts,
+ ...
+ }:
+ let
+ peerNames = filter (n: n != name) hostNames;
+ in
+ {
+ secrets."${iface}-key" = {
+ generator = fleetLib.mkX25519 { encoding = "base64"; };
+ };
+ secrets."${iface}-psk" = {
+ generator = "shared";
+ };
+
+ networking.wireguard.interfaces.${iface} = {
+ ips = [ "${hostIPv6 name}/64" ];
+ listenPort = cfg.port;
+ privateKeyFile = config.secrets."${iface}-key".secret.path;
+
+ peers = map (
+ peerName:
+ {
+ publicKey = nixosHosts.${peerName}.secrets."${iface}-key".public.data;
+ presharedKeyFile = config.secrets."${iface}-psk".secret.path;
+ allowedIPs = [ "${hostSubnet peerName}::/64" ];
+ }
+ // optionalAttrs (hosts.${peerName}.network.externalIps or [ ] != [ ]) {
+ endpoint = "${elemAt hosts.${peerName}.network.externalIps 0}:${toString cfg.port}";
+ }
+ ) peerNames;
+ };
+
+ networking.firewall.allowedUDPPorts = [ cfg.port ];
+ };
+ });
+ };
+ _file = ./default.nix;
+}
+----
+
+== How It Works
+
+=== Multi-Instance Module
+
+The outer function `{ iface }:` makes the module parameterizable — import it multiple times with different interface names to create independent WireGuard meshes with separate keys and subnets:
+
+[source,nix]
+----
+imports = [
+ (import ./wireguard { iface = "internal"; })
+ (import ./wireguard { iface = "management"; })
+];
+----
+
+=== IPv6 ULA Addressing
+
+Each host gets a unique `fd00::/64` subnet derived from `sha256(hostname + iface)`.
+No manual IP assignment needed — deterministic, collision-checked via assertion.
+
+For example, `bernard` on interface `testwg` gets `fd00:1c86:78d4:dcef::1/64`.
+
+=== Secrets
+
+`${iface}-key`::
+Per-host X25519 keypair via `fleetLib.mkX25519 { encoding = "base64"; }`.
+Private key encrypted in fleet state, public key plaintext — readable by other hosts at build time through `nixosHosts.${peerName}.secrets."${iface}-key".public.data`.
+
+`${iface}-psk`::
+Shared preshared key via `fleetLib.mkBase64Bytes { count = 32; }`.
+Defined at fleet level with `expectedOwners`, each host claims with `generator = "shared"`.
+
+=== NAT Handling
+
+Endpoint is set only for peers that have `network.externalIps`:
+
+[source,nix]
+----
+// optionalAttrs (hosts.${peerName}.network.externalIps or [ ] != [ ]) {
+ endpoint = "...";
+}
+----
+
+Hosts behind NAT initiate connections outward. Add `persistentKeepalive = 25;` if needed to maintain NAT mappings.
+
+== Deployment
+
+Single command generates all secrets and deploys:
+
+[source,shell]
+----
+fleet --only bernard --only edgeworth deploy switch
+----
+
+No separate key generation step — fleet generates and provisions secrets automatically.
+
+=== Fleet State After Deploy
+
+The fleet state file records encryption keys, per-host keypairs (separate distributions), and shared PSK:
+
+[source,nix]
+----
+{
+ hosts = {
+ bernard.encryptionKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ25i6...";
+ edgeworth.encryptionKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIELc1m...";
+ };
+ secrets = {
+ testwg-key = [
+ {
+ owners = [ "bernard" ];
+ createdAt = "2026-04-23T16:39:29.532714437Z";
+ public.raw = "<PLAINTEXT>T7F6A+6jU87kz4c8GftttyU+xB0KhHIaplDV65psb3M=";
+ secret.raw = "<ENCRYPTED><BASE64-ENCODED>...";
+ }
+ {
+ owners = [ "edgeworth" ];
+ createdAt = "2026-04-23T16:39:43.337788671Z";
+ public.raw = "<PLAINTEXT>QgERRQnE+blQaVNMNiQZ+uk+HEOFKVTmvg4O+MqQ0hA=";
+ secret.raw = "<ENCRYPTED><BASE64-ENCODED>...";
+ }
+ ];
+ testwg-psk = {
+ owners = [ "bernard" "edgeworth" ];
+ createdAt = "2026-04-23T16:39:30.126077659Z";
+ secret.raw = "<ENCRYPTED><BASE64-ENCODED>...";
+ };
+ };
+}
+----
+
+Note how `testwg-key` is a list — each host has its own distribution with a unique keypair.
+`testwg-psk` is a single distribution shared by both hosts.
+
+=== Verification
+
+`wg show` on `edgeworth`:
+
+----
+interface: testwg
+ public key: QgERRQnE+blQaVNMNiQZ+uk+HEOFKVTmvg4O+MqQ0hA=
+ private key: (hidden)
+ listening port: 51825
+
+peer: T7F6A+6jU87kz4c8GftttyU+xB0KhHIaplDV65psb3M=
+ preshared key: (hidden)
+ endpoint: 78.11.11.11:51825
+ allowed ips: fd00:1c86:78d4:dcef::/64
+ latest handshake: 1 second ago
+ transfer: 616 B received, 760 B sent
+----
+
+`wg show` on `bernard`:
+
+----
+interface: testwg
+ public key: T7F6A+6jU87kz4c8GftttyU+xB0KhHIaplDV65psb3M=
+ private key: (hidden)
+ listening port: 51825
+
+peer: QgERRQnE+blQaVNMNiQZ+uk+HEOFKVTmvg4O+MqQ0hA=
+ preshared key: (hidden)
+ endpoint: 109.11.11.11:51825
+ allowed ips: fd00:cac2:10cb:6ee9::/64
+ latest handshake: 7 minutes, 18 seconds ago
+ transfer: 468 B received, 380 B sent
+----
+
+Public keys match between state file and `wg show`. `edgeworth` has no external IP in fleet config, yet `bernard` sees its endpoint — `edgeworth` initiated the connection outward through NAT.
docs/features/_section.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/features/_section.adoc
@@ -0,0 +1,2 @@
+= Features
+:order: 1
docs/features/modules.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/features/modules.adoc
@@ -0,0 +1,51 @@
+= Multi-host Modules
+:order: 0
+
+Fleet modules configure multiple hosts simultaneously. Unlike standard NixOS modules which configure a single machine, fleet modules have access to all hosts in the cluster.
+
+== Basic Module
+
+[source,nix]
+----
+# wireguard/default.nix
+{ config, ... }: {
+ hosts = builtins.mapAttrs (name: host: {
+ nixos.networking.wireguard.interfaces.wg0 = {
+ ips = [ host.config.wireguardIp ];
+ peers = builtins.filter (p: p.name != name)
+ (builtins.attrValues config.hosts);
+ };
+ }) config.hosts;
+}
+----
+
+== Using Modules
+
+Modules are imported in the `fleetConfigurations` block:
+
+[source,nix]
+----
+fleetConfigurations.default = {
+ imports = [
+ ./wireguard
+ ./monitoring
+ ];
+
+ hosts.node-1 = { ... };
+ hosts.node-2 = { ... };
+};
+----
+
+== Multi-instance Modules
+
+Modules can be parameterized and imported multiple times:
+
+[source,nix]
+----
+fleetConfigurations.default = {
+ imports = [
+ (import ./kubernetes { hosts = ["cp-1" "cp-2"]; })
+ (import ./kubernetes { hosts = ["cp-3" "cp-4"]; })
+ ];
+};
+----
docs/features/rollback.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/features/rollback.adoc
@@ -0,0 +1,29 @@
+
+= Automatic Rollback
+:order: 2
+
+Fleet automatically rolls back deployments that fail, preventing bricked machines.
+
+== How It Works
+
+When deploying a new system configuration:
+
+1. Fleet activates the new configuration
+2. A health check verifies the system booted successfully
+3. If the check fails, the previous configuration is restored
+
+Rollback works as long as the system passes the initrd stage. Be careful with changes to root filesystem mounts — those are harder to recover from.
+
+== Deployment Process
+
+[source,shell]
+----
+# Deploy to all hosts
+fleet deploy
+
+# Deploy to specific host
+fleet deploy web-1
+
+# Deploy without rollback (not recommended)
+fleet deploy --no-rollback
+----
docs/features/secrets.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/features/secrets.adoc
@@ -0,0 +1,30 @@
+= Fleet Secrets Management System
+:order: 1
+
+== Overview
+
+Secret management system is a built-in way to deploy secrets to remote hosts, similar to agenix and other systems.
+
+Secrets are encrypted using system's host ssh key (/etc/ssh/ssh_host_ed25519_key), which is not required to build the
+remote system/add secret to fleet configuration, fleet users are encrypting secrets using received public key instead,
+they don't need the root access to receive the public encryption key.
+
+== Example
+
+[source,nix]
+----
+{
+ secrets = {
+ "my-secret" = {
+ expectedOwners = [ "host1" "host2" ];
+ regenerateOnOwnerAdded = true;
+ generator = {mkImpureSecretGenerator}:
+ mkImpureSecretGenerator {
+ script = ''
+ echo "secret content" | gh private -o $out/secret
+ '';
+ };
+ };
+ }
+}
+----
docs/getting-started/_section.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/getting-started/_section.adoc
@@ -0,0 +1,2 @@
+= Getting Started
+:order: 0
docs/getting-started/hosts.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/getting-started/hosts.adoc
@@ -0,0 +1,58 @@
+
+= Configuring Hosts
+:order: 1
+
+Each host in a fleet configuration represents a machine in your cluster.
+
+== Defining Hosts
+
+[source,nix]
+----
+fleetConfigurations.default = {
+ hosts.web-1 = {
+ system = "x86_64-linux";
+ nixos = {
+ imports = [
+ ./web-1/hardware-configuration.nix
+ ./web-1/configuration.nix
+ ];
+ };
+ };
+
+ hosts.web-2 = {
+ system = "x86_64-linux";
+ nixos = {
+ imports = [
+ ./web-2/hardware-configuration.nix
+ ./web-2/configuration.nix
+ ];
+ };
+ };
+};
+----
+
+== Host Systems
+
+Every host must specify a `system` attribute. Supported systems:
+
+- `x86_64-linux`
+- `aarch64-linux`
+- `armv7l-linux`
+- `armv6l-linux`
+
+== Inline NixOS Configuration
+
+NixOS configuration can be specified inline:
+
+[source,nix]
+----
+hosts.monitoring = {
+ system = "x86_64-linux";
+ nixos = {
+ imports = [ ./monitoring/hardware-configuration.nix ];
+ services.prometheus.enable = true;
+ services.grafana.enable = true;
+ networking.firewall.allowedTCPPorts = [ 3000 9090 ];
+ };
+};
+----
docs/getting-started/index.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/getting-started/index.adoc
@@ -0,0 +1,77 @@
+
+= Fleet
+:order: 0
+
+An NixOS cluster deployment tool.
+
+== Advantages over existing configuration systems (NixOps/Morph)
+
+- Modules can configure multiple hosts at once (i.e. for wireguard/kubernetes installation)
+- Secrets can be securely stored in Git (no one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.
+- Automatic rollback on deployment failure, which will work as long as system is passing initrd stage
+
+== Flake example
+
+[source,nix]
+----
+{
+ description = "My cluster configuration";
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs";
+ fleet = {
+ url = "github:CertainLach/fleet";
+ inputs.nixpkgs.follows = "nixpkgs";
+ };
+ flake-parts.url = "github:hercules-ci/flake-parts";
+ };
+ outputs = inputs:
+ inputs.flake-parts.lib.mkFlake {inherit inputs;} {
+ imports = [inputs.fleet.flakeModules.default];
+
+ fleetConfigurations.default = {
+ nixos = {
+ # Shared NixOS configuration for all hosts
+ };
+
+ imports = [
+ ./wireguard
+ ];
+
+ hosts.controlplane-1 = {
+ system = "x86_64-linux";
+ nixos = {
+ imports = [
+ ./controlplane-1/hardware-configuration.nix
+ ./controlplane-1/configuration.nix
+ ];
+ };
+ };
+ };
+ };
+}
+----
+
+== Secret generator example
+
+[source,nix]
+----
+{config, ...}: {
+ secrets = {
+ gitlab-initial-root = {
+ generator = {mkPassword}: mkPassword {};
+ owner = "gitlab";
+ group = "gitlab";
+ };
+ gitlab-secret = {
+ generator = {mkPassword}: mkPassword {};
+ owner = "gitlab";
+ group = "gitlab";
+ };
+ };
+ services.gitlab = {
+ enable = true;
+ initialRootPasswordFile = config.secrets.gitlab-initial-root.secret.path;
+ secrets.secretFile = config.secrets.gitlab-secret.secret.path;
+ };
+}
+----
docs/index.adocdiffbeforeafterboth--- a/docs/index.adoc
+++ /dev/null
@@ -1,78 +0,0 @@
-
-= Fleet
-:slug: index
-:order: 0
-
-An NixOS cluster deployment tool.
-
-== Advantages over existing configuration systems (NixOps/Morph)
-
-- Modules can configure multiple hosts at once (i.e. for wireguard/kubernetes installation)
-- Secrets can be securely stored in Git (no one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.
-- Automatic rollback on deployment failure, which will work as long as system is passing initrd stage
-
-== Flake example
-
-[source,nix]
-----
-{
- description = "My cluster configuration";
- inputs = {
- nixpkgs.url = "github:nixos/nixpkgs";
- fleet = {
- url = "github:CertainLach/fleet";
- inputs.nixpkgs.follows = "nixpkgs";
- };
- flake-parts.url = "github:hercules-ci/flake-parts";
- };
- outputs = inputs:
- inputs.flake-parts.lib.mkFlake {inherit inputs;} {
- imports = [inputs.fleet.flakeModules.default];
-
- fleetConfigurations.default = {
- nixos = {
- # Shared NixOS configuration for all hosts
- };
-
- imports = [
- ./wireguard
- ];
-
- hosts.controlplane-1 = {
- system = "x86_64-linux";
- nixos = {
- imports = [
- ./controlplane-1/hardware-configuration.nix
- ./controlplane-1/configuration.nix
- ];
- };
- };
- };
- };
-}
-----
-
-== Secret generator example
-
-[source,nix]
-----
-{config, ...}: {
- secrets = {
- gitlab-initial-root = {
- generator = {mkPassword}: mkPassword {};
- owner = "gitlab";
- group = "gitlab";
- };
- gitlab-secret = {
- generator = {mkPassword}: mkPassword {};
- owner = "gitlab";
- group = "gitlab";
- };
- };
- services.gitlab = {
- enable = true;
- initialRootPasswordFile = config.secrets.gitlab-initial-root.secretPath;
- secrets.secretFile = config.secrets.gitlab-secret.secretPath;
- };
-}
-----
docs/migration.adocdiffbeforeafterboth--- a/docs/migration.adoc
+++ /dev/null
@@ -1,42 +0,0 @@
-
-= Migration Guide
-:slug: migration
-:order: 2
-
-== fleet.nix <unset> => 0.1.0
-
-Add version field::
-Set it to 0.1.0; This field specifies which version of fleet do you use for cluster management, breaking changes will also break this value to make sure you read this guide.
-
-Move every secret part::
-Before it was only public and private, now it can be any number of parts.
-
-In your fleet.nix file, look at every record like this:
-[source,nix]
-----
-gitlab-initial-root = {
- createdAt = "2024-03-01T15:54:32.983358495Z";
- public = "example";
- secret = "vp%d6wO#0#D2.../dgCA+v4Gf:YG";
-};
-----
-
-And modify it as following:
-[source,nix]
-----
-gitlab-initial-root = {
- createdAt = "2024-03-01T15:54:32.983358495Z";
- public.raw = "<PLAINTEXT>example";
- secret.raw = ''
- <ENCRYPTED><Z85-ENCODED>
- vp%d6wO#0#D2.../dgCA+v4Gf:YG
- '';
-};
-----
-
-Default encoding was also changed from `Z85` to `base64`. This conversion will be done by fleet automatically.
-
-Update references to secrets in fleet/nixos configurations::
-Instead of `config.secrets.secret-name.secretPath` use `config.secrets.secret-name.secret.path`,
-instead of `config.secrets.secret-name.stableSecretPath` use `config.secrets.secret-name.secret.stablePath`,
-instead of `config.secrets.secret-name.public` use `config.secrets.secret-name.public.data`.
docs/reference/_section.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/reference/_section.adoc
@@ -0,0 +1,2 @@
+= Reference
+:order: 2
docs/reference/migration.adocdiffbeforeafterboth--- /dev/null
+++ b/docs/reference/migration.adoc
@@ -0,0 +1,41 @@
+
+= Migration Guide
+:order: 2
+
+== fleet.nix <unset> => 0.1.0
+
+Add version field::
+Set it to 0.1.0; This field specifies which version of fleet do you use for cluster management, breaking changes will also break this value to make sure you read this guide.
+
+Move every secret part::
+Before it was only public and private, now it can be any number of parts.
+
+In your fleet.nix file, look at every record like this:
+[source,nix]
+----
+gitlab-initial-root = {
+ createdAt = "2024-03-01T15:54:32.983358495Z";
+ public = "example";
+ secret = "vp%d6wO#0#D2.../dgCA+v4Gf:YG";
+};
+----
+
+And modify it as following:
+[source,nix]
+----
+gitlab-initial-root = {
+ createdAt = "2024-03-01T15:54:32.983358495Z";
+ public.raw = "<PLAINTEXT>example";
+ secret.raw = ''
+ <ENCRYPTED><Z85-ENCODED>
+ vp%d6wO#0#D2.../dgCA+v4Gf:YG
+ '';
+};
+----
+
+Default encoding was also changed from `Z85` to `base64`. This conversion will be done by fleet automatically.
+
+Update references to secrets in fleet/nixos configurations::
+Instead of `config.secrets.secret-name.secretPath` use `config.secrets.secret-name.secret.path`,
+instead of `config.secrets.secret-name.stableSecretPath` use `config.secrets.secret-name.secret.stablePath`,
+instead of `config.secrets.secret-name.public` use `config.secrets.secret-name.public.data`.
docs/secrets.adocdiffbeforeafterboth--- a/docs/secrets.adoc
+++ /dev/null
@@ -1,37 +0,0 @@
-= Fleet Secrets Management System
-:slug: secrets
-:order: 1
-
-== Overview
-
-Secret management system is a built-in way to deploy secrets to remote systems, similar to agenix and other similar systems.
-
-Secrets are encrypted using system's host ssh key (/etc/ssh/ssh_host_ed25519_key), which is not required to build the
-remote system/add secret to fleet configuration, fleet users are encrypting secrets using received public key instead,
-they don't need the root access to receive the public encryption key.
-
-== Example
-
-[source,nix]
-----
-{
- fleet.secrets = {
- "my-secret" = {
- expectedOwners = [ "host1" "host2" ];
- regenerateOnOwnerAdded = true;
- generator = {mkImpureSecretGenerator}:
- mkImpureSecretGenerator {
- script = ''
- echo "secret content" | gh private -o $out/secret
- '';
- };
- };
- }
-}
-----
-
-== Limitations and Future Improvements
-
-- Pure secret generators are currently disabled
-- Support for other secret management systems (e.g systemd-creds has planned asymmetric encryption support)
-
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -160,7 +160,6 @@
mkImpureSecretGenerator,
}:
mkImpureSecretGenerator {
- # TODO: Escape prompt/part (preferrably just use env) to prevent shell injection
script = ''
mkdir $out
${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}