git.delta.rocks / jrsonnet / refs/commits / 15ef984b843b

difftreelog

refactor secret management

Yaroslav Bolyukin2023-02-05parent: #4e7930a.patch.diff
in: trunk

19 files changed

modifiedCargo.lockdiffbeforeafterboth
before · Cargo.lock
214 packageslockfile v3
after · Cargo.lock
253 packageslockfile v3
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -13,14 +13,14 @@
 tempfile = "3.2"
 once_cell = "1.5"
 hostname = "0.3.1"
-age-core = "0.8.0"
+age-core = "0.9.0"
 peg = "0.8.0"
 nixlike = { path = "../../crates/nixlike" }
-age = { version = "0.8.1", features = ["ssh", "armor"] }
-base64 = "0.13.0"
+age = { version = "0.9.0", features = ["ssh", "armor"] }
+base64 = "0.21.0"
 chrono = { version = "0.4.19", features = ["serde"] }
 z85 = "3.0.3"
-clap = { version = "3.1.0", features = [
+clap = { version = "4.0.29", features = [
 	"derive",
 	"env",
 	"wrap_help",
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -8,9 +8,6 @@
 
 #[derive(Parser, Clone)]
 pub struct BuildSystems {
-	/// Jobs to run locally
-	#[clap(long)]
-	jobs: Option<usize>,
 	/// Do not continue on error
 	#[clap(long)]
 	fail_fast: bool,
@@ -19,13 +16,6 @@
 	privileged_build: bool,
 	#[clap(subcommand)]
 	subcommand: Subcommand,
-
-	/// --builders arg for nix
-	#[clap(long)]
-	builders: Option<String>,
-	/// --show-trace arg for nix
-	#[structopt(long)]
-	show_trace: bool,
 }
 
 enum UploadAction {
@@ -126,7 +116,7 @@
 			Command::new("nix")
 		};
 		nix_build
-			.args(&[
+			.args([
 				"build",
 				"--impure",
 				"--json",
@@ -140,22 +130,9 @@
 					"buildSystems.{}.{host}",
 					action.build_attr()
 				)),
-			);
+			)
+			.args(&config.nix_args);
 
-		if self.show_trace {
-			nix_build.arg("--show-trace");
-		}
-		if let Some(builders) = &self.builders {
-			nix_build.arg("--builders").arg(builders);
-		}
-		if let Some(jobs) = &self.jobs {
-			nix_build.arg("--max-jobs");
-			nix_build.arg(format!("{}", jobs));
-		}
-		if !self.fail_fast {
-			nix_build.arg("--keep-going");
-		}
-
 		nix_build.run_nix().await?;
 		let built = std::fs::canonicalize(built)?;
 
@@ -166,7 +143,7 @@
 					let mut tries = 0;
 					loop {
 						match Command::new("nix")
-							.args(&["copy", "--to"])
+							.args(["copy", "--to"])
 							.arg(format!("ssh://root@{}", host))
 							.arg(&built)
 							.inherit_stdio()
@@ -188,7 +165,7 @@
 						info!("switching generation");
 						config
 							.command_on(&host, "nix-env", true)
-							.args(&["-p", "/nix/var/nix/profiles/system", "--set"])
+							.args(["-p", "/nix/var/nix/profiles/system", "--set"])
 							.arg(&built)
 							.inherit_stdio()
 							.run()
@@ -219,18 +196,10 @@
 					Command::new("nix")
 				};
 				nix_build
-					.args(&["build", "--impure", "--no-link", "--out-link"])
+					.args(["build", "--impure", "--no-link", "--out-link"])
 					.arg(&out)
-					.arg(
-						config.configuration_attr_name(&format!("buildSystems.sdImage.{}", host,)),
-					);
-				if let Some(builders) = &self.builders {
-					nix_build.arg("--builders").arg(builders);
-				}
-				if let Some(jobs) = &self.jobs {
-					nix_build.arg("--max-jobs");
-					nix_build.arg(format!("{}", jobs));
-				}
+					.arg(config.configuration_attr_name(&format!("buildSystems.sdImage.{}", host,)))
+					.args(&config.nix_args);
 				if !self.fail_fast {
 					nix_build.arg("--keep-going");
 				}
@@ -250,21 +219,15 @@
 					Command::new("nix")
 				};
 				nix_build
-					.args(&["build", "--impure", "--no-link", "--out-link"])
+					.args(["build", "--impure", "--no-link", "--out-link"])
 					.arg(&out)
 					.arg(
 						config.configuration_attr_name(&format!(
 							"buildSystems.installationCd.{}",
 							host,
 						)),
-					);
-				if let Some(builders) = &self.builders {
-					nix_build.arg("--builders").arg(builders);
-				}
-				if let Some(jobs) = &self.jobs {
-					nix_build.arg("--max-jobs");
-					nix_build.arg(format!("{}", jobs));
-				}
+					)
+					.args(&config.nix_args);
 				if !self.fail_fast {
 					nix_build.arg("--keep-going");
 				}
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,8 +1,15 @@
-use std::collections::BTreeSet;
+use std::{collections::BTreeSet, time::Duration};
 
-use crate::host::Config;
-use anyhow::{ensure, Result};
+use crate::{command::CommandExt, host::Config};
+use anyhow::{bail, ensure, Result};
 use clap::Parser;
+use nixlike::format_nix;
+use serde_json::{json, Value};
+use tokio::{
+	fs::{self, File},
+	io::AsyncWriteExt,
+	process::Command,
+};
 
 #[derive(Parser)]
 pub struct Info {
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -2,13 +2,17 @@
 	fleetdata::{FleetSecret, FleetSharedSecret},
 	host::Config,
 };
-use anyhow::{bail, Result};
+use age::{Decryptor, Encryptor};
+use anyhow::{bail, ensure, Context, Result};
 use clap::Parser;
 use futures::{StreamExt, TryStreamExt};
 use std::{
-	io::{self, Cursor, Read},
+	collections::HashSet,
+	io::{self, Cursor, Read, Write},
+	iter,
 	path::PathBuf,
 };
+use tracing::{info, warn};
 
 #[derive(Parser)]
 pub enum Secrets {
@@ -42,6 +46,23 @@
 		#[clap(long)]
 		public_file: Option<PathBuf>,
 	},
+	/// Read secret from remote host, requires sudo on said host
+	Read {
+		name: String,
+		machine: String,
+	},
+	UpdateShared {
+		name: String,
+
+		machines: Option<Vec<String>>,
+
+		add_machines: Vec<String>,
+		remove_machines: Vec<String>,
+
+		/// Which host should we use to decrypt
+		prefer_identities: Vec<String>,
+	},
+	Regenerate,
 }
 
 impl Secrets {
@@ -78,9 +99,10 @@
 						let recipients = recipients
 							.iter()
 							.cloned()
-							.map(|r| Box::new(r) as Box<dyn age::Recipient>)
+							.map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
 							.collect();
 						let mut encryptor = age::Encryptor::with_recipients(recipients)
+							.expect("recipients provided")
 							.wrap_output(&mut encrypted)?;
 						io::copy(&mut Cursor::new(input), &mut encryptor)?;
 						encryptor.finish()?;
@@ -101,7 +123,7 @@
 							secret,
 							public: match (public, public_file) {
 								(Some(v), None) => Some(v),
-								(None, Some(v)) => Some(std::fs::read_to_string(&v)?),
+								(None, Some(v)) => Some(std::fs::read_to_string(v)?),
 								(Some(_), Some(_)) => {
 									bail!("only public or public_file should be set")
 								}
@@ -123,10 +145,14 @@
 				let secret = {
 					let mut input = vec![];
 					io::stdin().read_to_end(&mut input)?;
+					if input.is_empty() {
+						bail!("no data provided")
+					}
 
 					let mut encrypted = vec![];
-					let recipient = Box::new(recipient) as Box<dyn age::Recipient>;
+					let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;
 					let mut encryptor = age::Encryptor::with_recipients(vec![recipient])
+						.expect("recipients provided")
 						.wrap_output(&mut encrypted)?;
 					io::copy(&mut Cursor::new(input), &mut encryptor)?;
 					encryptor.finish()?;
@@ -145,13 +171,180 @@
 						secret,
 						public: match (public, public_file) {
 							(Some(v), None) => Some(v),
-							(None, Some(v)) => Some(std::fs::read_to_string(&v)?),
+							(None, Some(v)) => Some(std::fs::read_to_string(v)?),
 							(Some(_), Some(_)) => bail!("only public or public_file should be set"),
 							(None, None) => None,
 						},
 					},
 				);
 			}
+			// TODO: Instead of using sudo, decode secret on remote machine
+			#[allow(clippy::await_holding_refcell_ref)]
+			Secrets::Read { name, machine } => {
+				let data = config.data();
+
+				let Some(host_secrets) = data.host_secrets.get(&machine) else {
+                    bail!("no secrets for machine {machine}");
+                };
+				let Some(secret) = host_secrets.get(&name) else {
+                    bail!("machine {machine} has no secret {name}");
+                };
+				if secret.secret.is_empty() {
+					bail!("no secret {name}");
+				}
+				let identity = config.identity(&machine).await?;
+				let decryptor = Decryptor::new(Cursor::new(&secret.secret))?;
+				let decryptor = match decryptor {
+					Decryptor::Recipients(r) => r,
+					Decryptor::Passphrase(_) => bail!("should be recipients"),
+				};
+				let mut decryptor = decryptor
+					.decrypt(iter::once(&identity as &dyn age::Identity))
+					.context("failed to decrypt, wrong key?")?;
+
+				let mut decrypted = Vec::new();
+				decryptor
+					.read_to_end(&mut decrypted)
+					.context("failed to decrypt")?;
+				// secret.secret
+				std::io::stdout().lock().write_all(&decrypted)?;
+			}
+			Secrets::UpdateShared {
+				name,
+				machines,
+				mut add_machines,
+				mut remove_machines,
+				prefer_identities,
+			} => {
+				let mut data = config.data_mut();
+				if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {
+					bail!("no operation");
+				}
+
+				let Some(mut secret) = data.shared_secrets.get_mut(&name) else {
+                    bail!("no shared secret {name}");
+                };
+				if secret.secret.secret.is_empty() {
+					bail!("no secret");
+				}
+
+				let initial_machines = secret.owners.clone();
+				let mut target_machines = secret.owners.clone();
+
+				// ensure!(machines.is_some() || !add_machines.is_empty() || )
+				if let Some(machines) = machines {
+					ensure!(
+						add_machines.is_empty() && remove_machines.is_empty(),
+						"can't combine --machines and --add-machines/--remove-machines"
+					);
+					let target = initial_machines.iter().collect::<HashSet<_>>();
+					let source = machines.iter().collect::<HashSet<_>>();
+					for removed in target.difference(&source) {
+						remove_machines.push((*removed).clone());
+					}
+					for added in source.difference(&target) {
+						add_machines.push((*added).clone());
+					}
+				}
+
+				for machine in &remove_machines {
+					let mut removed = false;
+					while let Some(pos) = target_machines.iter().position(|m| m == machine) {
+						target_machines.swap_remove(pos);
+						removed = true;
+					}
+					if !removed {
+						bail!("secret is not enabled for {machine}");
+					}
+				}
+				for machine in &add_machines {
+					if target_machines.iter().any(|m| m == machine) {
+						warn!("secret is already added to {machine}");
+					}
+				}
+				if remove_machines.is_empty() {
+					warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
+				}
+				if target_machines.is_empty() {
+					info!("no machines left for secret, removing it");
+					data.shared_secrets.remove(&name);
+					return Ok(());
+				}
+
+				let identity_holder = if !prefer_identities.is_empty() {
+					prefer_identities
+						.iter()
+						.find(|i| initial_machines.iter().any(|s| s == *i))
+				} else {
+					secret.owners.first()
+				};
+				let Some(identity_holder) = identity_holder else {
+                    bail!("no available holder found");
+                };
+				let target_recipients = futures::stream::iter(&target_machines)
+					.flat_map(|m| futures::stream::once(config.recipient(m)))
+					.collect::<Vec<_>>()
+					.await
+					.into_iter()
+					.map(|v| v.map(|v| Box::new(v) as Box<dyn age::Recipient + Send>))
+					.collect::<Result<Vec<_>>>()?;
+
+				let identity = config.identity(identity_holder).await?;
+				let decryptor = Decryptor::new(Cursor::new(&secret.secret.secret))?;
+				let decryptor = match decryptor {
+					Decryptor::Recipients(r) => r,
+					Decryptor::Passphrase(_) => bail!("should be recipients"),
+				};
+				let mut decryptor = decryptor
+					.decrypt(iter::once(&identity as &dyn age::Identity))
+					.context("failed to decrypt, wrong key?")?;
+
+				let mut decrypted = Vec::new();
+				decryptor
+					.read_to_end(&mut decrypted)
+					.context("failed to decrypt")?;
+
+				let mut encrypted = vec![];
+				let mut encryptor = Encryptor::with_recipients(target_recipients)
+					.expect("recipients provided")
+					.wrap_output(&mut encrypted)?;
+				io::copy(&mut Cursor::new(decrypted), &mut encryptor)?;
+				encryptor.finish()?;
+
+				secret.secret.secret = encrypted;
+			}
+			Secrets::Regenerate => {
+				// config.data_mut().shared_secrets
+				{
+					let expected_shared_set =
+						config.shared_config_attr_names("sharedSecrets").await?;
+					let expected_shared_set = expected_shared_set.iter().collect::<HashSet<_>>();
+					let shared_set = config.data();
+					let shared_set = shared_set.shared_secrets.keys().collect::<HashSet<_>>();
+					for removed in expected_shared_set.difference(&shared_set) {
+						warn!("secret needs to be generated: {removed}")
+					}
+				}
+				let mut to_remove = Vec::new();
+				for (name, data) in &config.data().shared_secrets {
+					let expected_owners: Vec<String> = config
+						.shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))
+						.await?;
+					if expected_owners.is_empty() {
+						warn!("secret was removed from fleet config: {name}, removing from data");
+						to_remove.push(name.to_string());
+						continue;
+					}
+					let set = data.owners.iter().collect::<HashSet<_>>();
+					let expected_set = expected_owners.iter().collect::<HashSet<_>>();
+					if set != expected_set {
+						warn!("reconfiguring owners for {name}");
+					}
+				}
+				for k in to_remove {
+					config.data_mut().shared_secrets.remove(&k);
+				}
+			}
 		}
 		Ok(())
 	}
