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: flake-parts.lib.mkFlake { inherit inputs; } {34 imports = [inputs.fleet.flakeModules.default];3536 perSystem = {pkgs, system, ...}: {37 _module.args.pkgs = import nixpkgs { inherit system; };3839 formatter = pkgs.alejandra;40 devShells.default = pkgs.mkShell {41 packages = [42 inputs.fleet.packages.${system}.fleet43 ];44 };45 };4647 48 fleetConfigurations.default = {49 50 nixpkgs.buildUsing = nixpkgs;51 52 53 nixos.imports = [54 lanzaboote.nixosModules.lanzaboote55 {56 57 nix.registry.nixpkgs = {58 from = { id = "nixpkgs"; type = "indirect"; };59 flake = nixpkgs;60 exact = false;61 };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 81 nixos.imports = [82 ./controlplane-1/hardware-configuration.nix83 ./controlplane-1/configuration.nix84 85 {86 services.ray = {87 gpus = 4;88 cpus = 128;89 };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----