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
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,6 +1,5 @@
 use crate::{
 	better_nix_eval::Field,
-	command::MyCommand,
 	fleetdata::{FleetSecret, FleetSharedSecret, SecretData},
 	host::Config,
 	nix_go, nix_go_json,
@@ -9,16 +8,14 @@
 use chrono::{DateTime, Utc};
 use clap::Parser;
 use owo_colors::OwoColorize;
-use serde::{de::DeserializeOwned, Deserialize};
+use serde::Deserialize;
 use std::{
 	collections::{BTreeSet, HashSet},
 	io::{self, Cursor, Read},
-	path::{Path, PathBuf},
-	str::FromStr,
+	path::PathBuf,
 };
 use tabled::{Table, Tabled};
-use tempfile::tempdir;
-use tokio::fs::{self, read_to_string};
+use tokio::fs::read_to_string;
 use tracing::{error, info, info_span, warn, Instrument};
 
 #[derive(Parser)]
@@ -162,84 +159,13 @@
 }
 
 async fn generate_pure(
-	config: &Config,
+	_config: &Config,
 	_display_name: &str,
-	secret: Field,
-	default_generator: Field,
-	owners: &[String],
+	_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),
-	})
+	bail!("pure generators are broken for now")
 }
 async fn generate_impure(
 	config: &Config,
@@ -248,39 +174,53 @@
 	default_generator: Field,
 	owners: &[String],
 ) -> Result<FleetSecret> {
-	let config_field = &config.config_unchecked_field;
 	let generator = nix_go!(secret.generator);
+	let on: Option<String> = nix_go_json!(default_generator.impureOn);
 
-	let on: String = nix_go_json!(default_generator.impureOn);
-	let call_package = nix_go!(
-		config_field.hosts[{ on }]
-			.nixosSystem
-			.config
-			.nixpkgs
-			.resolvedPkgs
-			.callPackage
-	);
+	let host = if let Some(on) = &on {
+		config.host(on).await?
+	} else {
+		config.local_host()
+	};
+	let on_pkgs = host.pkgs().await?;
+	let call_package = nix_go!(on_pkgs.callPackage);
+	let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);
 
-	let host = config.host(&on).await?;
+	let mut recipients = Vec::new();
+	for owner in owners {
+		let key = config.key(owner).await?;
+		recipients.push(key);
+	}
+	let encrypt = nix_go!(mk_encrypt_secret(Obj {
+		recipients: { recipients },
+	}));
+
+	let generator = nix_go!(call_package(generator)(Obj {
+		encrypt,
+		rustfmt_please_newline: { true },
+	}));
 
-	let generator = nix_go!(call_package(generator)(Obj {}));
 	let generator = generator.build().await?;
 	let generator = generator
 		.get("out")
 		.ok_or_else(|| anyhow!("missing generateImpure out"))?;
 	let generator = host.remote_derivation(generator).await?;
 
-	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 = host.mktemp_dir().await?;
+	let out_parent = host.mktemp_dir().await?;
+	let out = format!("{out_parent}/out");
 
 	let mut gen = host.cmd(generator).await?;
-	gen.env("rageArgs", recipients).env("out", &out);
+	gen.env("out", &out);
+	if on.is_none() {
+		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.
+		let project_path: String = config
+			.directory
+			.clone()
+			.into_os_string()
+			.into_string()
+			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;
+		gen.env("FLEET_PROJECT", project_path);
+	}
 	gen.run().await.context("impure generator")?;
 
 	{
@@ -549,10 +489,7 @@
 					println!("{}", z85::encode(&data));
 				}
 			}
