git.delta.rocks / jrsonnet / refs/commits / d8c8e6d33ef5

difftreelog

refactor nix secret module

Yaroslav Bolyukin2024-04-14parent: #754b45c.patch.diff
in: trunk

17 files changed

modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = ["crates/*", "cmds/*"]
 resolver = "2"
+package.version = "0.1.0"
 
 [workspace.dependencies]
 nixlike = { path = "./crates/nixlike" }
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/secrets/mod.rs
1use crate::{2	better_nix_eval::Field,3	fleetdata::{FleetSecret, FleetSharedSecret, SecretData},4	host::Config,5	nix_go, nix_go_json,6};7use anyhow::{anyhow, bail, ensure, Context, Result};8use chrono::{DateTime, Utc};9use clap::Parser;10use owo_colors::OwoColorize;11use serde::Deserialize;12use std::{13	collections::{BTreeSet, HashSet},14	io::{self, Cursor, Read},15	path::PathBuf,16};17use tabled::{Table, Tabled};18use tokio::fs::read_to_string;19use tracing::{error, info, info_span, warn, Instrument};2021#[derive(Parser)]22pub enum Secret {23	/// Force load host keys for all defined hosts24	ForceKeys,25	/// Add secret, data should be provided in stdin26	AddShared {27		/// Secret name28		name: String,29		/// Secret owners30		machines: Vec<String>,31		/// Override secret if already present32		#[clap(long)]33		force: bool,34		/// Secret public part35		#[clap(long)]36		public: Option<String>,37		/// Load public part from specified file38		#[clap(long)]39		public_file: Option<PathBuf>,4041		/// Create a notification on secret expiration42		#[clap(long)]43		expires_at: Option<DateTime<Utc>>,4445		/// Secret with this name already exists, override its value while keeping the same owners.46		#[clap(long)]47		re_add: bool,48	},49	/// Add secret, data should be provided in stdin50	Add {51		/// Secret name52		name: String,53		/// Secret owners54		machine: String,55		/// Override secret if already present56		#[clap(long)]57		force: bool,58		#[clap(long)]59		public: Option<String>,60		#[clap(long)]61		public_file: Option<PathBuf>,62	},63	/// Read secret from remote host, requires sudo on said host64	Read {65		name: String,66		machine: String,67		#[clap(long)]68		plaintext: bool,69	},70	ReadPublic {71		name: String,72		machine: String,73	},74	UpdateShared {75		name: String,7677		#[clap(long)]78		machines: Option<Vec<String>>,7980		#[clap(long)]81		add_machines: Vec<String>,82		#[clap(long)]83		remove_machines: Vec<String>,8485		/// Which host should we use to decrypt86		#[clap(long)]87		prefer_identities: Vec<String>,88	},89	Regenerate {90		/// Which host should we use to decrypt, in case if reencryption is required, without91		/// regeneration92		#[clap(long)]93		prefer_identities: Vec<String>,94	},95	List {},96}9798#[tracing::instrument(skip(config, secret, field, prefer_identities))]99async fn update_owner_set(100	secret_name: &str,101	config: &Config,102	mut secret: FleetSharedSecret,103	field: Field,104	updated_set: &[String],105	prefer_identities: &[String],106) -> Result<FleetSharedSecret> {107	let original_set = secret.owners.clone();108109	let set = original_set.iter().collect::<BTreeSet<_>>();110	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();111112	if set == expected_set {113		info!("no need to update owner list, it is already correct");114		return Ok(secret);115	}116117	let should_regenerate = if set.difference(&expected_set).next().is_some() {118		// TODO: Remove this warning for revokable secrets.119		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");120		nix_go_json!(field.regenerateOnOwnerRemoved)121	} else if expected_set.difference(&set).next().is_some() {122		nix_go_json!(field.regenerateOnOwnerAdded)123	} else {124		false125	};126127	if should_regenerate {128		info!("secret is owner-dependent, will regenerate");129		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;130		Ok(generated)131	} else {132		let identity_holder = if !prefer_identities.is_empty() {133			prefer_identities134				.iter()135				.find(|i| original_set.iter().any(|s| s == *i))136		} else {137			secret.owners.first()138		};139		let Some(identity_holder) = identity_holder else {140			bail!("no available holder found");141		};142143		if let Some(data) = secret.secret.secret {144			let host = config.host(identity_holder).await?;145			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;146			secret.secret.secret = Some(encrypted);147		}148149		secret.owners = updated_set.to_vec();150		Ok(secret)151	}152}153154#[derive(Deserialize)]155#[serde(rename_all = "camelCase")]156enum GeneratorKind {157	Impure,158	Pure,159}160161async fn generate_pure(162	_config: &Config,163	_display_name: &str,164	_secret: Field,165	_default_generator: Field,166	_owners: &[String],167) -> Result<FleetSecret> {168	bail!("pure generators are broken for now")169}170async fn generate_impure(171	config: &Config,172	_display_name: &str,173	secret: Field,174	default_generator: Field,175	owners: &[String],176) -> Result<FleetSecret> {177	let generator = nix_go!(secret.generator);178	let on: Option<String> = nix_go_json!(default_generator.impureOn);179180	let host = if let Some(on) = &on {181		config.host(on).await?182	} else {183		config.local_host()184	};185	let on_pkgs = host.pkgs().await?;186	let call_package = nix_go!(on_pkgs.callPackage);187	let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);188189	let mut recipients = Vec::new();190	for owner in owners {191		let key = config.key(owner).await?;192		recipients.push(key);193	}194	let encrypt = nix_go!(mk_encrypt_secret(Obj {195		recipients: { recipients },196	}));197198	let generator = nix_go!(call_package(generator)(Obj {199		encrypt,200		rustfmt_please_newline: { true },201	}));202203	let generator = generator.build().await?;204	let generator = generator205		.get("out")206		.ok_or_else(|| anyhow!("missing generateImpure out"))?;207	let generator = host.remote_derivation(generator).await?;208209	let out_parent = host.mktemp_dir().await?;210	let out = format!("{out_parent}/out");211212	let mut gen = host.cmd(generator).await?;213	gen.env("out", &out);214	if on.is_none() {215		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.216		let project_path: String = config217			.directory218			.clone()219			.into_os_string()220			.into_string()221			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;222		gen.env("FLEET_PROJECT", project_path);223	}224	gen.run().await.context("impure generator")?;225226	{227		let marker = host.read_file_text(format!("{out}/marker")).await?;228		ensure!(marker == "SUCCESS", "generation not succeeded");229	}230231	let public = host.read_file_text(format!("{out}/public")).await.ok();232	let secret = host.read_file_bin(format!("{out}/secret")).await.ok();233	if let Some(secret) = &secret {234		ensure!(235			age::Decryptor::new(Cursor::new(&secret)).is_ok(),236			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."237		);238	}239240	let created_at = host.read_file_value(format!("{out}/created_at")).await?;241	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();242243	Ok(FleetSecret {244		created_at,245		expires_at,246		public,247		secret: secret.map(SecretData),248	})249}250async fn generate(251	config: &Config,252	display_name: &str,253	secret: Field,254	owners: &[String],255) -> Result<FleetSecret> {256	let generator = nix_go!(secret.generator);257	// Can't properly check on nix module system level258	{259		let gen_ty = generator.type_of().await?;260		if gen_ty == "null" {261			bail!("secret has no generator defined, can't automatically generate it.");262		}263		if gen_ty != "lambda" {264			bail!("generator should be lambda, got {gen_ty}");265		}266	}267	let default_pkgs = &config.default_pkgs;268	let default_call_package = nix_go!(default_pkgs.callPackage);269	// Generators provide additional information in passthru, to access270	// passthru we should call generator, but information about where this generator is supposed to build271	// is located in passthru... Thus evaluating generator on host.272	//273	// Maybe it is also possible to do some magic with __functor?274	//275	// I don't want to make modules always responsible for additional secret data anyway,276	// so it should be in derivation, and not in the secret data itself.277	let default_generator = nix_go!(default_call_package(generator)(Obj {}));278279	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);280281	match kind {282		GeneratorKind::Impure => {283			generate_impure(config, display_name, secret, default_generator, owners).await284		}285		GeneratorKind::Pure => {286			generate_pure(config, display_name, secret, default_generator, owners).await287		}288	}289}290async fn generate_shared(291	config: &Config,292	display_name: &str,293	secret: Field,294	expected_owners: Vec<String>,295) -> Result<FleetSharedSecret> {296	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);297	Ok(FleetSharedSecret {298		secret: generate(config, display_name, secret, &expected_owners).await?,299		owners: expected_owners,300	})301}302303async fn parse_public(304	public: Option<String>,305	public_file: Option<PathBuf>,306) -> Result<Option<String>> {307	Ok(match (public, public_file) {308		(Some(v), None) => Some(v),309		(None, Some(v)) => Some(read_to_string(v).await?),310		(Some(_), Some(_)) => {311			bail!("only public or public_file should be set")312		}313		(None, None) => None,314	})315}316317fn parse_machines(318	initial: Vec<String>,319	machines: Option<Vec<String>>,320	mut add_machines: Vec<String>,321	mut remove_machines: Vec<String>,322) -> Result<Vec<String>> {323	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {324		bail!("no operation");325	}326327	let initial_machines = initial.clone();328	let mut target_machines = initial;329	info!("Currently encrypted for {initial_machines:?}");330331	// ensure!(machines.is_some() || !add_machines.is_empty() || )332	if let Some(machines) = machines {333		ensure!(334			add_machines.is_empty() && remove_machines.is_empty(),335			"can't combine --machines and --add-machines/--remove-machines"336		);337		let target = initial_machines.iter().collect::<HashSet<_>>();338		let source = machines.iter().collect::<HashSet<_>>();339		for removed in target.difference(&source) {340			remove_machines.push((*removed).clone());341		}342		for added in source.difference(&target) {343			add_machines.push((*added).clone());344		}345	}346347	for machine in &remove_machines {348		let mut removed = false;349		while let Some(pos) = target_machines.iter().position(|m| m == machine) {350			target_machines.swap_remove(pos);351			removed = true;352		}353		if !removed {354			warn!("secret is not enabled for {machine}");355		}356	}357	for machine in &add_machines {358		if target_machines.iter().any(|m| m == machine) {359			warn!("secret is already added to {machine}");360		} else {361			target_machines.push(machine.to_owned());362		}363	}364	if !remove_machines.is_empty() {365		// TODO: maybe force secret regeneration?366		// Not that useful without revokation.367		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");368	}369	Ok(target_machines)370}371impl Secret {372	pub async fn run(self, config: &Config) -> Result<()> {373		match self {374			Secret::ForceKeys => {375				for host in config.list_hosts().await? {376					if config.should_skip(&host.name) {377						continue;378					}379					config.key(&host.name).await?;380				}381			}382			Secret::AddShared {383				mut machines,384				name,385				force,386				public,387				public_file,388				expires_at,389				re_add,390			} => {391				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).392393				let exists = config.has_shared(&name);394				if exists && !force && !re_add {395					bail!("secret already defined");396				}397				if re_add {398					// Fixme: use clap to limit this usage399					ensure!(!force, "--force and --readd are not compatible");400					ensure!(exists, "secret doesn't exists");401					ensure!(402						machines.is_empty(),403						"you can't use machines argument for --readd"404					);405					let shared = config.shared_secret(&name)?;406					machines = shared.owners;407				}408409				let recipients = config.recipients(machines.clone()).await?;410411				let secret = {412					let mut input = vec![];413					io::stdin().read_to_end(&mut input)?;414415					if input.is_empty() {416						None417					} else {418						Some(419							SecretData::encrypt(recipients, input)420								.ok_or_else(|| anyhow!("no recipients provided"))?,421						)422					}423				};424				let public = parse_public(public, public_file).await?;425				config.replace_shared(426					name,427					FleetSharedSecret {428						owners: machines,429						secret: FleetSecret {430							created_at: Utc::now(),431							expires_at,432							secret,433							public,434						},435					},436				);437			}438			Secret::Add {439				machine,440				name,441				force,442				public,443				public_file,444			} => {445				let recipient = config.recipient(&machine).await?;446447				let secret = {448					let mut input = vec![];449					io::stdin().read_to_end(&mut input)?;450					if input.is_empty() {451						bail!("no data provided")452					}453454					Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))455				};456457				if config.has_secret(&machine, &name) && !force {458					bail!("secret already defined");459				}460				let public = parse_public(public, public_file).await?;461462				config.insert_secret(463					&machine,464					name,465					FleetSecret {466						created_at: Utc::now(),467						expires_at: None,468						secret,469						public,470					},471				);472			}473			#[allow(clippy::await_holding_refcell_ref)]474			Secret::Read {475				name,476				machine,477				plaintext,478			} => {479				let secret = config.host_secret(&machine, &name)?;480				let Some(secret) = secret.secret else {481					bail!("no secret {name}");482				};483				let host = config.host(&machine).await?;484				let data = host.decrypt(secret).await?;485				if plaintext {486					let s = String::from_utf8(data).context("output is not utf8")?;487					print!("{s}");488				} else {489					println!("{}", z85::encode(&data));490				}491			}492			Secret::ReadPublic { name, machine } => {493				let secret = config.host_secret(&machine, &name)?;494				let Some(public) = secret.public else {495					bail!("no secret {name}");496				};497				print!("{public}");498			}499			Secret::UpdateShared {500				name,501				machines,502				add_machines,503				remove_machines,504				prefer_identities,505			} => {506				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).507508				let secret = config.shared_secret(&name)?;509				if secret.secret.secret.is_none() {510					bail!("no secret");511				}512513				let initial_machines = secret.owners.clone();514				let target_machines = parse_machines(515					initial_machines.clone(),516					machines,517					add_machines,518					remove_machines,519				)?;520521				if target_machines.is_empty() {522					info!("no machines left for secret, removing it");523					config.remove_shared(&name);524					return Ok(());525				}526527				let config_field = &config.config_unchecked_field;528				let field = nix_go!(config_field.sharedSecrets[{ name }]);529530				let updated = update_owner_set(531					&name,532					config,533					secret,534					field,535					&target_machines,536					&prefer_identities,537				)538				.await?;539				config.replace_shared(name, updated);540			}541			Secret::Regenerate { prefer_identities } => {542				info!("checking for secrets to regenerate");543				{544					let _span = info_span!("shared").entered();545					let expected_shared_set = config546						.list_configured_shared()547						.await?548						.into_iter()549						.collect::<HashSet<_>>();550					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();551					for missing in expected_shared_set.difference(&shared_set) {552						let config_field = &config.config_unchecked_field;553						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);554						let expected_owners: Option<Vec<String>> =555							nix_go_json!(secret.expectedOwners);556						let Some(expected_owners) = expected_owners else {557							// TODO: Might still need to regenerate558							continue;559						};560						info!("generating secret: {missing}");561						let shared = generate_shared(config, missing, secret, expected_owners)562							.in_current_span()563							.await?;564						config.replace_shared(missing.to_string(), shared)565					}566				}567				for host in config.list_hosts().await? {568					let _span = info_span!("host", host = host.name).entered();569					let expected_set = host570						.list_configured_secrets()571						.in_current_span()572						.await?573						.into_iter()574						.collect::<HashSet<_>>();575					let stored_set = config576						.list_secrets(&host.name)577						.into_iter()578						.collect::<HashSet<_>>();579					for missing in expected_set.difference(&stored_set) {580						info!("generating secret: {missing}");581						let secret = host.secret_field(missing).in_current_span().await?;582						let generated =583							match generate(config, missing, secret, &[host.name.clone()])584								.in_current_span()585								.await586							{587								Ok(v) => v,588								Err(e) => {589									error!("{e}");590									continue;591								}592							};593						config.insert_secret(&host.name, missing.to_string(), generated)594					}595				}596				let mut to_remove = Vec::new();597				for name in &config.list_shared() {598					info!("updating secret: {name}");599					let data = config.shared_secret(name)?;600					let config_field = &config.config_unchecked_field;601					let expected_owners: Vec<String> =602						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);603					if expected_owners.is_empty() {604						warn!("secret was removed from fleet config: {name}, removing from data");605						to_remove.push(name.to_string());606						continue;607					}608609					let secret = nix_go!(config_field.sharedSecrets[{ name }]);610					config.replace_shared(611						name.to_owned(),612						update_owner_set(613							name,614							config,615							data,616							secret,617							&expected_owners,618							&prefer_identities,619						)620						.await?,621					);622				}623				for k in to_remove {624					config.remove_shared(&k);625				}626			}627			Secret::List {} => {628				let _span = info_span!("loading secrets").entered();629				let configured = config.list_configured_shared().await?;630				#[derive(Tabled)]631				struct SecretDisplay {632					#[tabled(rename = "Name")]633					name: String,634					#[tabled(rename = "Owners")]635					owners: String,636				}637				let mut table = vec![];638				for name in configured.iter().cloned() {639					let config = config.clone();640					let expected_owners = config.shared_secret_expected_owners(&name).await?;641					let data = config.shared_secret(&name)?;642					let owners = data643						.owners644						.iter()645						.map(|o| {646							if expected_owners.contains(o) {647								o.green().to_string()648							} else {649								o.red().to_string()650							}651						})652						.collect::<Vec<_>>();653					table.push(SecretDisplay {654						owners: owners.join(", "),655						name,656					})657				}658				info!("loaded\n{}", Table::new(table).to_string())659			}660		}661		Ok(())662	}663}
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -54,7 +54,7 @@
 	pub local: bool,
 	pub session: OnceLock<Arc<openssh::Session>>,
 
