git.delta.rocks / jrsonnet / refs/commits / 718d88b1fcc6

difftreelog

fix post-refactor

Yaroslav Bolyukin2024-01-01parent: #d0d2d55.patch.diff
in: trunk

3 files changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
before · 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 futures::StreamExt;11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15	collections::{BTreeSet, HashSet},16	io::{self, Cursor, Read},17	path::PathBuf,18};19use tabled::{Table, Tabled};20use tokio::fs::read_to_string;21use tracing::{error, info, info_span, warn, Instrument};2223#[derive(Parser)]24pub enum Secret {25	/// Force load host keys for all defined hosts26	ForceKeys,27	/// Add secret, data should be provided in stdin28	AddShared {29		/// Secret name30		name: String,31		/// Secret owners32		machines: Vec<String>,33		/// Override secret if already present34		#[clap(long)]35		force: bool,36		/// Secret public part37		#[clap(long)]38		public: Option<String>,39		/// Load public part from specified file40		#[clap(long)]41		public_file: Option<PathBuf>,4243		/// Create a notification on secret expiration44		#[clap(long)]45		expires_at: Option<DateTime<Utc>>,4647		/// Secret with this name already exists, override its value while keeping the same owners.48		#[clap(long)]49		re_add: bool,50	},51	/// Add secret, data should be provided in stdin52	Add {53		/// Secret name54		name: String,55		/// Secret owners56		machine: String,57		/// Override secret if already present58		#[clap(long)]59		force: bool,60		#[clap(long)]61		public: Option<String>,62		#[clap(long)]63		public_file: Option<PathBuf>,64	},65	/// Read secret from remote host, requires sudo on said host66	Read {67		name: String,68		machine: String,69		#[clap(long)]70		plaintext: bool,71	},72	UpdateShared {73		name: String,7475		#[clap(long)]76		machines: Option<Vec<String>>,7778		#[clap(long)]79		add_machines: Vec<String>,80		#[clap(long)]81		remove_machines: Vec<String>,8283		/// Which host should we use to decrypt84		#[clap(long)]85		prefer_identities: Vec<String>,86	},87	Regenerate {88		/// Which host should we use to decrypt, in case if reencryption is required, without89		/// regeneration90		#[clap(long)]91		prefer_identities: Vec<String>,92	},93	List {},94}9596#[tracing::instrument(skip(config, secret, field, prefer_identities))]97async fn update_owner_set(98	secret_name: &str,99	config: &Config,100	mut secret: FleetSharedSecret,101	field: Field,102	updated_set: &[String],103	prefer_identities: &[String],104) -> Result<FleetSharedSecret> {105	let original_set = secret.owners.clone();106107	let set = original_set.iter().collect::<BTreeSet<_>>();108	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();109110	if set == expected_set {111		info!("no need to update owner list, it is already correct");112		return Ok(secret);113	}114115	let should_regenerate = if set.difference(&expected_set).next().is_some() {116		// TODO: Remove this warning for revokable secrets.117		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");118		nix_go_json!(field.regenerateOnOwnerRemoved)119	} else if expected_set.difference(&set).next().is_some() {120		nix_go_json!(field.regenerateOnOwnerAdded)121	} else {122		false123	};124125	if should_regenerate {126		info!("secret is owner-dependent, will regenerate");127		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;128		Ok(generated)129	} else {130		let identity_holder = if !prefer_identities.is_empty() {131			prefer_identities132				.iter()133				.find(|i| original_set.iter().any(|s| s == *i))134		} else {135			secret.owners.first()136		};137		let Some(identity_holder) = identity_holder else {138			bail!("no available holder found");139		};140141		if let Some(data) = secret.secret.secret {142			let host = config.host(identity_holder).await?;143			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;144			secret.secret.secret = Some(encrypted);145		}146147		secret.owners = updated_set.to_vec();148		Ok(secret)149	}150}151152#[derive(Deserialize)]153#[serde(rename_all = "camelCase")]154enum GeneratorKind {155	Impure,156}157158async fn generate_impure(159	config: &Config,160	_display_name: &str,161	secret: Field,162	default_generator: Field,163	owners: &[String],164) -> Result<FleetSecret> {165	let config_field = &config.config_unchecked_field;166	let generator = nix_go!(secret.generator);167168	let on: String = nix_go_json!(default_generator.impureOn);169	let call_package = nix_go!(170		config_field.buildableSystems(Obj {171			localSystem: { config.local_system.clone() },172		})[{ on }]173		.config174		.nixpkgs175		.resolvedPkgs176		.callPackage177	);178179	let host = config.host(&on).await?;180181	let generator = nix_go!(call_package(generator)(Obj {}));182	let generator = generator.build().await?;183	let generator = generator184		.get("out")185		.ok_or_else(|| anyhow!("missing generateImpure out"))?;186	let generator = host.remote_derivation(generator).await?;187188	let mut recipients = String::new();189	for owner in owners {190		let key = config.key(owner).await?;191		recipients.push_str(&format!("-r \"{key}\" "));192	}193	recipients.push_str("-e");194195	let out = host.mktemp_dir().await?;196197	let mut gen = host.cmd(generator).await?;198	gen.env("rageArgs", recipients).env("out", &out);199	gen.run().await.context("impure generator")?;200201	{202		let marker = host.read_file_text(format!("{out}/marker")).await?;203		ensure!(marker == "SUCCESS", "generation not succeeded");204	}205206	let public = host.read_file_text(format!("{out}/public")).await.ok();207	let secret = host.read_file_bin(format!("{out}/secret")).await.ok();208	if let Some(secret) = &secret {209		ensure!(210			age::Decryptor::new(Cursor::new(&secret)).is_ok(),211			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."212		);213	}214215	let created_at = host.read_file_value(format!("{out}/created_at")).await?;216	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();217218	Ok(FleetSecret {219		created_at,220		expires_at,221		public,222		secret: secret.map(SecretData),223	})224}225async fn generate(226	config: &Config,227	display_name: &str,228	secret: Field,229	owners: &[String],230) -> Result<FleetSecret> {231	let generator = nix_go!(secret.generator);232	// Can't properly check on nix module system level233	{234		let gen_ty = generator.type_of().await?;235		if gen_ty == "null" {236			bail!("secret has no generator defined, can't automatically generate it.");237		}238		if gen_ty != "lambda" {239			bail!("generator should be lambda, got {gen_ty}");240		}241	}242	let default_pkgs = &config.default_pkgs;243	let default_call_package = nix_go!(default_pkgs.callPackage);244	// Generators provide additional information in passthru, to access245	// passthru we should call generator, but information about where this generator is supposed to build246	// is located in passthru... Thus evaluating generator on host.247	//248	// Maybe it is also possible to do some magic with __functor?249	//250	// I don't want to make modules always responsible for additional secret data anyway,251	// so it should be in derivation, and not in the secret data itself.252	let default_generator = nix_go!(default_call_package(generator)(Obj {}));253254	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);255256	match kind {257		GeneratorKind::Impure => {258			generate_impure(config, display_name, secret, default_generator, owners).await259		}260	}261}262async fn generate_shared(263	config: &Config,264	display_name: &str,265	secret: Field,266	expected_owners: Vec<String>,267) -> Result<FleetSharedSecret> {268	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);269	Ok(FleetSharedSecret {270		secret: generate(config, display_name, secret, &expected_owners).await?,271		owners: expected_owners,272	})273}274275async fn parse_public(276	public: Option<String>,277	public_file: Option<PathBuf>,278) -> Result<Option<String>> {279	Ok(match (public, public_file) {280		(Some(v), None) => Some(v),281		(None, Some(v)) => Some(read_to_string(v).await?),282		(Some(_), Some(_)) => {283			bail!("only public or public_file should be set")284		}285		(None, None) => None,286	})287}288289fn parse_machines(290	initial: Vec<String>,291	machines: Option<Vec<String>>,292	mut add_machines: Vec<String>,293	mut remove_machines: Vec<String>,294) -> Result<Vec<String>> {295	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {296		bail!("no operation");297	}298299	let initial_machines = initial.clone();300	let mut target_machines = initial;301	info!("Currently encrypted for {initial_machines:?}");302303	// ensure!(machines.is_some() || !add_machines.is_empty() || )304	if let Some(machines) = machines {305		ensure!(306			add_machines.is_empty() && remove_machines.is_empty(),307			"can't combine --machines and --add-machines/--remove-machines"308		);309		let target = initial_machines.iter().collect::<HashSet<_>>();310		let source = machines.iter().collect::<HashSet<_>>();311		for removed in target.difference(&source) {312			remove_machines.push((*removed).clone());313		}314		for added in source.difference(&target) {315			add_machines.push((*added).clone());316		}317	}318319	for machine in &remove_machines {320		let mut removed = false;321		while let Some(pos) = target_machines.iter().position(|m| m == machine) {322			target_machines.swap_remove(pos);323			removed = true;324		}325		if !removed {326			warn!("secret is not enabled for {machine}");327		}328	}329	for machine in &add_machines {330		if target_machines.iter().any(|m| m == machine) {331			warn!("secret is already added to {machine}");332		} else {333			target_machines.push(machine.to_owned());334		}335	}336	if !remove_machines.is_empty() {337		// TODO: maybe force secret regeneration?338		// Not that useful without revokation.339		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");340	}341	Ok(target_machines)342}343impl Secret {344	pub async fn run(self, config: &Config) -> Result<()> {345		match self {346			Secret::ForceKeys => {347				for host in config.list_hosts().await? {348					if config.should_skip(&host.name) {349						continue;350					}351					config.key(&host.name).await?;352				}353			}354			Secret::AddShared {355				mut machines,356				name,357				force,358				public,359				public_file,360				expires_at,361				re_add,362			} => {363				let exists = config.has_shared(&name);364				if exists && !force && !re_add {365					bail!("secret already defined");366				}367				if re_add {368					// Fixme: use clap to limit this usage369					ensure!(!force, "--force and --readd are not compatible");370					ensure!(exists, "secret doesn't exists");371					ensure!(372						machines.is_empty(),373						"you can't use machines argument for --readd"374					);375					let shared = config.shared_secret(&name)?;376					machines = shared.owners;377				}378379				let recipients = config.recipients(machines.clone()).await?;380381				let secret = {382					let mut input = vec![];383					io::stdin().read_to_end(&mut input)?;384385					if input.is_empty() {386						None387					} else {388						Some(389							SecretData::encrypt(recipients, input)390								.ok_or_else(|| anyhow!("no recipients provided"))?,391						)392					}393				};394				let public = parse_public(public, public_file).await?;395				config.replace_shared(396					name,397					FleetSharedSecret {398						owners: machines,399						secret: FleetSecret {400							created_at: Utc::now(),401							expires_at,402							secret,403							public,404						},405					},406				);407			}408			Secret::Add {409				machine,410				name,411				force,412				public,413				public_file,414			} => {415				let recipient = config.recipient(&machine).await?;416417				let secret = {418					let mut input = vec![];419					io::stdin().read_to_end(&mut input)?;420					if input.is_empty() {421						bail!("no data provided")422					}423424					Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))425				};426427				if config.has_secret(&machine, &name) && !force {428					bail!("secret already defined");429				}430				let public = parse_public(public, public_file).await?;431432				config.insert_secret(433					&machine,434					name,435					FleetSecret {436						created_at: Utc::now(),437						expires_at: None,438						secret,439						public,440					},441				);442			}443			#[allow(clippy::await_holding_refcell_ref)]444			Secret::Read {445				name,446				machine,447				plaintext,448			} => {449				let secret = config.host_secret(&machine, &name)?;450				let Some(secret) = secret.secret else {451					bail!("no secret {name}");452				};453				let host = config.host(&machine).await?;454				let data = host.decrypt(secret).await?;455				if plaintext {456					let s = String::from_utf8(data).context("output is not utf8")?;457					print!("{s}");458				} else {459					println!("{}", z85::encode(&data));460				}461			}462			Secret::UpdateShared {463				name,464				machines,465				add_machines,466				remove_machines,467				prefer_identities,468			} => {469				let secret = config.shared_secret(&name)?;470				if secret.secret.secret.is_none() {471					bail!("no secret");472				}473474				let initial_machines = secret.owners.clone();475				let target_machines = parse_machines(476					initial_machines.clone(),477					machines,478					add_machines,479					remove_machines,480				)?;481482				if target_machines.is_empty() {483					info!("no machines left for secret, removing it");484					config.remove_shared(&name);485					return Ok(());486				}487488				let config_field = &config.config_unchecked_field;489				let config_field = nix_go!(config_field.configUnchecked);490				let field = nix_go!(config_field.sharedSecrets[{ name }]);491492				let updated = update_owner_set(493					&name,494					config,495					secret,496					field,497					&target_machines,498					&prefer_identities,499				)500				.await?;501				config.replace_shared(name, updated);502			}503			Secret::Regenerate { prefer_identities } => {504				info!("checking for secrets to regenerate");505				{506					let _span = info_span!("shared").entered();507					let expected_shared_set = config508						.list_configured_shared()509						.await?510						.into_iter()511						.collect::<HashSet<_>>();512					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();513					for missing in expected_shared_set.difference(&shared_set) {514						let config_field = &config.config_unchecked_field;515						let config_field = nix_go!(config_field.configUnchecked);516						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);517						let expected_owners: Option<Vec<String>> =518							nix_go_json!(secret.expectedOwners);519						let Some(expected_owners) = expected_owners else {520							// TODO: Might still need to regenerate521							continue;522						};523						info!("generating secret: {missing}");524						let shared = generate_shared(config, missing, secret, expected_owners)525							.in_current_span()526							.await?;527						config.replace_shared(missing.to_string(), shared)528					}529				}530				for host in config.list_hosts().await? {531					let _span = info_span!("host", host = host.name).entered();532					let expected_set = host533						.list_configured_secrets()534						.in_current_span()535						.await?536						.into_iter()537						.collect::<HashSet<_>>();538					let stored_set = config539						.list_secrets(&host.name)540						.into_iter()541						.collect::<HashSet<_>>();542					for missing in expected_set.difference(&stored_set) {543						info!("generating secret: {missing}");544						let secret = host.secret_field(missing).in_current_span().await?;545						let generated =546							match generate(config, missing, secret, &[host.name.clone()])547								.in_current_span()548								.await549							{550								Ok(v) => v,551								Err(e) => {552									error!("{e}");553									continue;554								}555							};556						config.insert_secret(&host.name, missing.to_string(), generated)557					}558				}559				let mut to_remove = Vec::new();560				for name in &config.list_shared() {561					info!("updating secret: {name}");562					let data = config.shared_secret(name)?;563					let config_field = &config.config_unchecked_field;564					let config_field = nix_go!(config_field.configUnchecked);565					let expected_owners: Vec<String> =566						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);567					if expected_owners.is_empty() {568						warn!("secret was removed from fleet config: {name}, removing from data");569						to_remove.push(name.to_string());570						continue;571					}572573					let secret = nix_go!(config_field.sharedSecrets[{ name }]);574					config.replace_shared(575						name.to_owned(),576						update_owner_set(577							&name,578							config,579							data,580							secret,581							&expected_owners,582							&prefer_identities,583						)584						.await?,585					);586				}587				for k in to_remove {588					config.remove_shared(&k);589				}590			}591			Secret::List {} => {592				let _span = info_span!("loading secrets").entered();593				let configured = config.list_configured_shared().await?;594				#[derive(Tabled)]595				struct SecretDisplay {596					#[tabled(rename = "Name")]597					name: String,598					#[tabled(rename = "Owners")]599					owners: String,600				}601				let mut table = vec![];602				for name in configured.iter().cloned() {603					let config = config.clone();604					let expected_owners = config.shared_secret_expected_owners(&name).await?;605					let data = config.shared_secret(&name)?;606					let owners = data607						.owners608						.iter()609						.map(|o| {610							if expected_owners.contains(o) {611								o.green().to_string()612							} else {613								o.red().to_string()614							}615						})616						.collect::<Vec<_>>();617					table.push(SecretDisplay {618						owners: owners.join(", "),619						name,620					})621				}622				info!("loaded\n{}", Table::new(table).to_string())623			}624		}625		Ok(())626	}627}
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 futures::StreamExt;11use itertools::Itertools;12use owo_colors::OwoColorize;13use serde::Deserialize;14use std::{15	collections::{BTreeSet, HashSet},16	io::{self, Cursor, Read},17	path::PathBuf,18};19use tabled::{Table, Tabled};20use tokio::fs::read_to_string;21use tracing::{error, info, info_span, warn, Instrument};2223#[derive(Parser)]24pub enum Secret {25	/// Force load host keys for all defined hosts26	ForceKeys,27	/// Add secret, data should be provided in stdin28	AddShared {29		/// Secret name30		name: String,31		/// Secret owners32		machines: Vec<String>,33		/// Override secret if already present34		#[clap(long)]35		force: bool,36		/// Secret public part37		#[clap(long)]38		public: Option<String>,39		/// Load public part from specified file40		#[clap(long)]41		public_file: Option<PathBuf>,4243		/// Create a notification on secret expiration44		#[clap(long)]45		expires_at: Option<DateTime<Utc>>,4647		/// Secret with this name already exists, override its value while keeping the same owners.48		#[clap(long)]49		re_add: bool,50	},51	/// Add secret, data should be provided in stdin52	Add {53		/// Secret name54		name: String,55		/// Secret owners56		machine: String,57		/// Override secret if already present58		#[clap(long)]59		force: bool,60		#[clap(long)]61		public: Option<String>,62		#[clap(long)]63		public_file: Option<PathBuf>,64	},65	/// Read secret from remote host, requires sudo on said host66	Read {67		name: String,68		machine: String,69		#[clap(long)]70		plaintext: bool,71	},72	UpdateShared {73		name: String,7475		#[clap(long)]76		machines: Option<Vec<String>>,7778		#[clap(long)]79		add_machines: Vec<String>,80		#[clap(long)]81		remove_machines: Vec<String>,8283		/// Which host should we use to decrypt84		#[clap(long)]85		prefer_identities: Vec<String>,86	},87	Regenerate {88		/// Which host should we use to decrypt, in case if reencryption is required, without89		/// regeneration90		#[clap(long)]91		prefer_identities: Vec<String>,92	},93	List {},94}9596#[tracing::instrument(skip(config, secret, field, prefer_identities))]97async fn update_owner_set(98	secret_name: &str,99	config: &Config,100	mut secret: FleetSharedSecret,101	field: Field,102	updated_set: &[String],103	prefer_identities: &[String],104) -> Result<FleetSharedSecret> {105	let original_set = secret.owners.clone();106107	let set = original_set.iter().collect::<BTreeSet<_>>();108	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();109110	if set == expected_set {111		info!("no need to update owner list, it is already correct");112		return Ok(secret);113	}114115	let should_regenerate = if set.difference(&expected_set).next().is_some() {116		// TODO: Remove this warning for revokable secrets.117		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");118		nix_go_json!(field.regenerateOnOwnerRemoved)119	} else if expected_set.difference(&set).next().is_some() {120		nix_go_json!(field.regenerateOnOwnerAdded)121	} else {122		false123	};124125	if should_regenerate {126		info!("secret is owner-dependent, will regenerate");127		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;128		Ok(generated)129	} else {130		let identity_holder = if !prefer_identities.is_empty() {131			prefer_identities132				.iter()133				.find(|i| original_set.iter().any(|s| s == *i))134		} else {135			secret.owners.first()136		};137		let Some(identity_holder) = identity_holder else {138			bail!("no available holder found");139		};140141		if let Some(data) = secret.secret.secret {142			let host = config.host(identity_holder).await?;143			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;144			secret.secret.secret = Some(encrypted);145		}146147		secret.owners = updated_set.to_vec();148		Ok(secret)149	}150}151152#[derive(Deserialize)]153#[serde(rename_all = "camelCase")]154enum GeneratorKind {155	Impure,156}157158async fn generate_impure(159	config: &Config,160	_display_name: &str,161	secret: Field,162	default_generator: Field,163	owners: &[String],164) -> Result<FleetSecret> {165	let config_field = &config.config_unchecked_field;166	let generator = nix_go!(secret.generator);167168	let on: String = nix_go_json!(default_generator.impureOn);169	let call_package = nix_go!(170		config_field.hosts[{ on }]171			.nixosSystem172			.config173			.nixpkgs174			.resolvedPkgs175			.callPackage176	);177178	let host = config.host(&on).await?;179180	let generator = nix_go!(call_package(generator)(Obj {}));181	let generator = generator.build().await?;182	let generator = generator183		.get("out")184		.ok_or_else(|| anyhow!("missing generateImpure out"))?;185	let generator = host.remote_derivation(generator).await?;186187	let mut recipients = String::new();188	for owner in owners {189		let key = config.key(owner).await?;190		recipients.push_str(&format!("-r \"{key}\" "));191	}192	recipients.push_str("-e");193194	let out = host.mktemp_dir().await?;195196	let mut gen = host.cmd(generator).await?;197	gen.env("rageArgs", recipients).env("out", &out);198	gen.run().await.context("impure generator")?;199200	{201		let marker = host.read_file_text(format!("{out}/marker")).await?;202		ensure!(marker == "SUCCESS", "generation not succeeded");203	}204205	let public = host.read_file_text(format!("{out}/public")).await.ok();206	let secret = host.read_file_bin(format!("{out}/secret")).await.ok();207	if let Some(secret) = &secret {208		ensure!(209			age::Decryptor::new(Cursor::new(&secret)).is_ok(),210			"builder produced non-encrypted value as secret, this is highly insecure, and not allowed."211		);212	}213214	let created_at = host.read_file_value(format!("{out}/created_at")).await?;215	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();216217	Ok(FleetSecret {218		created_at,219		expires_at,220		public,221		secret: secret.map(SecretData),222	})223}224async fn generate(225	config: &Config,226	display_name: &str,227	secret: Field,228	owners: &[String],229) -> Result<FleetSecret> {230	let generator = nix_go!(secret.generator);231	// Can't properly check on nix module system level232	{233		let gen_ty = generator.type_of().await?;234		if gen_ty == "null" {235			bail!("secret has no generator defined, can't automatically generate it.");236		}237		if gen_ty != "lambda" {238			bail!("generator should be lambda, got {gen_ty}");239		}240	}241	let default_pkgs = &config.default_pkgs;242	let default_call_package = nix_go!(default_pkgs.callPackage);243	// Generators provide additional information in passthru, to access244	// passthru we should call generator, but information about where this generator is supposed to build245	// is located in passthru... Thus evaluating generator on host.246	//247	// Maybe it is also possible to do some magic with __functor?248	//249	// I don't want to make modules always responsible for additional secret data anyway,250	// so it should be in derivation, and not in the secret data itself.251	let default_generator = nix_go!(default_call_package(generator)(Obj {}));252253	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);254255	match kind {256		GeneratorKind::Impure => {257			generate_impure(config, display_name, secret, default_generator, owners).await258		}259	}260}261async fn generate_shared(262	config: &Config,263	display_name: &str,264	secret: Field,265	expected_owners: Vec<String>,266) -> Result<FleetSharedSecret> {267	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);268	Ok(FleetSharedSecret {269		secret: generate(config, display_name, secret, &expected_owners).await?,270		owners: expected_owners,271	})272}273274async fn parse_public(275	public: Option<String>,276	public_file: Option<PathBuf>,277) -> Result<Option<String>> {278	Ok(match (public, public_file) {279		(Some(v), None) => Some(v),280		(None, Some(v)) => Some(read_to_string(v).await?),281		(Some(_), Some(_)) => {282			bail!("only public or public_file should be set")283		}284		(None, None) => None,285	})286}287288fn parse_machines(289	initial: Vec<String>,290	machines: Option<Vec<String>>,291	mut add_machines: Vec<String>,292	mut remove_machines: Vec<String>,293) -> Result<Vec<String>> {294	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {295		bail!("no operation");296	}297298	let initial_machines = initial.clone();299	let mut target_machines = initial;300	info!("Currently encrypted for {initial_machines:?}");301302	// ensure!(machines.is_some() || !add_machines.is_empty() || )303	if let Some(machines) = machines {304		ensure!(305			add_machines.is_empty() && remove_machines.is_empty(),306			"can't combine --machines and --add-machines/--remove-machines"307		);308		let target = initial_machines.iter().collect::<HashSet<_>>();309		let source = machines.iter().collect::<HashSet<_>>();310		for removed in target.difference(&source) {311			remove_machines.push((*removed).clone());312		}313		for added in source.difference(&target) {314			add_machines.push((*added).clone());315		}316	}317318	for machine in &remove_machines {319		let mut removed = false;320		while let Some(pos) = target_machines.iter().position(|m| m == machine) {321			target_machines.swap_remove(pos);322			removed = true;323		}324		if !removed {325			warn!("secret is not enabled for {machine}");326		}327	}328	for machine in &add_machines {329		if target_machines.iter().any(|m| m == machine) {330			warn!("secret is already added to {machine}");331		} else {332			target_machines.push(machine.to_owned());333		}334	}335	if !remove_machines.is_empty() {336		// TODO: maybe force secret regeneration?337		// Not that useful without revokation.338		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");339	}340	Ok(target_machines)341}342impl Secret {343	pub async fn run(self, config: &Config) -> Result<()> {344		match self {345			Secret::ForceKeys => {346				for host in config.list_hosts().await? {347					if config.should_skip(&host.name) {348						continue;349					}350					config.key(&host.name).await?;351				}352			}353			Secret::AddShared {354				mut machines,355				name,356				force,357				public,358				public_file,359				expires_at,360				re_add,361			} => {362				let exists = config.has_shared(&name);363				if exists && !force && !re_add {364					bail!("secret already defined");365				}366				if re_add {367					// Fixme: use clap to limit this usage368					ensure!(!force, "--force and --readd are not compatible");369					ensure!(exists, "secret doesn't exists");370					ensure!(371						machines.is_empty(),372						"you can't use machines argument for --readd"373					);374					let shared = config.shared_secret(&name)?;375					machines = shared.owners;376				}377378				let recipients = config.recipients(machines.clone()).await?;379380				let secret = {381					let mut input = vec![];382					io::stdin().read_to_end(&mut input)?;383384					if input.is_empty() {385						None386					} else {387						Some(388							SecretData::encrypt(recipients, input)389								.ok_or_else(|| anyhow!("no recipients provided"))?,390						)391					}392				};393				let public = parse_public(public, public_file).await?;394				config.replace_shared(395					name,396					FleetSharedSecret {397						owners: machines,398						secret: FleetSecret {399							created_at: Utc::now(),400							expires_at,401							secret,402							public,403						},404					},405				);406			}407			Secret::Add {408				machine,409				name,410				force,411				public,412				public_file,413			} => {414				let recipient = config.recipient(&machine).await?;415416				let secret = {417					let mut input = vec![];418					io::stdin().read_to_end(&mut input)?;419					if input.is_empty() {420						bail!("no data provided")421					}422423					Some(SecretData::encrypt(vec![recipient], input).expect("recipient provided"))424				};425426				if config.has_secret(&machine, &name) && !force {427					bail!("secret already defined");428				}429				let public = parse_public(public, public_file).await?;430431				config.insert_secret(432					&machine,433					name,434					FleetSecret {435						created_at: Utc::now(),436						expires_at: None,437						secret,438						public,439					},440				);441			}442			#[allow(clippy::await_holding_refcell_ref)]443			Secret::Read {444				name,445				machine,446				plaintext,447			} => {448				let secret = config.host_secret(&machine, &name)?;449				let Some(secret) = secret.secret else {450					bail!("no secret {name}");451				};452				let host = config.host(&machine).await?;453				let data = host.decrypt(secret).await?;454				if plaintext {455					let s = String::from_utf8(data).context("output is not utf8")?;456					print!("{s}");457				} else {458					println!("{}", z85::encode(&data));459				}460			}461			Secret::UpdateShared {462				name,463				machines,464				add_machines,465				remove_machines,466				prefer_identities,467			} => {468				let secret = config.shared_secret(&name)?;469				if secret.secret.secret.is_none() {470					bail!("no secret");471				}472473				let initial_machines = secret.owners.clone();474				let target_machines = parse_machines(475					initial_machines.clone(),476					machines,477					add_machines,478					remove_machines,479				)?;480481				if target_machines.is_empty() {482					info!("no machines left for secret, removing it");483					config.remove_shared(&name);484					return Ok(());485				}486487				let config_field = &config.config_unchecked_field;488				let field = nix_go!(config_field.sharedSecrets[{ name }]);489490				let updated = update_owner_set(491					&name,492					config,493					secret,494					field,495					&target_machines,496					&prefer_identities,497				)498				.await?;499				config.replace_shared(name, updated);500			}501			Secret::Regenerate { prefer_identities } => {502				info!("checking for secrets to regenerate");503				{504					let _span = info_span!("shared").entered();505					let expected_shared_set = config506						.list_configured_shared()507						.await?508						.into_iter()509						.collect::<HashSet<_>>();510					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();511					for missing in expected_shared_set.difference(&shared_set) {512						let config_field = &config.config_unchecked_field;513						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);514						let expected_owners: Option<Vec<String>> =515							nix_go_json!(secret.expectedOwners);516						let Some(expected_owners) = expected_owners else {517							// TODO: Might still need to regenerate518							continue;519						};520						info!("generating secret: {missing}");521						let shared = generate_shared(config, missing, secret, expected_owners)522							.in_current_span()523							.await?;524						config.replace_shared(missing.to_string(), shared)525					}526				}527				for host in config.list_hosts().await? {528					let _span = info_span!("host", host = host.name).entered();529					let expected_set = host530						.list_configured_secrets()531						.in_current_span()532						.await?533						.into_iter()534						.collect::<HashSet<_>>();535					let stored_set = config536						.list_secrets(&host.name)537						.into_iter()538						.collect::<HashSet<_>>();539					for missing in expected_set.difference(&stored_set) {540						info!("generating secret: {missing}");541						let secret = host.secret_field(missing).in_current_span().await?;542						let generated =543							match generate(config, missing, secret, &[host.name.clone()])544								.in_current_span()545								.await546							{547								Ok(v) => v,548								Err(e) => {549									error!("{e}");550									continue;551								}552							};553						config.insert_secret(&host.name, missing.to_string(), generated)554					}555				}556				let mut to_remove = Vec::new();557				for name in &config.list_shared() {558					info!("updating secret: {name}");559					let data = config.shared_secret(name)?;560					let config_field = &config.config_unchecked_field;561					let expected_owners: Vec<String> =562						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);563					if expected_owners.is_empty() {564						warn!("secret was removed from fleet config: {name}, removing from data");565						to_remove.push(name.to_string());566						continue;567					}568569					let secret = nix_go!(config_field.sharedSecrets[{ name }]);570					config.replace_shared(571						name.to_owned(),572						update_owner_set(573							&name,574							config,575							data,576							secret,577							&expected_owners,578							&prefer_identities,579						)580						.await?,581					);582				}583				for k in to_remove {584					config.remove_shared(&k);585				}586			}587			Secret::List {} => {588				let _span = info_span!("loading secrets").entered();589				let configured = config.list_configured_shared().await?;590				#[derive(Tabled)]591				struct SecretDisplay {592					#[tabled(rename = "Name")]593					name: String,594					#[tabled(rename = "Owners")]595					owners: String,596				}597				let mut table = vec![];598				for name in configured.iter().cloned() {599					let config = config.clone();600					let expected_owners = config.shared_secret_expected_owners(&name).await?;601					let data = config.shared_secret(&name)?;602					let owners = data603						.owners604						.iter()605						.map(|o| {606							if expected_owners.contains(o) {607								o.green().to_string()608							} else {609								o.red().to_string()610							}611						})612						.collect::<Vec<_>>();613					table.push(SecretDisplay {614						owners: owners.join(", "),615						name,616					})617				}618				info!("loaded\n{}", Table::new(table).to_string())619			}620		}621		Ok(())622	}623}
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -204,7 +204,7 @@
 
 	pub async fn host(&self, name: &str) -> Result<ConfigHost> {
 		let config = &self.config_unchecked_field;
-		let nixos_config = nix_go!(config.configuredSystems[{ name }].config);
+		let nixos_config = nix_go!(config.hosts[{ name }].nixosSystem.config);
 		Ok(ConfigHost {
 			config: self.clone(),
 			name: name.to_owned(),
@@ -236,9 +236,7 @@
 	/// Shared secrets configured in fleet.nix or in flake
 	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
 		let config_field = &self.config_unchecked_field;
-		nix_go!(config_field.configUnchecked.sharedSecrets)
-			.list_fields()
-			.await
+		nix_go!(config_field.sharedSecrets).list_fields().await
 	}
 	/// Shared secrets configured in fleet.nix
 	pub fn list_shared(&self) -> Vec<String> {
@@ -299,7 +297,7 @@
 	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {
 		let config_field = &self.config_unchecked_field;
 		Ok(nix_go_json!(
-			config_field.configUnchecked.sharedSecrets[{ secret }].expectedOwners
+			config_field.sharedSecrets[{ secret }].expectedOwners
 		))
 	}
 
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -22,7 +22,7 @@
         ++ [
           data
           ({...}: {
-            inherit globalModules hosts;
+            inherit globalModules hosts overlays;
           })
         ]
         ++ modules;
@@ -39,7 +39,6 @@
       root,
       data,
     }: {
-      configuredHosts = root.config.hosts;
       config = root.config;
     };
     defaultData = withData {
@@ -49,9 +48,9 @@
     uncheckedData = withData {inherit data root;};
   in {
     inherit nixpkgs overlays;
-    inherit (defaultData) configuredHosts configuredSystems config buildableSystems;
+    inherit (defaultData) config;
     unchecked = {
-      inherit (uncheckedData) configuredHosts configuredSystems config buildableSystems;
+      inherit (uncheckedData) config;
     };
   };
 }