modifiedcmds/fleet/src/command.rsdiffbeforeafterboth
--- a/cmds/fleet/src/command.rs
+++ b/cmds/fleet/src/command.rs
@@ -128,8 +128,15 @@
 							};
 							match log {
 								NixLog::Msg { msg, raw_msg, .. } => {
-									if !(msg.ends_with(" is dirty") && msg.contains("warning:") && msg.contains(" Git tree ")) {
-										info!(target: "nix", "{}", raw_msg.unwrap_or(msg))
+									if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty"))
+										&& !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake")
+										&& msg != "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy" {
+										if let Some(raw_msg) = raw_msg {
+											info!(target: "nix", "{raw_msg}\n{msg}")
+										}else {
+											info!(target: "nix", "{msg}")
+
+										}
 									}
 								},
 								NixLog::Start { ref fields, typ, .. } if typ == 105 && !fields.is_empty() => {
@@ -163,6 +170,9 @@
 								NixLog::Start { text, level: 1, typ: 111, .. } if text.starts_with("waiting for a machine to build ") => {
 									// Useless repeating notification about build
 								}
+								NixLog::Start { text, level: 3, typ: 111, .. } if text.starts_with("resolved derivation:  ") => {
+									// CA resolved
+								}
 								NixLog::Stop { .. } => {},
 								NixLog::Result { .. } => {},
 								_ => warn!("unknown log: {:?}", log)
modifiedcmds/fleet/src/fleetdata.rsdiffbeforeafterboth
--- a/cmds/fleet/src/fleetdata.rs
+++ b/cmds/fleet/src/fleetdata.rs
@@ -1,6 +1,17 @@
+use anyhow::{bail, Result};
 use chrono::{DateTime, Utc};
+use nixlike::format_nix;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use serde_json::{json, Value};
 use std::collections::BTreeMap;
+use tempfile::TempDir;
+use tokio::{
+	fs::{self, File},
+	io::AsyncWriteExt,
+	process::Command,
+};
+
+use crate::command::CommandExt;
 
 #[derive(Serialize, Deserialize, Default)]
 #[serde(rename_all = "camelCase")]
@@ -52,7 +63,7 @@
 where
 	S: Serializer,
 {
-	serializer.serialize_str(&z85::encode(&key))
+	serializer.serialize_str(&z85::encode(key))
 }
 
 fn from_z85<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
@@ -61,5 +72,39 @@
 {
 	use serde::de::Error;
 	String::deserialize(deserializer)
-		.and_then(|string| z85::decode(&string).map_err(|err| Error::custom(err.to_string())))
+		.and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))
+}
+
+pub async fn dummy_flake() -> Result<TempDir> {
+	let data_str = fs::read_to_string("fleet.nix").await?;
+
+	let mut cmd = Command::new("nix");
+	cmd.arg("flake").arg("metadata").arg("--json");
+
+	let flake_dir = tempfile::tempdir()?;
+	let mut flake_nix = flake_dir.path().to_path_buf();
+	flake_nix.push("flake.nix");
+	// flake_dir
+
+	File::create(&flake_nix)
+		.await?
+		.write_all(
+			format_nix(&format!(
+				"
+						{{
+							outputs = {{self, ...}}: {{
+								data = {data_str};
+							}};
+						}}
+					"
+			))
+			.as_bytes(),
+		)
+		.await?;
+
+	// std::thread::sleep(Duration::MAX);
+	// flake_dir.close()
+	// FIXME
+	dbg!(&flake_nix);
+	Ok(flake_dir)
 }
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -2,6 +2,7 @@
 	cell::{Ref, RefCell, RefMut},
 	env::current_dir,
 	ffi::{OsStr, OsString},
+	io::Write,
 	ops::Deref,
 	path::PathBuf,
 	sync::Arc,
@@ -10,15 +11,20 @@
 use anyhow::Result;
 use clap::{ArgGroup, Parser};
 use serde::de::DeserializeOwned;
+use tempfile::{NamedTempFile, TempDir};
 use tokio::process::Command;
 
-use crate::{command::CommandExt, fleetdata::FleetData};
+use crate::{
+	command::CommandExt,
+	fleetdata::{dummy_flake, FleetData},
+};
 
 pub struct FleetConfigInternals {
 	pub local_system: String,
 	pub directory: PathBuf,
 	pub opts: FleetOpts,
 	pub data: RefCell<FleetData>,
+	pub nix_args: Vec<OsString>,
 }
 
 #[derive(Clone)]
@@ -80,7 +86,27 @@
 		Command::new("nix")
 			.arg("eval")
 			.arg(self.configuration_attr_name("configuredHosts"))
-			.args(&["--apply", "builtins.attrNames", "--json", "--show-trace"])
+			.args(["--apply", "builtins.attrNames", "--json", "--show-trace"])
+			.args(&self.nix_args)
+			.run_nix_json()
+			.await
+	}
+	pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {
+		Command::new("nix")
+			.arg("eval")
+			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))
+			.args(["--json", "--show-trace"])
+			.args(&self.nix_args)
+			.run_nix_json()
+			.await
+	}
+	pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {
+		Command::new("nix")
+			.arg("eval")
+			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))
+			.args(["--apply", "builtins.attrNames"])
+			.args(["--json", "--show-trace"])
+			.args(&self.nix_args)
 			.run_nix_json()
 			.await
 	}
