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 lanzaboote = {28 url = "github:nix-community/lanzaboote/v0.3.0";29 inputs.nixpkgs.follows = "nixpkgs";30 };31 };32 outputs = {33 nixpkgs,34 fleet,35 lanzaboote,36 ...37 }: {38 39 formatter.x86_64-linux = let40 pkgs = import nixpkgs {system = "x86_64-linux";};41 in42 pkgs.alejandra;4344 devShell.x86_64-linux = let45 pkgs = import nixpkgs {46 system = "x86_64-linux";47 };48 in49 pkgs.mkShell {50 buildInputs = with pkgs; [51 fleet.packages.x86_64-linux.fleet52 ];53 };5455 56 fleetConfigurations.default = fleet.lib.fleetConfiguration {57 58 inherit nixpkgs;59 60 61 data = import ./fleet.nix;62 63 64 nixosModules = [65 lanzaboote.nixosModules.lanzaboote66 ({67 config,68 lib,69 ...70 }: {71 72 nix.registry.nixpkgs = {73 from = { id = "nixpkgs"; type = "indirect"; };74 flake = nixpkgs;75 exact = false;76 };77 })78 ];7980 81 82 83 84 fleetModules = [85 ./wireguard86 87 (import ./kubernetes {hosts = ["a" "b"];})88 (import ./kubernetes {hosts = ["c" "d"];})89 ];9091 92 hosts.controlplane-1 = {93 94 system = "x86_64-linux";95 96 nixosModules = [97 ./controlplane-1/hardware-configuration.nix98 ./controlplane-1/configuration.nix99 100 ({...}: {101 services.ray = {102 gpus = 4;103 cpus = 128;104 };105 })106 ];107 };108 };109 };110}111----112113== Secret generator example114115TODO:: This section should into some kind of fleet documentation... But as there is none, it is just left here as-is.116117=== Quickly run securely setup gitlab118119[source,nix]120----121{config, ...}: {122 secrets = let ownership = { owner = "gitlab"; group = "gitlab"; }; in {123 gitlab-initial-root = {124 generator = {mkPassword}: mkPassword {};125 } // ownership;126 gitlab-secret = {127 generator = {mkPassword}: mkPassword {};128 } // ownership;129 gitlab-otp = {130 generator = {mkPassword}: mkPassword {};131 } // ownership;132 gitlab-db = {133 generator = {mkPassword}: mkPassword {};134 } // ownership;135 gitlab-jws = {136 generator = {mkRsa}: mkRsa {};137 } // ownership;138 };139 services.gitlab = let secrets = config.secrets; in {140 enable = true;141 initialRootPasswordFile = secrets.gitlab-initial-root.secretPath;142 secrets = {143 secretFile = secrets.gitlab-secret.secretPath;144 otpFile = secrets.gitlab-otp.secretPath;145 dbFile = secrets.gitlab-db.secretPath;146 jwsFile = secrets.gitlab-jws.secretPath;147 };148 };149}150----151152=== Securely initialize kubernetes secrets153154In my homelab and clusters, I almost always have some sort of HSM, and to issue new kubernetes certs I directly connect to it.155This 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.156I want to build ansible-like script execution in fleet for this kind of tasks.157158[source,nix]159----160{...}: {161 162 nixpkgs.overlays = [163 (final: prev: let164 lib = final.lib;165 in {166 readKubernetesCa = {impureOn}:167 final.mkImpureSecretGenerator ''168 cd ~/ca169170 cert=kubernetes-intermediateCA.crt171172 expires_at=$(openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")173 echo -n $expires_at > $out/expires_at174175 cat $cert > $out/public176 ''177 impureOn;178 mkKubernetesCert = {179 subj,180 sans ? [],181 impureOn,182 }:183 final.mkImpureSecretGenerator ''184 cd ~/ca185186 params=$(sudo mktemp)187 csr=$(sudo mktemp)188 cert=$(sudo mktemp)189 sudo openssl ecparam -genkey -name secp384r1 -out $params190 sudo openssl req -new -key $params \191 -subj "${lib.strings.concatStringsSep "" (lib.attrsets.mapAttrsToList (k: v: "/${k}=${v}") subj)}" \192 ${lib.optionalString (sans != []) "-addext \"subjectAltName = ${lib.strings.concatStringsSep "," sans}\""} \193 -out $csr194 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 $cert195196 expires_at=$(sudo openssl x509 -in $cert -noout -enddate | cut -d= -f2 | xargs -I{} date -u -d {} +"%Y-%m-%dT%H:%M:%S.%NZ")197 echo -n $expires_at > $out/expires_at198199 sudo cat $params | encrypt > $out/secret200 sudo cat $cert > $out/public201 ''202 impureOn;203 })204 ];205 206 207 208 environment.systemPackages = [209 pkgs.openssl.bin210 (pkgs.writeShellApplication {211 name = "hsms";212 text = ''213 set -eu214 export OPENSSL_CONF=${openssl-conf}215 216 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})217 exec ${pkgs.openssl}/bin/openssl "$@" -keyform=engine -CAkeyform=engine -engine=pkcs11 -passin=pass:"$HSM_PIN"218 '';219 })220 (pkgs.writeShellApplication {221 name = "hsmt";222 text = ''223 set -eu224 HSM_PIN=$(cat ${config.secrets.hsm-pin.secretPath})225 exec ${pkgs.opensc}/bin/pkcs11-tool -l --pin="$HSM_PIN" "$@"226 '';227 })228 ];229 230 231 sharedSecrets = {232 "ca.pem" = {233 234 regenerateOnOwnerAdded = false;235 236 expectedOwners = ["controlplane-1" "controlplane-2" "worker-1" "worker-2"];237 generator = {readKubernetesCa}:238 readKubernetesCa {239 impureOn = "[CENSORED]";240 };241 };242 "kube-admin.pem" = {243 regenerateOnOwnerAdded = false;244 expectedOwners = ["cluster-admin"];245 generator = {mkKubernetesCert}:246 mkKubernetesCert {247 subj = {248 CN = "admin";249 O = "system:masters";250 };251 impureOn = "[CENSORED]";252 };253 };254 "kube-apiserver.pem" = {255 256 257 258 259 260 261 262 regenerateOnOwnerAdded = true;263 expectedOwners = ["controlplane-1" "controlplane-2"];264 generator = {mkKubernetesCert}:265 mkKubernetesCert {266 inherit sans;267 subj.CN = "kubernetes";268 impureOn = "[CENSORED]";269 };270 };271}272----