-	pub nixos_config: Field,
+	pub nixos_config: Option<Field>,
 }
 impl ConfigHost {
 	async fn open_session(&self) -> Result<Arc<openssh::Session>> {
@@ -169,7 +169,9 @@
 	}
 
 	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {
-		let nixos = &self.nixos_config;
+		let Some(nixos) = &self.nixos_config else {
+			return Ok(vec![]);
+		};
 		let secrets = nix_go!(nixos.secrets);
 		let mut out = Vec::new();
 		for name in secrets.list_fields().await? {
@@ -183,9 +185,19 @@
 		Ok(out)
 	}
 	pub async fn secret_field(&self, name: &str) -> Result<Field> {
-		let nixos = &self.nixos_config;
+		let Some(nixos) = &self.nixos_config else {
+			bail!("host is virtual and has no secrets");
+		};
 		Ok(nix_go!(nixos.secrets[{ name }]))
 	}
+
+	/// Packages for this host, resolved with nixpkgs overlays
+	pub async fn pkgs(&self) -> Result<Field> {
+		let Some(nixos) = &self.nixos_config else {
+			return Ok(self.config.default_pkgs.clone());
+		};
+		Ok(nix_go!(nixos.nixpkgs.resolvedPkgs))
+	}
 }
 
 impl Config {
@@ -202,6 +214,16 @@
 		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)
 	}
 
