1++++2<p align="center"><a href="https://github.com/CertainLach/fleet"><img alt="fleet logo" src="./docs/logo.svg" height="150"></img></a></p>3++++45An NixOS cluster deployment tool.67== Advantages over existing configuration systems (NixOps/Morph)89- Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)10- Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.11- Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)1213== Flake example1415[source,nix]16----17{18 description = "My cluster configuration";19 inputs = {20 nixpkgs.url = "github:nixos/nixpkgs";21 fleet = {22 url = "github:CertainLach/fleet";23 inputs.nixpkgs.follows = "nixpkgs";24 };25 flake-parts.url = "github:hercules-ci/flake-parts";26 lanzaboote = {27 url = "github:nix-community/lanzaboote/v0.3.0";28 inputs.nixpkgs.follows = "nixpkgs";29 };30 };31 outputs = inputs:32 inputs.flake-parts.lib.mkFlake {inherit inputs;} {33 imports = [inputs.fleet.flakeModules.default];3435 perSystem = {36 inputs',37 pkgs,38 system,39 ...40 }: {41 formatter = pkgs.alejandra;42 devShells.default =43 pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};44 };4546 47 fleetConfigurations.default = {48 49 nixos = {50 imports = [inputs.lanzaboote.nixosModules.lanzaboote];5152 53 nix.registry.nixpkgs = {54 from = {55 id = "nixpkgs";56 type = "indirect";57 };58 flake = inputs.nixpkgs;59 exact = false;60 };61 };6263 64 65 66 67 imports = [68 ./wireguard69 70 (import ./kubernetes {hosts = ["a" "b"];})71 (import ./kubernetes {hosts = ["c" "d"];})72 ];7374 75 hosts.controlplane-1 = {76 77 system = "x86_64-linux";78 nixos = {79 80 imports = [81 ./controlplane-1/hardware-configuration.nix82 ./controlplane-1/configuration.nix83 ];84 85 services.ray = {86 gpus = 4;87 cpus = 128;88 };89 };90 };91 };92 };93}94----9596== Secret generator example9798TODO:: This section should into some kind of fleet documentation... But as there is none, it is just left here as-is.99100=== Quickly run securely setup gitlab101102[source,nix]103----104{config, ...}: {105 secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {106 gitlab-initial-root = {107 generator = {mkPassword}: mkPassword {};108 } // ownership;109 gitlab-secret = {110 generator = {mkPassword}: mkPassword {};111 } // ownership;112 gitlab-otp = {113 generator = {mkPassword}: mkPassword {};114 } // ownership;115 gitlab-db = {116 generator = {mkPassword}: mkPassword {};117 } // ownership;118 gitlab-jws = {119 generator = {mkRsa}: mkRsa {};120 } // ownership;121 };122 services.gitlab = let secrets = config.secrets; in {123 enable = true;124 initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;125 secrets = {126 secretFile = secrets.gitlab-secret.secretPath;127 otpFile = secrets.gitlab-otp.secretPath;128 dbFile = secrets.gitlab-db.secretPath;129 jwsFile = secrets.gitlab-jws.secretPath;130 };131 };132}133----134135=== Securely initialize kubernetes secrets136137In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it.138This 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.139I want to build ansible-like script execution in fleet for this kind of tasks.140141[source,nix]142----143{...}: {144 145 nixpkgs.overlays = [146 (final: prev: let147 lib = final.lib;148 in {149 readKubernetesCa = {impureOn}:150 final.mkImpureSecretGenerator ''151 cd ~/ca152153 cert=kubernetes-intermediateCA.crt154155 expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")156 echo -n $expires_at > $out/expires_at157158 cat $cert > $out/public159 ''160 impureOn;161 mkKubernetesCert = {162 subj,163 sans ? [],164 impureOn,165 }:166 final.mkImpureSecretGenerator ''167 cd ~/ca168169 params=$(sudo mktemp)170 csr=$(sudo mktemp)171 cert=$(sudo mktemp)172 sudo openssl ecparam -genkey -name secp384r1 -out $params173 sudo openssl req -new -key $params \174 -subj "${lib.strings.concatStringsSep "" (lib.attrsets.mapAttrsToList (k: v: "/${k}=${v}") subj)}" \175 ${lib.optionalString (sans != []) "-addext \"subjectAltName = ${lib.strings.concatStringsSep "," sans}\""} \176 -out $csr177 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 $cert178179 expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")180 echo -n $expires_at > $out/expires_at181182 sudo cat $params | encrypt > $out/secret183 sudo cat $cert > $out/public184 ''185 impureOn;186 })187 ];188 189 190 191 environment.systemPackages = [192 pkgs.openssl.bin193 (pkgs.writeShellApplication {194 name = "hsms";195 text = ''196 set -eu197 export OPENSSL_CONF=${openssl-conf}198 199 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})200 exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"201 '';202 })203 (pkgs.writeShellApplication {204 name = "hsmt";205 text = ''206 set -eu207 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})208 exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"209 '';210 })211 ];212 213 214 secrets = {215 "ca.pem" = {216 217 regenerateOnOwnerAdded = false;218 219 expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];220 generator = {readKubernetesCa}:221 readKubernetesCa {222 impureOn = "[CENSORED]";223 };224 };225 "kube-admin.pem" = {226 regenerateOnOwnerAdded = false;227 expectedOwners = ["cluster-admin"];228 generator = {mkKubernetesCert}:229 mkKubernetesCert {230 subj = {231 CN = "admin";232 O = "system:masters";233 };234 impureOn = "[CENSORED]";235 };236 };237 "kube-apiserver.pem" = {238 239 240 241 242 243 244 245 regenerateOnOwnerAdded = true;246 expectedOwners = ["controlplane-1" "controlplane-2"];247 generator = {mkKubernetesCert}:248 mkKubernetesCert {249 inherit sans;250 subj.CN = "kubernetes";251 impureOn = "[CENSORED]";252 };253 };254}255----