@@ -93,7 +119,8 @@
 					host, attr
 				)),
 			)
-			.args(&["--json", "--show-trace"])
+			.args(["--json", "--show-trace"])
+			.args(&self.nix_args)
 			.run_nix_json()
 			.await
 	}
@@ -106,16 +133,18 @@
 	}
 
 	pub fn save(&self) -> Result<()> {
-		let mut fleet_data_path = self.directory.clone();
-		fleet_data_path.push("fleet.nix");
+		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;
 		let data = nixlike::serialize(&self.data() as &FleetData)?;
-		std::fs::write(
-			fleet_data_path,
+		tempfile.write_all(
 			format!(
 				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",
 				data
-			),
+			)
+			.as_bytes(),
 		)?;
+		let mut fleet_data_path = self.directory.clone();
+		fleet_data_path.push("fleet.nix");
+		tempfile.persist(fleet_data_path)?;
 		Ok(())
 	}
 }
@@ -143,7 +172,7 @@
 }
 
 impl FleetOpts {
-	pub fn build(mut self) -> Result<Config> {
+	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {
 		let local_system = self.local_system.clone();
 		if self.localhost.is_none() {
 			self.localhost
@@ -161,6 +190,7 @@
 			directory,
 			data,
 			local_system,
+			nix_args,
 		})))
 	}
 }