+	pub fn local_host(&self) -> ConfigHost {
+		ConfigHost {
+			config: self.clone(),
+			name: "<virtual localhost>".to_owned(),
+			local: true,
+			session: OnceLock::new(),
+			nixos_config: None,
+		}
+	}
+
 	pub async fn host(&self, name: &str) -> Result<ConfigHost> {
 		let config = &self.config_unchecked_field;
 		let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);
@@ -210,7 +232,7 @@
 			name: name.to_owned(),
 			local: self.is_local(name),
 			session: OnceLock::new(),
-			nixos_config,
+			nixos_config: Some(nixos_config),
 		})
 	}
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -11,7 +11,6 @@
 
 mod fleetdata;
 
-use std::time::Duration;
 use std::{ffi::OsString, process::ExitCode};
 
 use anyhow::{bail, Result};
@@ -158,7 +157,7 @@
 	let reg = tracing_subscriber::registry().with({
 		let sub = tracing_subscriber::fmt::layer()
 			.without_time()
-			.with_target(true);
+			.with_target(false);
 		#[cfg(feature = "indicatif")]
 		let sub = sub.with_writer(indicatif_layer.get_stdout_writer());
 		sub.with_filter(filter) // .withou,
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -13,7 +13,7 @@
 use std::path::Path;
 use std::str::{from_utf8, FromStr};
 use std::{collections::HashMap, path::PathBuf};
-use tracing::{error, info, warn};
+use tracing::{error, info, info_span, warn};
 use tracing_subscriber::filter::LevelFilter;
 use tracing_subscriber::EnvFilter;
 
@@ -213,12 +213,9 @@
 
 	let mut failed = false;
 	for (name, value) in data {
-		info!("initializing secret {name}");
+		let _span = info_span!("init", name = name);
 		if let Err(e) = init_secret(&identity, value) {
-			error!(
-				"{:?}",
-				e.context(format!("failed to initialize secret {}", name))
-			);
+			error!("{e}");
 			failed = true;
 		}
 	}
@@ -237,6 +234,7 @@
 				.from_env_lossy(),
 		)
 		.without_time()
+		.with_target(false)
 		.init();
 
 	let opts = Opts::parse();
modifiedcrates/better-command/src/handler.rsdiffbeforeafterboth
--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -274,7 +274,10 @@
 							#[cfg(feature = "indicatif")]
 							span.pb_set_message(&process_message(s.trim()));
 							#[cfg(not(feature = "indicatif"))]
-							info!("{}", process_message(s));
+							{
+								let _span = span.enter();
+								info!("{}", process_message(s));
+							}
 						} else {
 							warn!("bad fields: {fields:?}");
 						}