-			Secret::ReadPublic {
-				name,
-				machine,
-			} => {
+			Secret::ReadPublic { name, machine } => {
 				let secret = config.host_secret(&machine, &name)?;
 				let Some(public) = secret.public else {
 					bail!("no secret {name}");
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
before · cmds/fleet/src/host.rs
1use std::{2	env::current_dir,3	ffi::{OsStr, OsString},4	fmt::Display,5	io::Write,6	ops::Deref,7	path::PathBuf,8	str::FromStr,9	sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{anyhow, bail, Context, Result};13use clap::{ArgGroup, Parser};14use openssh::SessionBuilder;15use serde::de::DeserializeOwned;16use tempfile::NamedTempFile;1718use crate::{19	better_nix_eval::{Field, NixSessionPool},20	command::MyCommand,21	fleetdata::{FleetData, FleetSecret, FleetSharedSecret, SecretData},22	nix_go, nix_go_json,23};2425pub struct FleetConfigInternals {26	pub local_system: String,27	pub directory: PathBuf,28	pub opts: FleetOpts,29	pub data: Mutex<FleetData>,30	pub nix_args: Vec<OsString>,31	/// fleet_config.config32	pub config_field: Field,33	/// fleet_config.unchecked.config34	pub config_unchecked_field: Field,3536	/// import nixpkgs {system = local};37	pub default_pkgs: Field,38}3940#[derive(Clone)]41pub struct Config(Arc<FleetConfigInternals>);4243impl Deref for Config {44	type Target = FleetConfigInternals;4546	fn deref(&self) -> &Self::Target {47		&self.048	}49}5051pub struct ConfigHost {52	config: Config,53	pub name: String,54	pub local: bool,55	pub session: OnceLock<Arc<openssh::Session>>,5657	pub nixos_config: Field,58}59impl ConfigHost {60	async fn open_session(&self) -> Result<Arc<openssh::Session>> {61		assert!(!self.local, "do not open ssh connection to local session");62		// FIXME: TOCTOU63		if let Some(session) = &self.session.get() {64			return Ok((*session).clone());65		};66		let session = SessionBuilder::default();6768		let session = session69			.connect(&self.name)70			.await71			.map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;72		let session = Arc::new(session);73		self.session.set(session.clone()).expect("TOCTOU happened");74		Ok(session)75	}76	pub async fn mktemp_dir(&self) -> Result<String> {77		let mut cmd = self.cmd("mktemp").await?;78		cmd.arg("-d");79		let path = cmd.run_string().await?;80		Ok(path.trim_end().to_owned())81	}82	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {83		let mut cmd = self.cmd("cat").await?;84		cmd.arg(path);85		cmd.run_bytes().await86	}87	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {88		let mut cmd = self.cmd("cat").await?;89		cmd.arg(path);90		cmd.run_string().await91	}92	#[allow(dead_code)]93	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {94		let text = self.read_file_text(path).await?;95		Ok(serde_json::from_str(&text)?)96	}97	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>98	where99		<D as FromStr>::Err: Display,100	{101		let text = self.read_file_text(path).await?;102		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))103	}104	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {105		if self.local {106			Ok(MyCommand::new(cmd))107		} else {108			let session = self.open_session().await?;109			Ok(MyCommand::new_on(cmd, session))110		}111	}112113	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {114		let mut cmd = self.cmd("fleet-install-secrets").await?;115		cmd.arg("decrypt").eqarg("--secret", data.encode_z85());116		let encoded = cmd117			.sudo()118			.run_string()119			.await120			.context("failed to call remote host for decrypt")?;121		z85::decode(encoded.trim_end()).context("bad encoded data? outdated host?")122	}123	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {124		let mut cmd = self.cmd("fleet-install-secrets").await?;125		cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());126		for target in targets {127			let key = self.config.key(&target).await?;128			cmd.eqarg("--targets", key);129		}130		let encoded = cmd131			.sudo()132			.run_string()133			.await134			.context("failed to call remote host for decrypt")?;135		SecretData::decode_z85(encoded.trim_end()).context("bad encoded data? outdated host?")136	}137	/// Returns path for futureproofing, as path might change i.e on conversion to CA138	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {139		if self.local {140			// Path is located locally, thus already trusted.141			return Ok(path.to_owned());142		}143		let mut nix = MyCommand::new("nix");144		nix.arg("copy")145			.arg("--substitute-on-destination")146			.comparg("--to", format!("ssh-ng://{}", self.name))147			.arg(path);148		nix.run_nix().await.context("nix copy")?;149		Ok(path.to_owned())150	}151	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {152		let mut cmd = self.cmd("systemctl").await?;153		cmd.arg("stop").arg(name);154		cmd.sudo().run().await155	}156	pub async fn systemctl_start(&self, name: &str) -> Result<()> {157		let mut cmd = self.cmd("systemctl").await?;158		cmd.arg("start").arg(name);159		cmd.sudo().run().await160	}161162	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {163		let mut cmd = self.cmd("rm").await?;164		cmd.arg("-f").arg(path);165		if sudo {166			cmd = cmd.sudo()167		}168		cmd.run().await169	}170171	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {172		let nixos = &self.nixos_config;173		let secrets = nix_go!(nixos.secrets);174		let mut out = Vec::new();175		for name in secrets.list_fields().await? {176			let secret = nix_go!(secrets[{ name }]);177			let is_shared: bool = nix_go_json!(secret.shared);178			if is_shared {179				continue;180			}181			out.push(name);182		}183		Ok(out)184	}185	pub async fn secret_field(&self, name: &str) -> Result<Field> {186		let nixos = &self.nixos_config;187		Ok(nix_go!(nixos.secrets[{ name }]))188	}189}190191impl Config {192	pub fn should_skip(&self, host: &str) -> bool {193		if !self.opts.skip.is_empty() {194			self.opts.skip.iter().any(|h| h as &str == host)195		} else if !self.opts.only.is_empty() {196			!self.opts.only.iter().any(|h| h as &str == host)197		} else {198			false199		}200	}201	pub fn is_local(&self, host: &str) -> bool {202		self.opts.localhost.as_ref().map(|s| s as &str) == Some(host)203	}204205	pub async fn host(&self, name: &str) -> Result<ConfigHost> {206		let config = &self.config_unchecked_field;207		let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);208		Ok(ConfigHost {209			config: self.clone(),210			name: name.to_owned(),211			local: self.is_local(name),212			session: OnceLock::new(),213			nixos_config,214		})215	}216	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {217		let config = &self.config_unchecked_field;218		let names = nix_go!(config.hosts).list_fields().await?;219		let mut out = vec![];220		for name in names {221			out.push(self.host(&name).await?);222		}223		Ok(out)224	}225	pub async fn system_config(&self, host: &str) -> Result<Field> {226		let fleet_field = &self.config_unchecked_field;227		Ok(nix_go!(fleet_field.hosts[{ host }].nixosSystem.config))228	}229230	pub(super) fn data(&self) -> MutexGuard<FleetData> {231		self.data.lock().unwrap()232	}233	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {234		self.data.lock().unwrap()235	}236	/// Shared secrets configured in fleet.nix or in flake237	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {238		let config_field = &self.config_unchecked_field;239		nix_go!(config_field.sharedSecrets).list_fields().await240	}241	/// Shared secrets configured in fleet.nix242	pub fn list_shared(&self) -> Vec<String> {243		let data = self.data();244		data.shared_secrets.keys().cloned().collect()245	}246	pub fn has_shared(&self, name: &str) -> bool {247		let data = self.data();248		data.shared_secrets.contains_key(name)249	}250	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {251		let mut data = self.data_mut();252		data.shared_secrets.insert(name.to_owned(), shared);253	}254	pub fn remove_shared(&self, secret: &str) {255		let mut data = self.data_mut();256		data.shared_secrets.remove(secret);257	}258259	pub fn list_secrets(&self, host: &str) -> Vec<String> {260		let data = self.data();261		let Some(secrets) = data.host_secrets.get(host) else {262			return Vec::new();263		};264		secrets.keys().cloned().collect()265	}266267	pub fn has_secret(&self, host: &str, secret: &str) -> bool {268		let data = self.data();269		let Some(host_secrets) = data.host_secrets.get(host) else {270			return false;271		};272		host_secrets.contains_key(secret)273	}274	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {275		let mut data = self.data_mut();276		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();277		host_secrets.insert(secret, value);278	}279280	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {281		let data = self.data();282		let Some(host_secrets) = data.host_secrets.get(host) else {283			bail!("no secrets for machine {host}");284		};285		let Some(secret) = host_secrets.get(secret) else {286			bail!("machine {host} has no secret {secret}");287		};288		Ok(secret.clone())289	}290	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {291		let data = self.data();292		let Some(secret) = data.shared_secrets.get(secret) else {293			bail!("no shared secret {secret}");294		};295		Ok(secret.clone())296	}297	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {298		let config_field = &self.config_unchecked_field;299		Ok(nix_go_json!(300			config_field.sharedSecrets[{ secret }].expectedOwners301		))302	}303304	pub fn save(&self) -> Result<()> {305		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;306		let data = nixlike::serialize(&self.data() as &FleetData)?;307		tempfile.write_all(308			format!(309				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",310				data311			)312			.as_bytes(),313		)?;314		let mut fleet_data_path = self.directory.clone();315		fleet_data_path.push("fleet.nix");316		tempfile.persist(fleet_data_path)?;317		Ok(())318	}319}320321#[derive(Parser, Clone)]322#[clap(group = ArgGroup::new("target_hosts"))]323pub struct FleetOpts {324	/// All hosts except those would be skipped325	#[clap(long, number_of_values = 1, group = "target_hosts")]326	only: Vec<String>,327328	/// Hosts to skip329	#[clap(long, number_of_values = 1, group = "target_hosts")]330	skip: Vec<String>,331332	/// Host, which should be threaten as current machine333	#[clap(long)]334	pub localhost: Option<String>,335336	/// Override detected system for host, to perform builds via337	/// binfmt-declared qemu instead of trying to crosscompile338	#[clap(long, default_value = "detect")]339	pub local_system: String,340}341342impl FleetOpts {343	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {344		if self.localhost.is_none() {345			self.localhost346				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());347		}348		let directory = current_dir()?;349350		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;351		let root_field = pool.get().await?;352353		let builtins_field = Field::field(root_field.clone(), "builtins").await?;354		if self.local_system == "detect" {355			self.local_system = nix_go_json!(builtins_field.currentSystem);356		}357		let local_system = self.local_system.clone();358359		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;360		let fleet_field = nix_go!(fleet_root.default);361362		let config_field = nix_go!(fleet_field.config);363		let config_unchecked_field = nix_go!(fleet_field.unchecked.config);364365		let import = nix_go!(builtins_field.import);366		let overlays = nix_go!(fleet_field.overlays);367		let nixpkgs = nix_go!(fleet_field.nixpkgs | import);368369		let default_pkgs = nix_go!(nixpkgs(Obj {370			overlays,371			system: { self.local_system.clone() },372		}));373374		let mut fleet_data_path = directory.clone();375		fleet_data_path.push("fleet.nix");376		let bytes = std::fs::read_to_string(fleet_data_path)?;377		let data = nixlike::parse_str(&bytes)?;378379		Ok(Config(Arc::new(FleetConfigInternals {380			opts: self,381			directory,382			data,383			local_system,384			nix_args,385			config_field,386			config_unchecked_field,387			default_pkgs,388		})))389	}390}
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"]