1= Fleet23++++4<p align="center"><a href="https://github.com/CertainLach/fleet"><img alt="fleet temporary logo generated with midjourney" src="./docs/tmplogo.png" width="200px"></img></a></p>5++++67An NixOS cluster deployment tool.89== Advantages over existing configuration systems (NixOps/Morph)1011- Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)12- Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.13- Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)1415== Flake example1617[source,nix]18----19{20 description = "My cluster configuration";21 inputs = {22 nixpkgs.url = "github:nixos/nixpkgs";23 fleet = {24 url = "github:CertainLach/fleet";25 inputs.nixpkgs.follows = "nixpkgs";26 };27 flake-parts.url = "github:hercules-ci/flake-parts";28 lanzaboote = {29 url = "github:nix-community/lanzaboote/v0.3.0";30 inputs.nixpkgs.follows = "nixpkgs";31 };32 };33 outputs = inputs:34 inputs.flake-parts.lib.mkFlake {inherit inputs;} {35 imports = [inputs.fleet.flakeModules.default];3637 perSystem = {38 inputs',39 pkgs,40 system,41 ...42 }: {43 formatter = pkgs.alejandra;44 devShells.default =45 pkgs.mkShell {packages = [inputs'.fleet.packages.fleet];};46 };4748 49 fleetConfigurations.default = {50 51 nixos = {52 imports = [inputs.lanzaboote.nixosModules.lanzaboote];5354 55 nix.registry.nixpkgs = {56 from = {57 id = "nixpkgs";58 type = "indirect";59 };60 flake = inputs.nixpkgs;61 exact = false;62 };63 };6465 66 67 68 69 imports = [70 ./wireguard71 72 (import ./kubernetes {hosts = ["a" "b"];})73 (import ./kubernetes {hosts = ["c" "d"];})74 ];7576 77 hosts.controlplane-1 = {78 79 system = "x86_64-linux";80 nixos = {81 82 imports = [83 ./controlplane-1/hardware-configuration.nix84 ./controlplane-1/configuration.nix85 ];86 87 services.ray = {88 gpus = 4;89 cpus = 128;90 };91 };92 };93 };94 };95}96----9798== Secret generator example99100TODO:: This section should into some kind of fleet documentation... But as there is none, it is just left here as-is.101102=== Quickly run securely setup gitlab103104[source,nix]105----106{config, ...}: {107 secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {108 gitlab-initial-root = {109 generator = {mkPassword}: mkPassword {};110 } // ownership;111 gitlab-secret = {112 generator = {mkPassword}: mkPassword {};113 } // ownership;114 gitlab-otp = {115 generator = {mkPassword}: mkPassword {};116 } // ownership;117 gitlab-db = {118 generator = {mkPassword}: mkPassword {};119 } // ownership;120 gitlab-jws = {121 generator = {mkRsa}: mkRsa {};122 } // ownership;123 };124 services.gitlab = let secrets = config.secrets; in {125 enable = true;126 initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;127 secrets = {128 secretFile = secrets.gitlab-secret.secretPath;129 otpFile = secrets.gitlab-otp.secretPath;130 dbFile = secrets.gitlab-db.secretPath;131 jwsFile = secrets.gitlab-jws.secretPath;132 };133 };134}135----136137=== Securely initialize kubernetes secrets138139In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it.140This 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.141I want to build ansible-like script execution in fleet for this kind of tasks.142143[source,nix]144----145{...}: {146 147 nixpkgs.overlays = [148 (final: prev: let149 lib = final.lib;150 in {151 readKubernetesCa = {impureOn}:152 final.mkImpureSecretGenerator ''153 cd ~/ca154155 cert=kubernetes-intermediateCA.crt156157 expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")158 echo -n $expires_at > $out/expires_at159160 cat $cert > $out/public161 ''162 impureOn;163 mkKubernetesCert = {164 subj,165 sans ? [],166 impureOn,167 }:168 final.mkImpureSecretGenerator ''169 cd ~/ca170171 params=$(sudo mktemp)172 csr=$(sudo mktemp)173 cert=$(sudo mktemp)174 sudo openssl ecparam -genkey -name secp384r1 -out $params175 sudo openssl req -new -key $params \176 -subj "${lib.strings.concatStringsSep "" (lib.attrsets.mapAttrsToList (k: v: "/${k}=${v}") subj)}" \177 ${lib.optionalString (sans != []) "-addext \"subjectAltName = ${lib.strings.concatStringsSep "," sans}\""} \178 -out $csr179 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 $cert180181 expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")182 echo -n $expires_at > $out/expires_at183184 sudo cat $params | encrypt > $out/secret185 sudo cat $cert > $out/public186 ''187 impureOn;188 })189 ];190 191 192 193 environment.systemPackages = [194 pkgs.openssl.bin195 (pkgs.writeShellApplication {196 name = "hsms";197 text = ''198 set -eu199 export OPENSSL_CONF=${openssl-conf}200 201 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})202 exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"203 '';204 })205 (pkgs.writeShellApplication {206 name = "hsmt";207 text = ''208 set -eu209 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})210 exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"211 '';212 })213 ];214 215 216 sharedSecrets = {217 "ca.pem" = {218 219 regenerateOnOwnerAdded = false;220 221 expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];222 generator = {readKubernetesCa}:223 readKubernetesCa {224 impureOn = "[CENSORED]";225 };226 };227 "kube-admin.pem" = {228 regenerateOnOwnerAdded = false;229 expectedOwners = ["cluster-admin"];230 generator = {mkKubernetesCert}:231 mkKubernetesCert {232 subj = {233 CN = "admin";234 O = "system:masters";235 };236 impureOn = "[CENSORED]";237 };238 };239 "kube-apiserver.pem" = {240 241 242 243 244 245 246 247 regenerateOnOwnerAdded = true;248 expectedOwners = ["controlplane-1" "controlplane-2"];249 generator = {mkKubernetesCert}:250 mkKubernetesCert {251 inherit sans;252 subj.CN = "kubernetes";253 impureOn = "[CENSORED]";254 };255 };256}257----