modifiedcrates/better-command/src/lib.rsdiffbeforeafterboth
--- a/crates/better-command/src/lib.rs
+++ b/crates/better-command/src/lib.rs
@@ -1,5 +1,5 @@
 mod handler;
-pub use handler::{Handler, PlainHandler, NoopHandler, NixHandler, ClonableHandler};
+pub use handler::{ClonableHandler, Handler, NixHandler, NoopHandler, PlainHandler};
 
 pub fn add(left: usize, right: usize) -> usize {
 	left + right
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -1,26 +1,28 @@
 {
   "nodes": {
-    "flake-utils": {
+    "crane": {
       "inputs": {
-        "systems": "systems"
+        "nixpkgs": [
+          "nixpkgs"
+        ]
       },
       "locked": {
-        "lastModified": 1705309234,
-        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "lastModified": 1712681629,
+        "narHash": "sha256-bMDXn4AkTXLCpoZbII6pDGoSeSe9gI87jxPsHRXgu/E=",
+        "owner": "ipetkov",
+        "repo": "crane",
+        "rev": "220387ac8e99cbee0ca4c95b621c4bc782b6a235",
         "type": "github"
       },
       "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
+        "owner": "ipetkov",
+        "repo": "crane",
         "type": "github"
       }
     },
-    "flake-utils_2": {
+    "flake-utils": {
       "inputs": {
-        "systems": "systems_2"
+        "systems": "systems"
       },
       "locked": {
         "lastModified": 1705309234,
@@ -54,6 +56,7 @@
     },
     "root": {
       "inputs": {
+        "crane": "crane",
         "flake-utils": "flake-utils",
         "nixpkgs": "nixpkgs",
         "rust-overlay": "rust-overlay"
@@ -61,7 +64,9 @@
     },
     "rust-overlay": {
       "inputs": {
-        "flake-utils": "flake-utils_2",
+        "flake-utils": [
+          "flake-utils"
+        ],
         "nixpkgs": [
           "nixpkgs"
         ]
@@ -81,21 +86,6 @@
       }
     },
     "systems": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
-    },
-    "systems_2": {
       "locked": {
         "lastModified": 1681028828,
         "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -5,15 +5,23 @@
     nixpkgs.url = "github:nixos/nixpkgs/master";
     rust-overlay = {
       url = "github:oxalica/rust-overlay";
+      inputs = {
+        nixpkgs.follows = "nixpkgs";
+        flake-utils.follows = "flake-utils";
+      };
+    };
+    flake-utils.url = "github:numtide/flake-utils";
+    crane = {
+      url = "github:ipetkov/crane";
       inputs.nixpkgs.follows = "nixpkgs";
     };
-    flake-utils = {url = "github:numtide/flake-utils";};
   };
   outputs = {
     self,
     rust-overlay,
     flake-utils,
     nixpkgs,
+    crane,
   }:
     with nixpkgs.lib;
       {
@@ -26,20 +34,16 @@
             inherit system;
             overlays = [(import rust-overlay)];
           };
-        llvmPkgs = pkgs.buildPackages.llvmPackages_11;
-        rust =
-          (pkgs.rustChannelOf {
-            date = "2024-02-10";
-            channel = "nightly";
-          })
-          .default
-          .override {extensions = ["rust-src" "rust-analyzer"];};
+        rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
+        craneLib = (crane.mkLib pkgs).overrideToolchain rust;
       in {
-        packages = (import ./pkgs) pkgs pkgs;
-        devShell = (pkgs.mkShell.override {stdenv = llvmPkgs.stdenv;}) {
+        packages = import ./pkgs {
+          inherit (pkgs) callPackage;
+          inherit craneLib;
+        };
+        devShell = craneLib.devShell {
           nativeBuildInputs = with pkgs; [
             alejandra
-            rust
             lld
             cargo-edit
             cargo-udeps
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -10,11 +10,14 @@
     hosts,
     modules,
     globalModules ? [],
+    extraFleetLib ? {},
   }: let
     hostNames = nixpkgs.lib.attrNames hosts;
-    fleetLib = import ./fleetLib.nix {
-      inherit nixpkgs hostNames;
-    };
+    fleetLib =
+      (import ./fleetLib.nix {
+        inherit nixpkgs hostNames;
+      })
+      // extraFleetLib;
   in let
     root = nixpkgs.lib.evalModules {
       modules =
modifiedlib/fleetLib.nixdiffbeforeafterboth
--- a/lib/fleetLib.nix
+++ b/lib/fleetLib.nix
@@ -39,4 +39,32 @@
   mkFleetDefault = mkOverride 999;
   # Some generators use mkDefault, but optionDefault is set by nixpkgs.
   mkFleetGeneratorDefault = mkOverride 1001;
+
+  mkPassword = {size ? 32}: {
+    coreutils,
+    encrypt,
+    mkSecretGenerator,
+  }:
+    mkSecretGenerator {
+      script = ''
+        ${coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
+          | ${coreutils}/bin/head -c ${toString size} \
+          | ${encrypt} > $out/secret
+      '';
+    };
+
+  mkRsa = {size ? 4096}: {
+    openssl,
+    encrypt,
+    mkSecretGenerator,
+  }:
+    mkSecretGenerator {
+      script = ''
+        ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
+        ${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
+      '';
+    };
 }
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -8,6 +8,13 @@
 with fleetLib; let
   sharedSecret = with types; ({config, ...}: {
     options = {
+      managed = mkOption {
+        type = bool;
+        description = ''
+          Is this secret managed by configuration (I.e will work with reencrypt/etc), or it is configured by user
+        '';
+      };
+
       expectedOwners = mkOption {
         type = nullOr (listOf str);
         description = ''
@@ -146,77 +153,81 @@
     overlays = [
       (final: prev: let
         lib = final.lib;
+        inherit (lib) strings;
+        inherit (strings) escapeShellArgs;
       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
+        mkEncryptSecret = {
+          rage ? prev.rage,
+          recipients,
+        }:
+          prev.writeShellScript "encryptor" ''
+            #!/bin/sh
+            exec ${rage}/bin/rage ${escapeShellArgs recipients} -e "$@"
           '';
         # 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:
+        mkImpureSecretGenerator = {
+          script,
+          # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
+          # (Some secrets-encryption-in-git/managed PKI solution is expected)
+          impureOn ? null,
+        }:
           (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
-            }
+            cd /var/empty
 
             created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
-            echo -n $created_at > $out/created_at
 
-            ${generatorText}
+            ${script}
 
+            if ! test -d $out; then
+              echo "impure generator script did not produce expected \$out output"
+              exit 1
+            fi
+
+            echo -n $created_at > $out/created_at
             echo -n SUCCESS > $out/marker
           '')
           .overrideAttrs (old: {
             passthru = {
+              inherit impureOn;
               generatorKind = "impure";
-              impureOn = machine;
             };
           });
+        # Pure generators are disabled for now
+        mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;};
+
         # 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;
-          });
+        # mkSecretGenerator = {script}:
+        #   (prev.writeShellScript "generator.sh" ''
+        #     #!/bin/sh
+        #     set -eu
+        #     # TODO: make nix daemon build secret, not just the script.
+        #     cd /var/empty
+        #
+        #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
+        #
+        #     ${script}
+        #     if ! test -d $out; then
+        #       echo "impure generator script did not produce expected \$out output"
+        #       exit 1
+        #     fi
+        #
+        #     echo -n $created_at > $out/created_at
+        #     echo -n SUCCESS > $out/marker
+        #   '')
+        #   .overrideAttrs (old: {
+        #     passthru = {
+        #       generatorKind = "pure";
+        #     };
+        #     # TODO: make nix daemon build secret, not just the script.
+        #     # __impure = true;
+        #   });
       })
     ];
   };
modifiednixos/fleetPkgs.nixdiffbeforeafterboth
--- a/nixos/fleetPkgs.nix
+++ b/nixos/fleetPkgs.nix
@@ -1,3 +1,24 @@
-{ ... }: {
-  nixpkgs.overlays = [ (import ../pkgs) ];
+{...}: {
+  nixpkgs.overlays = [
+    # Not using craneLib here, because we don't want to have two different rust versions for some platforms.
+    (final: prev: {
+      fleet-install-secrets = prev.callPackage ({rustPlatform}:
+        rustPlatform.buildRustPackage rec {
+          pname = "fleet-install-secrets";
+          name = "${pname}";
+
+          src = ../.;
+          strictDeps = true;
+
+          buildAndTestSubdir = "cmds/install-secrets";
+
+          cargoLock = {
+            lockFile = ../Cargo.lock;
+            outputHashes = {
+              "alejandra-3.0.0" = "sha256-lStDIPizbJipd1JpNKX1olBKzyIosyC2U/mVFwJPcZE=";
+            };
+          };
+        }) {};
+    })
+  ];
 }
modifiedpkgs/default.nixdiffbeforeafterboth
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,6 +1,9 @@
-pkgs: super:
-with pkgs;
 {
-  fleet-install-secrets = callPackage ./fleet-install-secrets.nix { };
-  fleet = callPackage ./fleet.nix { };
+  callPackage,
+  craneLib,
+}: rec {
+  default = fleet;
+
+  fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;};
+  fleet = callPackage ./fleet.nix {inherit craneLib;};
 }
modifiedpkgs/fleet-install-secrets.nixdiffbeforeafterboth
--- a/pkgs/fleet-install-secrets.nix
+++ b/pkgs/fleet-install-secrets.nix
@@ -1,16 +1,9 @@
-{ rustPlatform, lib }:
-
-rustPlatform.buildRustPackage rec {
+{craneLib}:
+craneLib.buildPackage rec {
   pname = "fleet-install-secrets";
-  version = "0.0.1";
-  name = "${pname}-${version}";
 
-  src = ../.;
-  buildAndTestSubdir = "cmds/install-secrets";
-  cargoLock = {
-    lockFile = ../Cargo.lock;
-    outputHashes = {
-      "alejandra-3.0.0" = "sha256-lStDIPizbJipd1JpNKX1olBKzyIosyC2U/mVFwJPcZE=";
-    };
-  };
+  src = craneLib.cleanCargoSource (craneLib.path ../.);
+  strictDeps = true;
+
+  cargoExtraArgs = "--locked -p ${pname}";
 }
modifiedpkgs/fleet.nixdiffbeforeafterboth
--- a/pkgs/fleet.nix
+++ b/pkgs/fleet.nix
@@ -1,16 +1,9 @@
-{ rustPlatform }:
-
-rustPlatform.buildRustPackage rec {
+{craneLib}:
+craneLib.buildPackage rec {
   pname = "fleet";
-  version = "0.0.1";
-  name = "${pname}-${version}";
 
-  src = ../.;
-  cargoBuildFlags = "-p ${pname}";
-  cargoLock = {
-    lockFile = ../Cargo.lock;
-    outputHashes = {
-      "alejandra-3.0.0" = "sha256-YSdHsJ73G7TEFzbmpZ2peuMefIa9/vNB2g+xdiyma3U=";
-    };
-  };
+  src = craneLib.cleanCargoSource (craneLib.path ../.);
+  strictDeps = true;
+
+  cargoExtraArgs = "--locked -p ${pname}";
 }
addedrust-toolchain.tomldiffbeforeafterboth
--- /dev/null
+++ b/rust-toolchain.toml
@@ -0,0 +1,3 @@
+[toolchain]
+channel = "nightly-2024-02-10"
+components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]