modifiedcmds/fleet/src/keys.rsdiffbeforeafterboth
--- a/cmds/fleet/src/keys.rs
+++ b/cmds/fleet/src/keys.rs
@@ -35,6 +35,16 @@
 			Ok(key)
 		}
 	}
+	/// Insecure, requires root
+	pub async fn identity(&self, host: &str) -> anyhow::Result<age::ssh::Identity> {
+		warn!("Loading private key for {host}");
+		let key = self
+			.command_on(host, "cat", true)
+			.arg("/etc/ssh/ssh_host_ed25519_key")
+			.run_string()
+			.await?;
+		Ok(age::ssh::Identity::from_buffer(key.as_bytes(), None)?)
+	}
 	pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {
 		let key = self.key(host).await?;
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -59,8 +59,9 @@
 		.map_err(|e| anyhow!("Failed to initialize logger: {}", e))?;
 
 	info!("Starting");
-	let opts = RootOpts::parse();
-	let config = opts.fleet_opts.build()?;
+	let mut os_args = std::env::args_os();
+	let opts = RootOpts::parse_from((&mut os_args).take_while(|v| v != "--"));
+	let config = opts.fleet_opts.build(os_args.collect()).await?;
 
 	match run_command(&config, opts.command).await {
 		Ok(()) => {
modifiedcmds/install-secrets/Cargo.tomldiffbeforeafterboth
--- a/cmds/install-secrets/Cargo.toml
+++ b/cmds/install-secrets/Cargo.toml
@@ -4,14 +4,14 @@
 edition = "2021"
 
 [dependencies]
-age = { version = "0.8.1", features = ["ssh"] }
+age = { version = "0.9.0", features = ["ssh"] }
 anyhow = "1.0.44"
-env_logger = "0.9.0"
+env_logger = "0.10.0"
 log = "0.4.14"
-nix = "0.25.0"
+nix = "0.26.1"
 serde = "1.0.130"
-serde_json = "1.0.68"
-clap = { version = "3.1.0", features = [
+serde_json = "1.0.89"
+clap = { version = "4.0.29", features = [
 	"derive",
 	"env",
 	"wrap_help",
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -1,7 +1,7 @@
 use age::Decryptor;
 use anyhow::{anyhow, bail, Context, Result};
 use clap::Parser;
-use log::error;
+use log::{error, info, warn};
 use nix::sys::stat::Mode;
 use nix::unistd::{chown, Group, User};
 use serde::{Deserialize, Deserializer};
@@ -43,7 +43,7 @@
 	use serde::de::Error;
 	if let Some(v) = <Option<String>>::deserialize(deserializer)? {
 		Ok(Some(
-			z85::decode(&v).map_err(|err| Error::custom(err.to_string()))?,
+			z85::decode(v).map_err(|err| Error::custom(err.to_string()))?,
 		))
 	} else {
 		Ok(None)
@@ -71,6 +71,7 @@
 			.context("failed to persist")?;
 	}
 	if value.secret.is_none() {
+		info!("no secret data found");
 		return Ok(());
 	}
 	let secret = value.secret.as_ref().unwrap();
@@ -109,6 +110,9 @@
 			.context("failed to decrypt")?;
 		decrypted
 	};
+	if decrypted.is_empty() {
+		warn!("secret is decoded as empty, something is broken?");
+	}
 
 	io::copy(&mut Cursor::new(&decrypted), &mut stable_temp)
 		.context("failed to write decrypted file")?;
@@ -155,6 +159,7 @@
 
 	let mut failed = false;
 	for (name, value) in data {
+		info!("initializing secret {name}");
 		if let Err(e) = init_secret(&identity, value) {
 			error!(
 				"{:?}",
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -4,8 +4,12 @@
 edition = "2021"
 
 [dependencies]
-dprint-core = "0.51.0"
+alejandra = "1.5.0"
+rnix = "=0.10.2"
 linked-hash-map = "0.5.4"
 peg = "0.8.0"
 serde = "1.0.130"
 thiserror = "1.0.29"
+serde_json = "1.0.91"
+ron = "0.8.0"
+serde-transcode = "1.1.1"
modifiedcrates/nixlike/src/lib.rsdiffbeforeafterboth
--- a/crates/nixlike/src/lib.rs
+++ b/crates/nixlike/src/lib.rs
@@ -121,5 +121,9 @@
 
 #[test]
 fn test() {
-	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"");
+	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");
+}
+pub fn format_nix(value: &String) -> String {
+	let (_, out) = alejandra::format::in_memory("".to_owned(), value.to_owned());
+	out
 }
modifiedcrates/nixlike/src/to_string.rsdiffbeforeafterboth
--- a/crates/nixlike/src/to_string.rs
+++ b/crates/nixlike/src/to_string.rs
@@ -1,10 +1,6 @@
 use crate::Value;
-use dprint_core::formatting::{
-	condition_resolvers, conditions, format, ConditionResolverContext, Info, PrintItems,
-	PrintOptions, Signal,
-};
 
-fn write_nix_obj_key_buf(k: &str, v: &Value, out: &mut PrintItems) {
+fn write_nix_obj_key_buf(k: &str, v: &Value, out: &mut String) {
 	if k.contains('.') {
 		out.push_str("\"");
 		out.push_str(k);
@@ -27,99 +23,54 @@
 	}
 }
 
-fn write_nix_buf(value: &Value, out: &mut PrintItems) {
+fn write_nix_str(str: &str, out: &mut String) {
+	out.push_str(&format!(
+		"\"{}\"",
+		str.replace('\\', "\\\\")
+			.replace('"', "\\\"")
+			.replace('\n', "\\n")
+			.replace('\t', "\\t")
+			.replace('\r', "\\r")
+			.replace('$', "\\$")
+	))
+}
+
+fn write_nix_buf(value: &Value, out: &mut String) {
 	match value {
 		Value::Null => out.push_str("null"),
 		Value::Boolean(v) => out.push_str(if *v { "true" } else { "false" }),
 		Value::Number(n) => out.push_str(&format!("{}", n)),
-		Value::String(s) => out.push_str(&format!(
-			"\"{}\"",
-			s.replace('\\', "\\\\")
-				.replace('"', "\\\"")
-				.replace('\n', "\\n")
-				.replace('\t', "\\t")
-				.replace('\r', "\\r")
-				.replace('$', "\\$")
-		)),
+		Value::String(s) => write_nix_str(s, out),
 		Value::Array(a) => {
 			if a.is_empty() {
 				out.push_str("[ ]");
 			} else {
-				let start_info = Info::new("start");
-				let end_info = Info::new("end");
-				let is_multiple_lines = move |ctx: &mut ConditionResolverContext| {
-					condition_resolvers::is_multiple_lines(ctx, &start_info, &end_info)
-				};
-				out.push_str("[");
-				out.push_info(start_info);
-				out.push_signal(Signal::StartIndent);
-				out.push_condition(conditions::if_true_or(
-					"array start",
-					is_multiple_lines,
-					Signal::NewLine.into(),
-					Signal::SpaceOrNewLine.into(),
-				));
+				out.push('[');
 				for item in a {
 					write_nix_buf(item, out);
-					out.push_condition(conditions::if_true_or(
-						"element separator",
-						is_multiple_lines,
-						Signal::NewLine.into(),
-						Signal::SpaceOrNewLine.into(),
-					));
+					out.push('\n');
 				}
-				out.push_signal(Signal::FinishIndent);
-				out.push_info(end_info);
-				out.push_str("]");
+				out.push(']');
 			}
 		}
 		Value::Object(obj) => {
 			if obj.is_empty() {
 				out.push_str("{ }")
 			} else {
-				let start_info = Info::new("start");
-				let end_info = Info::new("end");
-				let is_multiple_lines = move |ctx: &mut ConditionResolverContext| {
-					condition_resolvers::is_multiple_lines(ctx, &start_info, &end_info)
-				};
-				out.push_str("{");
-				out.push_info(start_info);
-				out.push_signal(Signal::StartIndent);
-				out.push_condition(conditions::if_true_or(
-					"object start",
-					is_multiple_lines,
-					Signal::NewLine.into(),
-					Signal::SpaceOrNewLine.into(),
-				));
+				out.push('{');
 				for (k, v) in obj {
 					write_nix_obj_key_buf(k, v, out);
-					out.push_condition(conditions::if_true_or(
-						"element separator",
-						is_multiple_lines,
-						Signal::NewLine.into(),
-						Signal::SpaceOrNewLine.into(),
-					));
+					out.push('\n');
 				}
-				out.push_signal(Signal::FinishIndent);
-				out.push_info(end_info);
-				out.push_str("}");
+				out.push('}');
 			}
 		}
 	};
 }
 
 pub fn write_nix(value: &Value) -> String {
-	format(
-		|| {
-			let mut items = PrintItems::new();
-			write_nix_buf(value, &mut items);
-			items
-		},
-		PrintOptions {
-			max_width: 120,
-			use_tabs: false,
-			indent_width: 2,
-			new_line_text: "\n",
-		},
-	)
+	let mut out = String::new();
+	write_nix_buf(value, &mut out);
+	let (_, out) = alejandra::format::in_memory("".to_owned(), out);
+	out
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,26 @@
   "nodes": {
     "flake-utils": {
       "locked": {
-        "lastModified": 1653893745,
-        "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=",
+        "lastModified": 1667395993,
+        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1",
+        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "locked": {
+        "lastModified": 1659877975,
+        "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
         "type": "github"
       },
       "original": {
@@ -17,16 +32,16 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1655726478,
-        "narHash": "sha256-n0ArNOgTpxabE1wp7iGGYQMf8CBUN1/SjItuV+vyjvw=",
+        "lastModified": 1670700221,
+        "narHash": "sha256-+Fy/Wu8qeAppA14R4gLSlxmD0jGNVWYrgAJUaL23qkI=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "439dae554611b75c181e09ad55b8485ae50da0c6",
+        "rev": "ccf0f09e2e6744dcd721860a44c633e8708fde2b",
         "type": "github"
       },
       "original": {
         "owner": "nixos",
-        "ref": "staging-next",
+        "ref": "master",
         "repo": "nixpkgs",
         "type": "github"
       }
@@ -39,13 +54,18 @@
       }
     },
     "rust-overlay": {
-      "flake": false,
+      "inputs": {
+        "flake-utils": "flake-utils_2",
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
       "locked": {
-        "lastModified": 1655692957,
-        "narHash": "sha256-PubmAIcfn/PQRA1G4FdEA9r+oo5JpgjPlx5EcTAgelM=",
+        "lastModified": 1670639101,
+        "narHash": "sha256-UvPSgbtaOk9WcgVqywnvQXOEEHx6OXdG+QXIwnbyvCw=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "72b262045a2afa8f6dca94572f6ed5409ef346ab",
+        "rev": "d00c488cb455c21fea731167bf8c1b8da605aac3",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -2,9 +2,9 @@
   description = "NixOS configuration management";
 
   inputs = {
-    nixpkgs.url = "github:nixos/nixpkgs/staging-next";
-    rust-overlay = { url = "github:oxalica/rust-overlay"; flake = false; };
-    flake-utils.url = "github:numtide/flake-utils";
+    nixpkgs.url = "github:nixos/nixpkgs/master";
+    rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; };
+    flake-utils = { url = "github:numtide/flake-utils"; };
   };
   outputs = { self, rust-overlay, flake-utils, nixpkgs }: with nixpkgs.lib; rec {
     lib = import ./lib { inherit flake-utils; };
@@ -15,7 +15,7 @@
           inherit system; overlays = [ (import rust-overlay) ];
         };
       llvmPkgs = pkgs.buildPackages.llvmPackages_11;
-      rust = (pkgs.rustChannelOf { date = "2022-02-02"; channel = "nightly"; }).default.override { extensions = [ "rust-src" ]; };
+      rust = (pkgs.rustChannelOf { date = "2022-12-02"; channel = "nightly"; }).default.override { extensions = [ "rust-src" ]; };
       rustPlatform = pkgs.makeRustPlatform { cargo = rust; rustc = rust; };
     in
     {
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -51,6 +51,7 @@
       in
       rec {
         inherit configuredHosts configuredSecrets configuredSystems;
+        configUnchecked = root.config;
         buildSystems = {
           toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [ ]);
           sdImage = builtins.mapAttrs (_name: value: value.config.system.build.sdImage) (configuredSystemsWithExtraModules [
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -9,6 +9,7 @@
           if not matches expectedOwners - then this secret is considered outdated, and
           should be regenerated/reencrypted
         '';
+        default = [ ];
       };
       expectedOwners = mkOption {
         type = listOf str;
@@ -80,7 +81,7 @@
     assertions = mapAttrsToList
       (name: secret: {
         assertion = 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}";
+        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: {