git.delta.rocks / jrsonnet / refs/commits / 989a90dd7b97

difftreelog

feat move secret generation helpers to core

Yaroslav Bolyukin2024-03-02parent: #a31940c.patch.diff
in: trunk

13 files changed

modifiedCargo.lockdiffbeforeafterboth
before · Cargo.lock
330 packageslockfile v3
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,5 +6,5 @@
 nixlike = { path = "./crates/nixlike" }
 better-command = { path = "./crates/better-command" }
 bifrostlink = "0.1.0"
-uuid = { version = "1.3.3", features = ["v4"] }
-tokio = { version = "1.33.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }
+uuid = { version = "1.7.0", features = ["v4"] }
+tokio = { version = "1.36.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }
modifiedREADME.adocdiffbeforeafterboth
--- a/README.adoc
+++ b/README.adoc
@@ -11,3 +11,164 @@
 - 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)
+
+== 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
+
+[source,nix]
+----
+{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.
+
+[source,nix]
+----
+{...}: {
+  # 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 (k: v: "/${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.
+  sharedSecrets = {
+    "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]";
+        };
+    };
+}
+----
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -12,17 +12,17 @@
 anyhow = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-time = { version = "0.3.30", features = ["serde"] }
-tempfile = "3.8"
-once_cell = "1.18.0"
-hostname = "0.3.1"
-age-core = "0.9.0"
-peg = "0.8.2"
-age = { version = "0.9.2", features = ["ssh", "armor"] }
-base64 = "0.21.5"
-chrono = { version = "0.4.31", features = ["serde"] }
-z85 = "3.0.5"
-clap = { version = "4.4.7", features = [
+time = { version = "0.3", features = ["serde"] }
+tempfile = "3.10"
+once_cell = "1.19"
+hostname = "0.3"
+age-core = "0.10"
+peg = "0.8"
+age = { version = "0.10", features = ["ssh", "armor"] }
+base64 = "0.21"
+chrono = { version = "0.4", features = ["serde"] }
+z85 = "3.0"
+clap = { version = "4.5", features = [
 	"derive",
 	"env",
 	"wrap_help",
@@ -30,18 +30,18 @@
 ] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
-tokio-util = { version = "0.7.10", features = ["codec"] }
-async-trait = "0.1.74"
-futures = "0.3.29"
-tracing-indicatif = "0.3.5"
-indicatif = "0.17.7"
-itertools = "0.11.0"
-shlex = "1.2.0"
-tabled = { version = "0.14.0", features = ["color"] }
-owo-colors = { version = "3.5.0", features = ["supports-color", "supports-colors"] }
+tokio-util = { version = "0.7", features = ["codec"] }
+async-trait = "0.1"
+futures = "0.3"
+tracing-indicatif = "0.3"
+indicatif = "0.17"
+itertools = "0.12"
+shlex = "1.3"
+tabled = { version = "0.15" }
+owo-colors = { version = "4.0", features = ["supports-color", "supports-colors"] }
 r2d2 = "0.8.10"
-abort-on-drop = "0.2.2"
-unindent = "0.2.3"
-regex = "1.10.2"
-openssh = "0.10.1"
-human-repr = "1.1.0"
+abort-on-drop = "0.2"
+unindent = "0.2"
+regex = "1.10"
+openssh = "0.10"
+human-repr = "1.1"
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,5 +1,6 @@
 use crate::{
 	better_nix_eval::Field,
+	command::MyCommand,
 	fleetdata::{FleetSecret, FleetSharedSecret, SecretData},
 	host::Config,
 	nix_go, nix_go_json,
@@ -8,14 +9,16 @@
 use chrono::{DateTime, Utc};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use serde::Deserialize;
+use serde::{de::DeserializeOwned, Deserialize};
 use std::{
 	collections::{BTreeSet, HashSet},
 	io::{self, Cursor, Read},
-	path::PathBuf,
+	path::{Path, PathBuf},
+	str::FromStr,
 };
 use tabled::{Table, Tabled};
-use tokio::fs::read_to_string;
+use tempfile::tempdir;
+use tokio::fs::{self, read_to_string};
 use tracing::{error, info, info_span, warn, Instrument};
 
 #[derive(Parser)]
@@ -67,6 +70,10 @@
 		#[clap(long)]
 		plaintext: bool,
 	},
+	ReadPublic {
+		name: String,
+		machine: String,
+	},
 	UpdateShared {
 		name: String,
 
@@ -151,8 +158,89 @@
 #[serde(rename_all = "camelCase")]
 enum GeneratorKind {
 	Impure,
+	Pure,
 }
 
+async fn generate_pure(
+	config: &Config,
+	_display_name: &str,
+	secret: Field,
+	default_generator: Field,
+	owners: &[String],
+) -> Result<FleetSecret> {
+	// TODO: pure secrets are supposed to be generated by nix daemon itself,
+	// inside of a sandbox... But we aren't here yet.
+	let config_field = &config.config_unchecked_field;
+	let generator = nix_go!(secret.generator);
+	let default_pkgs = &config.default_pkgs;
+
+	let call_package = nix_go!(default_pkgs.callPackage);
+
+	let generator = nix_go!(call_package(generator)(Obj {}));
+	let generator = generator.build().await?;
+	let generator = generator
+		.get("out")
+		.ok_or_else(|| anyhow!("missing generate out"))?;
+
+	let mut recipients = String::new();
+	for owner in owners {
+		let key = config.key(owner).await?;
+		recipients.push_str(&format!("-r \"{key}\" "));
+	}
+	recipients.push_str("-e");
+
+	let out = tempdir()?;
+
+	let mut gen = MyCommand::new(generator);
+	gen.env("rageArgs", recipients);
+	gen.env(
+		"out",
+		out.path().to_str().expect("sane tempdir should be utf-8"),
+	);
+	gen.run().await.context("impure generator")?;
+
+	{
+		let mut marker_path = out.path().to_owned();
+		marker_path.push("marker");
+		let marker = fs::read_to_string(&marker_path).await?;
+		ensure!(marker == "SUCCESS", "generation not succeeded");
+	}
+
+	let mut public_path = out.path().to_owned();
+	public_path.push("public");
+	let mut secret_path = out.path().to_owned();
+	secret_path.push("secret");
+	let public = fs::read_to_string(&public_path).await.ok();
+	let secret = fs::read(&secret_path).await.ok();
+	if let Some(secret) = &secret {
+		ensure!(
+			age::Decryptor::new(Cursor::new(&secret)).is_ok(),
+			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."
+		);
+	}
+
+	let mut created_at_path = out.path().to_owned();
+	created_at_path.push("created_at");
+	let mut expires_at_path = out.path().to_owned();
+	expires_at_path.push("expires_at");
+
+	async fn read_value<T: FromStr>(path: &Path) -> Result<T> {
+		dbg!(path);
+		let raw = fs::read(path).await?;
+		let raw = String::from_utf8(raw)?;
+		raw.parse().map_err(|_| anyhow!("fromStr failed"))
+	}
+
+	let created_at = read_value(&created_at_path).await?;
+	let expires_at = read_value(&expires_at_path).await.ok();
+
+	Ok(FleetSecret {
+		created_at,
+		expires_at,
+		public,
+		secret: secret.map(SecretData),
+	})
+}
 async fn generate_impure(
 	config: &Config,
 	_display_name: &str,
@@ -254,6 +342,9 @@
 		GeneratorKind::Impure => {
 			generate_impure(config, display_name, secret, default_generator, owners).await
 		}
+		GeneratorKind::Pure => {
+			generate_pure(config, display_name, secret, default_generator, owners).await
+		}
 	}
 }
 async fn generate_shared(
@@ -357,6 +448,8 @@
 				expires_at,
 				re_add,
 			} => {
+				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
+
 				let exists = config.has_shared(&name);
 				if exists && !force && !re_add {
 					bail!("secret already defined");
@@ -456,6 +549,16 @@
 					println!("{}", z85::encode(&data));
 				}
 			}
+			Secret::ReadPublic {
+				name,
+				machine,
+			} => {
+				let secret = config.host_secret(&machine, &name)?;
+				let Some(public) = secret.public else {
+					bail!("no secret {name}");
+				};
+				print!("{public}");
+			}
 			Secret::UpdateShared {
 				name,
 				machines,
@@ -463,6 +566,8 @@
 				remove_machines,
 				prefer_identities,
 			} => {
+				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
+
 				let secret = config.shared_secret(&name)?;
 				if secret.secret.secret.is_none() {
 					bail!("no secret");
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -11,9 +11,8 @@
 
 mod fleetdata;
 
-use std::ffi::OsString;
-use std::process::exit;
 use std::time::Duration;
+use std::{ffi::OsString, process::ExitCode};
 
 use anyhow::{bail, Result};
 use clap::Parser;
@@ -62,6 +61,7 @@
 				path.push(entry.path());
 
 				let mut status = MyCommand::new("nix");
+				status.args(&config.nix_args);
 				status.arg("store").arg("prefetch-file").arg(path);
 				status.run_nix_string().instrument(span).await?;
 				Ok(())
@@ -118,7 +118,11 @@
 				return;
 			};
 			let pos = state.pos();
-			let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());
+			if pos > len {
+				let _ = write!(writer, "{}", pos.human_count_bare());
+			} else {
+				let _ = write!(writer, "{} / {}", pos.human_count_bare(), len.human_count_bare());
+			}
 		})
 		.with_key(
 			"color_start",
@@ -151,7 +155,7 @@
 			tracing_subscriber::fmt::layer()
 				.without_time()
 				.with_target(true)
-				.with_writer(indicatif_layer.get_stderr_writer())
+				.with_writer(indicatif_layer.get_stdout_writer())
 				.with_filter(filter), // .withou,
 		)
 		.with(indicatif_layer)
@@ -159,12 +163,15 @@
 }
 
 #[tokio::main]
-async fn main() {
+async fn main() -> ExitCode {
 	setup_logging();
 	if let Err(e) = main_real().await {
+		// If I remove this line, the next error!() line gets eaten.
+		info!("fixme: this line gets eaten by tracing-indicatif on levels info+");
 		error!("{e:#}");
-		exit(1);
+		return ExitCode::FAILURE;
 	}
+	ExitCode::SUCCESS
 }
 
 async fn main_real() -> Result<()> {
modifiedcmds/install-secrets/Cargo.tomldiffbeforeafterboth
--- a/cmds/install-secrets/Cargo.toml
+++ b/cmds/install-secrets/Cargo.toml
@@ -4,18 +4,18 @@
 edition = "2021"
 
 [dependencies]
-age = { version = "0.9.2", features = ["ssh"] }
-anyhow = "1.0.75"
-env_logger = "0.10.0"
-log = "0.4.20"
+age = { version = "0.10.0", features = ["ssh"] }
+anyhow = "1.0.79"
+tracing-subscriber = "0.3"
+tracing = "0.1"
 nix = {version = "0.27.1", features = ["user", "fs"]}
-serde = { version = "1.0.190", features = ["derive"] }
-serde_json = "1.0.107"
-clap = { version = "4.4.7", features = [
+serde = { version = "1.0.196", features = ["derive"] }
+serde_json = "1.0.113"
+clap = { version = "4.5.1", features = [
 	"derive",
 	"env",
 	"wrap_help",
 	"unicode",
 ] }
-tempfile = "3.8.1"
+tempfile = "3.10.0"
 z85 = "3.0.5"
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -2,9 +2,8 @@
 use age::{Encryptor, Identity, Recipient};
 use anyhow::{anyhow, bail, Context, Result};
 use clap::Parser;
-use log::{error, info, warn};
 use nix::sys::stat::Mode;
-use nix::unistd::{User, Group, chown};
+use nix::unistd::{chown, Group, User};
 use serde::{Deserialize, Deserializer};
 use std::fmt::{self, Display};
 use std::fs::{self, File};
@@ -14,6 +13,9 @@
 use std::path::Path;
 use std::str::{from_utf8, FromStr};
 use std::{collections::HashMap, path::PathBuf};
+use tracing::{error, info, warn};
+use tracing_subscriber::filter::LevelFilter;
+use tracing_subscriber::EnvFilter;
 
 #[derive(Clone, Debug)]
 struct SecretWrapper(Vec<u8>);
@@ -228,8 +230,13 @@
 }
 
 fn main() -> anyhow::Result<()> {
-	env_logger::Builder::new()
-		.filter_level(log::LevelFilter::Info)
+	tracing_subscriber::fmt()
+		.with_env_filter(
+			EnvFilter::builder()
+				.with_default_directive(LevelFilter::INFO.into())
+				.from_env_lossy(),
+		)
+		.without_time()
 		.init();
 
 	let opts = Opts::parse();
modifiedcrates/better-command/Cargo.tomldiffbeforeafterboth
--- a/crates/better-command/Cargo.toml
+++ b/crates/better-command/Cargo.toml
@@ -3,12 +3,10 @@
 version = "0.1.0"
 edition = "2021"
 
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
 [dependencies]
-once_cell = "1.19.0"
-regex = "1.10.2"
-serde = { version = "1.0.193", features = ["derive"] }
-serde_json = "1.0.108"
-tracing = "0.1.40"
-tracing-indicatif = "0.3.6"
+once_cell = "1.19"
+regex = "1.10"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tracing = "0.1"
+tracing-indicatif = "0.3"
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -7,8 +7,8 @@
 alejandra = {git = "https://github.com/kamadorueda/alejandra"}
 linked-hash-map = "0.5.6"
 peg = "0.8.2"
-serde = "1.0.190"
-thiserror = "1.0.50"
-serde_json = "1.0.107"
+serde = "1.0.196"
+thiserror = "1.0.57"
+serde_json = "1.0.113"
 ron = "0.8.1"
 serde-transcode = "1.1.1"
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1701680307,
-        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
+        "lastModified": 1705309234,
+        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
+        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
         "type": "github"
       },
       "original": {
@@ -23,11 +23,11 @@
         "systems": "systems_2"
       },
       "locked": {
-        "lastModified": 1681202837,
-        "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
+        "lastModified": 1705309234,
+        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
+        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
         "type": "github"
       },
       "original": {
@@ -38,11 +38,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1704409229,
-        "narHash": "sha256-Vc41cRJ3trOnocovLe0zZE35pK5Lfuo/zHk0xx3CNDY=",
+        "lastModified": 1708177587,
+        "narHash": "sha256-Tj/YV9kdC+I7V/kjrq3Bdl8z2VIHT5hiAG74s52vLgw=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "786f788914f2a6e94cedf361541894e972b8fd23",
+        "rev": "3c43b81701e73452df1c080b05770407da9e16d6",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1704075545,
-        "narHash": "sha256-L3zgOuVKhPjKsVLc3yTm2YJ6+BATyZBury7wnhyc8QU=",
+        "lastModified": 1708135817,
+        "narHash": "sha256-EUMO/K3+Wgh0THOLoRXhxrh6G/pQ7BlJ8No+ciy1nKA=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "a0df72e106322b67e9c6e591fe870380bd0da0d5",
+        "rev": "c77e68d33a84ce3f9e86905c0f2ef78d5defad28",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -29,7 +29,7 @@
         llvmPkgs = pkgs.buildPackages.llvmPackages_11;
         rust =
           (pkgs.rustChannelOf {
-            date = "2024-01-01";
+            date = "2024-02-10";
             channel = "nightly";
           })
           .default
@@ -38,12 +38,14 @@
         packages = (import ./pkgs) pkgs pkgs;
         devShell = (pkgs.mkShell.override {stdenv = llvmPkgs.stdenv;}) {
           nativeBuildInputs = with pkgs; [
+            alejandra
             rust
             lld
             cargo-edit
             cargo-udeps
             cargo-fuzz
             cargo-watch
+            cargo-outdated
 
             pkg-config
             openssl
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -1,5 +1,11 @@
-{ lib, fleetLib, config, ... }: with lib; with fleetLib;
-let
+{
+  lib,
+  fleetLib,
+  config,
+  ...
+}:
+with lib;
+with fleetLib; let
   sharedSecret = with types; ({config, ...}: {
     options = {
       expectedOwners = mkOption {
@@ -9,13 +15,14 @@
 
           Secrets would be decrypted and stored to /run/secrets/$\{name} on owners
         '';
+        default = null;
       };
       # TODO: Aren't those options may be just desugared to data/expectedData?
       regenerateOnOwnerAdded = mkOption {
         type = bool;
         description = ''
           Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.
-          
+
           You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.
         '';
       };
@@ -24,7 +31,7 @@
         type = bool;
         description = ''
           Should this secret be removed on owner removal, or it may be just reencrypted
-          
+
           Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.
           Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked
           in some other way than by this secret ownership, I.e by firewall/etc.
@@ -55,7 +62,7 @@
 
           Imported from fleet.nix
         '';
-        default = [ ];
+        default = [];
       };
       # TODO: Make secret generator generate arbitrary number of secret/public parts?
       # Make it generate a folder, where all files except suffixed by .enc are public, and the rest are secret?
@@ -96,43 +103,121 @@
       };
     };
   };
-in
-{
+in {
   options = with types; {
     sharedSecrets = mkOption {
       type = attrsOf (submodule sharedSecret);
-      default = { };
+      default = {};
       description = "Shared secrets";
     };
     hostSecrets = mkOption {
       type = attrsOf (attrsOf (submodule hostSecret));
-      default = { };
+      default = {};
       description = "Host secrets. Imported from fleet.nix";
       internal = true;
     };
   };
   config = {
-    assertions = mapAttrsToList
+    assertions =
+      mapAttrsToList
       (name: secret: {
         assertion = secret.expectedOwners == null || builtins.sort (a: b: a < b) secret.owners == builtins.sort (a: b: a < b) secret.expectedOwners;
         message = "Shared secret ${name} is expected to be encrypted for ${builtins.toJSON secret.expectedOwners}, but it is encrypted for ${builtins.toJSON secret.owners}. Run fleet secrets regenerate to fix";
       })
       config.sharedSecrets;
     hosts = hostsToAttrs (host: {
-      modules =
-        let
-          cleanupSecret = (secretName: v: {
-            inherit (v) public secret;
-            shared = true;
-          });
-        in
-        [
-          {
-            secrets = (mapAttrs cleanupSecret
+      modules = let
+        cleanupSecret = secretName: v: {
+          inherit (v) public secret;
+          shared = true;
+        };
+      in [
+        {
+          secrets =
+            (
+              mapAttrs cleanupSecret
               (filterAttrs (_: v: builtins.elem host v.owners) config.sharedSecrets)
-            ) // (mapAttrs cleanupSecret (config.hostSecrets.${host} or { }));
-          }
-        ];
+            )
+            // (mapAttrs cleanupSecret (config.hostSecrets.${host} or {}));
+        }
+      ];
     });
+    # TODO: Should this attribute be moved to `nixpkgs.overlays`?
+    overlays = [
+      (final: prev: let
+        lib = final.lib;
+      in {
+        mkPassword = {size ? 32}:
+          final.mkSecretGenerator ''
+            ${final.coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
+              | ${final.coreutils}/bin/head -c ${toString size} \
+              | encrypt > $out/secret
+          '';
+        mkRsa = {size ? 4096}:
+          final.mkSecretGenerator ''
+            ${final.openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
+            ${final.openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
+
+            sudo cat rsa_private.key | encrypt > $out/secret
+            sudo cat rsa_public.key > $out/public
+          '';
+        # TODO: Move to fleet
+        # TODO: Merge both generators to one with consistent options syntax?
+        # Impure generator is built on local machine, then built closure is copied to remote machine,
+        # and then it is ran in inpure context, so that this generator may access HSMs and other things.
+        mkImpureSecretGenerator = generatorText: machine:
+          (prev.writeShellScript "impureGenerator.sh" ''
+            #!/bin/sh
+            set -eu
+
+            # TODO: Provide encryption function as script passed to `callPackage generator {encrypt = ...;}`
+            function encrypt() {
+              eval ${final.rage}/bin/rage $rageArgs
+            }
+
+            created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+            echo -n $created_at > $out/created_at
+
+            ${generatorText}
+
+            echo -n SUCCESS > $out/marker
+          '')
+          .overrideAttrs (old: {
+            passthru = {
+              generatorKind = "impure";
+              impureOn = machine;
+            };
+          });
+        # TODO: Implement consistent naming
+        # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...
+        # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.
+        mkSecretGenerator = generatorText:
+          (prev.writeShellScript "generator.sh" ''
+            #!/bin/sh
+            set -eu
+            # TODO: User should create output directory by themselves.
+            cd $out
+
+            # TODO: Provide encryption function as script passed to `callPackage generator {encrypt = ...;}`
+            function encrypt() {
+              eval ${final.rage}/bin/rage $rageArgs
+            }
+
+            created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+            echo -n $created_at > $out/created_at
+
+            ${generatorText}
+
+            echo -n SUCCESS > $out/marker
+          '')
+          .overrideAttrs (old: {
+            passthru = {
+              generatorKind = "pure";
+            };
+            # TODO: make nix daemon build secret, not just the script.
+            # __impure = true;
+          });
+      })
+    ];
   };
 }