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

difftreelog

refactor temporarly break cross, but greatly cleanup data

Yaroslav Bolyukin2023-12-31parent: #7c6930a.patch.diff
in: trunk

16 files changed

modifiedCargo.lockdiffbeforeafterboth
before · Cargo.lock
320 packageslockfile v3
after · Cargo.lock
329 packageslockfile v3
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "fleet"
 description = "NixOS configuration management"
-version = "0.1.0"
+version = "0.2.0"
 authors = ["Yaroslav Bolyukin <iam@lach.pw>"]
 edition = "2021"
 
modifiedcmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth
--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -9,7 +9,7 @@
 use std::sync::{Arc, OnceLock};
 
 use anyhow::{anyhow, bail, ensure, Context, Result};
-use better_command::{ClonableHandler, NixHandler, Handler, NoopHandler};
+use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
 use futures::StreamExt;
 use itertools::Itertools;
 use r2d2::{Pool, PooledConnection};
@@ -299,8 +299,11 @@
 		let mut fexpr = b"builtins.toJSON (".to_vec();
 		fexpr.extend_from_slice(expr.as_ref());
 		fexpr.push(b')');
-		let v = self.execute_expression_string(fexpr).await?;
-		Ok(serde_json::from_str(&v)?)
+		let v = self
+			.execute_expression_string(fexpr)
+			.await
+			.context("string expression")?;
+		serde_json::from_str(&v).context("json parse")
 	}
 	async fn execute_expression_wrapping(
 		&mut self,
@@ -450,15 +453,26 @@
 
 #[macro_export]
 macro_rules! nix_expr_inner {
-	(Obj { $($ident:ident: $($val:tt)+),* $(,)? }) => {{
-		use $crate::better_nix_eval::NixExprBuilder;
+	//(@munch_object FIXME: value should be arbitrary nix_expr_inner input... Time to write proc-macro?
+	(@obj($o:ident) $field:ident, $($tt:tt)*) => {{
+		$o.obj_key(
+			NixExprBuilder::string(stringify!($field)),
+			NixExprBuilder::field($field),
+		);
+		nix_expr_inner!(@obj($o) $($tt)*);
+	}};
+	(@obj($o:ident) $field:ident: $v:block, $($tt:tt)*) => {{
+		$o.obj_key(
+			NixExprBuilder::string(stringify!($field)),
+			NixExprBuilder::serialized(&$v),
+		);
+		nix_expr_inner!(@obj($o) $($tt)*);
+	}};
+	(@obj($o:ident)) => {{}};
+	(Obj { $($tt:tt)* }) => {{
+		use $crate::{better_nix_eval::NixExprBuilder, nix_expr_inner};
 		let mut out = NixExprBuilder::object();
-		$(
-			out.obj_key(
-				NixExprBuilder::string(stringify!($ident)),
-				$crate::nix_expr_inner!($($val)+),
-			);
-		)*
+		nix_expr_inner!(@obj(out) $($tt)*);
 		out.end_obj();
 		out
 	}};
@@ -522,6 +536,9 @@
 		$o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
 		nix_go!(@o($o) $($tt)*);
 	};
+	(@o($o:ident) | $($var:tt)*) => {
+		$o.push(Index::Pipe($crate::nix_expr_inner!($($var)+)));
+	};
 	(@o($o:ident)) => {};
 	($field:ident $($tt:tt)+) => {{
 		use $crate::{nix_go, better_nix_eval::Index};
@@ -545,6 +562,7 @@
 	Apply(String),
 	Expr(NixExprBuilder),
 	ExprApply(NixExprBuilder),
+	Pipe(NixExprBuilder),
 }
 impl Index {
 	pub fn var(v: impl AsRef<str>) -> Self {
@@ -582,6 +600,9 @@
 			Index::ExprApply(e) => {
 				write!(f, "<apply>({})", e.out)
 			}
+			Index::Pipe(e) => {
+				write!(f, "<map>({})", e.out)
+			}
 		}
 	}
 }
@@ -604,9 +625,9 @@
 	session: NixSession,
 	value: Option<u32>,
 }
-fn context(full_path: Option<&[Index]>, query: &str) -> String {
+fn context(op: &str, full_path: Option<&[Index]>, query: &str) -> String {
 	if let Some(full_path) = &full_path {
-		format!("full path: {}", PathDisplay(full_path))
+		format!("on {op}, full path: {}", PathDisplay(full_path))
 	} else {
 		format!("query: {query:?}")
 	}
@@ -628,7 +649,7 @@
 			.await
 			.execute_assign(query)
 			.await
-			.with_context(|| context(None, query))?;
+			.with_context(|| context("new root", None, query))?;
 		Ok(Self(Arc::new(FieldInner {
 			full_path: None,
 			session,
@@ -686,6 +707,12 @@
 					query.push_str(&index);
 					query = format!("({query})");
 				}
+				Index::Pipe(v) => {
+					let index = Field::new(self.0.session.clone(), &v.out).await?;
+					used_fields.push(index.clone());
+					let index = format!("sess_field_{}", index.0.value.expect("value"));
+					query = format!("({index} {query})");
+				}
 			}
 		}
 
@@ -720,7 +747,7 @@
 			.await
 			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| context(self.0.full_path.as_deref(), &query))
+			.with_context(|| context("as_json", self.0.full_path.as_deref(), &query))
 	}
 	pub async fn has_field(&self, name: &str) -> Result<bool> {
 		let id = self.0.value.expect("can't list root fields");
@@ -733,7 +760,7 @@
 			.await
 			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| context(self.0.full_path.as_deref(), &query))
+			.with_context(|| context("has_field", self.0.full_path.as_deref(), &query))
 	}
 	pub async fn list_fields(&self) -> Result<Vec<String>> {
 		let id = self.0.value.expect("can't list root fields");
@@ -745,7 +772,7 @@
 			.await
 			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| context(self.0.full_path.as_deref(), &query))
+			.with_context(|| context("list field", self.0.full_path.as_deref(), &query))
 	}
 	pub async fn type_of(&self) -> Result<String> {
 		let id = self.0.value.expect("can't list root fields");
@@ -757,7 +784,11 @@
 			.await
 			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| context(self.0.full_path.as_deref(), &query))
+			.with_context(|| context("type_of", self.0.full_path.as_deref(), &query))
+	}
+	pub async fn import(&self) -> Result<Self> {
+		let import = Self::new(self.0.session.clone(), "import").await?;
+		Ok(nix_go!(self | import))
 	}
 	pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
 		let id = self.0.value.expect("can't use build on not-value");
@@ -773,7 +804,7 @@
 		ensure!(
 			!vid.is_empty(),
 			"build failed: {}",
-			context(self.0.full_path.as_deref(), &query),
+			context("build", self.0.full_path.as_deref(), &query),
 		);
 		let Some(vid) = vid.strip_prefix("This derivation produced the following outputs:\n")
 		else {
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -53,7 +53,7 @@
 	fn build_attr(&self) -> String {
 		match self {
 			PackageAction::SdImage => "sdImage".to_owned(),
-			PackageAction::InstallationCd => "installationCd".to_owned(),
+			PackageAction::InstallationCd => "isoImage".to_owned(),
 		}
 	}
 }
@@ -178,7 +178,7 @@
 	if !build.disable_rollback {
 		let _span = info_span!("preparing").entered();
 		info!("preparing for rollback");
-		let generation = get_current_generation(&host).await?;
+		let generation = get_current_generation(host).await?;
 		info!(
 			"rollback target would be {} {}",
 			generation.id, generation.datetime
@@ -234,7 +234,7 @@
 		let mut switch_script = built.clone();
 		switch_script.push("bin");
 		switch_script.push("switch-to-configuration");
-		let mut cmd = host.cmd(switch_script).await?;
+		let mut cmd = host.cmd(switch_script).in_current_span().await?;
 		cmd.arg(action.name());
 		if let Err(e) = cmd.sudo().run().in_current_span().await {
 			error!("failed to activate: {e}");
@@ -285,11 +285,9 @@
 		info!("building");
 		let host = config.host(&host).await?;
 		let action = Action::from(self.subcommand.clone());
-		let fleet_field = &config.fleet_field;
+		let fleet_config = &config.config_field;
 		let drv = nix_go!(
-			fleet_field.buildSystems(Obj {
-				localSystem: { config.local_system.clone() }
-			})[{ action.build_attr() }][{ &host.name }]
+			fleet_config.hosts[{ &host.name }].nixosSystem.config.system.build[{ action.build_attr() }]
 		);
 		let outputs = drv.build().await.map_err(|e| {
 			if action.build_attr() == "sdImage" {
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -37,9 +37,9 @@
 			InfoCmd::ListHosts { ref tagged } => {
 				'host: for host in config.list_hosts().await? {
 					if !tagged.is_empty() {
-						let fleet_field = &config.fleet_field;
+						let config = &config.config_unchecked_field;
 						let tags: Vec<String> =
-							nix_go_json!(fleet_field.configuredSystems[{ host.name }].config.tags);
+							nix_go_json!(config.hosts[{ host.name }].nixosSystem.config.tags);
 						for tag in tagged {
 							if !tags.contains(tag) {
 								continue 'host;
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -10,14 +10,15 @@
 use futures::StreamExt;
 use itertools::Itertools;
 use owo_colors::OwoColorize;
+use serde::Deserialize;
 use std::{
-	collections::HashSet,
+	collections::{BTreeSet, HashSet},
 	io::{self, Cursor, Read},
 	path::PathBuf,
 };
 use tabled::{Table, Tabled};
 use tokio::fs::read_to_string;
-use tracing::{info, info_span, warn};
+use tracing::{error, info, info_span, warn, Instrument};
 
 #[derive(Parser)]
 pub enum Secret {
@@ -92,77 +93,182 @@
 	List {},
 }
 
-async fn generate_shared(
+#[tracing::instrument(skip(config, secret, field, prefer_identities))]
+async fn update_owner_set(
+	secret_name: &str,
 	config: &Config,
-	display_name: &str,
-	secret: Field,
+	mut secret: FleetSharedSecret,
+	field: Field,
+	updated_set: &[String],
+	prefer_identities: &[String],
 ) -> Result<FleetSharedSecret> {
-	Ok(if secret.has_field("generateImpure").await? {
-		let config_field = &config.config_unchecked_field;
-		let generate = nix_go!(secret.generateImpure);
-		let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
+	let original_set = secret.owners.clone();
 
-		let on: String = nix_go_json!(generate.on);
-		let call_package = nix_go!(
-			config_field.buildableSystems(Obj {
-				localSystem: { config.local_system.clone() }
-			})[{ on }]
-			.config
-			.nixpkgs
-			.resolvedPkgs
-			.callPackage
-		);
+	let set = original_set.iter().collect::<BTreeSet<_>>();
+	let expected_set = updated_set.iter().collect::<BTreeSet<_>>();
 
-		let host = config.host(&on).await?;
+	if set == expected_set {
+		info!("no need to update owner list, it is already correct");
+		return Ok(secret);
+	}
 
-		let generator = nix_go!(call_package(generate.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 should_regenerate = if set.difference(&expected_set).next().is_some() {
+		// TODO: Remove this warning for revokable secrets.
+		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");
+		nix_go_json!(field.regenerateOnOwnerRemoved)
+	} else if expected_set.difference(&set).next().is_some() {
+		nix_go_json!(field.regenerateOnOwnerAdded)
+	} else {
+		false
+	};
+
+	if should_regenerate {
+		info!("secret is owner-dependent, will regenerate");
+		let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;
+		Ok(generated)
+	} else {
+		let identity_holder = if !prefer_identities.is_empty() {
+			prefer_identities
+				.iter()
+				.find(|i| original_set.iter().any(|s| s == *i))
+		} else {
+			secret.owners.first()
+		};
+		let Some(identity_holder) = identity_holder else {
+			bail!("no available holder found");
+		};
 
-		let mut recipients = String::new();
-		for owner in &owners {
-			let key = config.key(owner).await?;
-			recipients.push_str(&format!("-r \"{key}\" "));
+		if let Some(data) = secret.secret.secret {
+			let host = config.host(identity_holder).await?;
+			let encrypted = host.reencrypt(data, updated_set.to_vec()).await?;
+			secret.secret.secret = Some(encrypted);
 		}
-		recipients.push_str("-e");
 
-		let out = host.mktemp_dir().await?;
+		secret.owners = updated_set.to_vec();
+		Ok(secret)
+	}
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum GeneratorKind {
+	Impure,
+}
+
+async fn generate_impure(
+	config: &Config,
+	_display_name: &str,
+	secret: Field,
+	default_generator: Field,
+	owners: &[String],
+) -> Result<FleetSecret> {
+	let config_field = &config.config_unchecked_field;
+	let generator = nix_go!(secret.generator);
+
+	let on: String = nix_go_json!(default_generator.impureOn);
+	let call_package = nix_go!(
+		config_field.buildableSystems(Obj {
+			localSystem: { config.local_system.clone() },
+		})[{ on }]
+		.config
+		.nixpkgs
+		.resolvedPkgs
+		.callPackage
+	);
+
+	let host = config.host(&on).await?;
+
+	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 mut gen = host.cmd(generator).await?;
-		gen.env("rageArgs", recipients).env("out", &out);
-		gen.run().await?;
+	let out = host.mktemp_dir().await?;
 
-		{
-			let marker = host.read_file_text(format!("{out}/marker")).await?;
-			ensure!(marker == "SUCCESS", "generation not succeeded");
-		}
+	let mut gen = host.cmd(generator).await?;
+	gen.env("rageArgs", recipients).env("out", &out);
+	gen.run().await.context("impure generator")?;
 
-		let public = host.read_file_text(format!("{out}/public")).await.ok();
-		let secret = host.read_file_bin(format!("{out}/secret")).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"
-			);
+	{
+		let marker = host.read_file_text(format!("{out}/marker")).await?;
+		ensure!(marker == "SUCCESS", "generation not succeeded");
+	}
+
+	let public = host.read_file_text(format!("{out}/public")).await.ok();
+	let secret = host.read_file_bin(format!("{out}/secret")).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 created_at = host.read_file_value(format!("{out}/created_at")).await?;
+	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();
+
+	Ok(FleetSecret {
+		created_at,
+		expires_at,
+		public,
+		secret: secret.map(SecretData),
+	})
+}
+async fn generate(
+	config: &Config,
+	display_name: &str,
+	secret: Field,
+	owners: &[String],
+) -> Result<FleetSecret> {
+	let generator = nix_go!(secret.generator);
+	// Can't properly check on nix module system level
+	{
+		let gen_ty = generator.type_of().await?;
+		if gen_ty == "null" {
+			bail!("secret has no generator defined, can't automatically generate it.");
 		}
+		if gen_ty != "lambda" {
+			bail!("generator should be lambda, got {gen_ty}");
+		}
+	}
+	let default_pkgs = &config.default_pkgs;
+	let default_call_package = nix_go!(default_pkgs.callPackage);
+	// Generators provide additional information in passthru, to access
+	// passthru we should call generator, but information about where this generator is supposed to build
+	// is located in passthru... Thus evaluating generator on host.
+	//
+	// Maybe it is also possible to do some magic with __functor?
+	//
+	// I don't want to make modules always responsible for additional secret data anyway,
+	// so it should be in derivation, and not in the secret data itself.
+	let default_generator = nix_go!(default_call_package(generator)(Obj {}));
 
-		let created_at = host.read_file_value(format!("{out}/created_at")).await?;
-		let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();
+	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);
 
-		FleetSharedSecret {
-			owners,
-			secret: FleetSecret {
-				created_at,
-				expires_at,
-				public,
-				secret: secret.map(SecretData),
-			},
+	match kind {
+		GeneratorKind::Impure => {
+			generate_impure(config, display_name, secret, default_generator, owners).await
 		}
-	} else {
-		bail!("no generator defined for {display_name}")
+	}
+}
+async fn generate_shared(
+	config: &Config,
+	display_name: &str,
+	secret: Field,
+	expected_owners: Vec<String>,
+) -> Result<FleetSharedSecret> {
+	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
+	Ok(FleetSharedSecret {
+		secret: generate(config, display_name, secret, &expected_owners).await?,
+		owners: expected_owners,
 	})
 }
 
@@ -270,9 +376,7 @@
 					machines = shared.owners;
 				}
 
-				let recipients = config
-					.recipients(&machines.iter().map(String::as_str).collect_vec())
-					.await?;
+				let recipients = config.recipients(machines.clone()).await?;
 
 				let secret = {
 					let mut input = vec![];
@@ -362,7 +466,7 @@
 				remove_machines,
 				prefer_identities,
 			} => {
-				let mut secret = config.shared_secret(&name)?;
+				let secret = config.shared_secret(&name)?;
 				if secret.secret.secret.is_none() {
 					bail!("no secret");
 				}
@@ -378,61 +482,84 @@
 				if target_machines.is_empty() {
 					info!("no machines left for secret, removing it");
 					config.remove_shared(&name);
-					return Ok(());
-				}
-
-				if target_machines == initial_machines {
-					warn!("secret owners are already correct");
 					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)
-					.then(|m| async { config.key(m).await })
-					.collect::<Vec<_>>()
-					.await;
-				let target_recipients =
-					target_recipients.into_iter().collect::<Result<Vec<_>>>()?;
+				let config_field = &config.config_unchecked_field;
+				let config_field = nix_go!(config_field.configUnchecked);
+				let field = nix_go!(config_field.sharedSecrets[{ name }]);
 
-				if let Some(data) = secret.secret.secret {
-					let host = config.host(&identity_holder).await?;
-					let encrypted = host.reencrypt(data, target_recipients).await?;
-					secret.secret.secret = Some(encrypted);
-				}
-
-				secret.owners = target_machines;
-				config.replace_shared(name, secret);
+				let updated = update_owner_set(
+					&name,
+					config,
+					secret,
+					field,
+					&target_machines,
+					&prefer_identities,
+				)
+				.await?;
+				config.replace_shared(name, updated);
 			}
 			Secret::Regenerate { prefer_identities } => {
+				info!("checking for secrets to regenerate");
 				{
+					let _span = info_span!("shared").entered();
 					let expected_shared_set = config
 						.list_configured_shared()
 						.await?
 						.into_iter()
 						.collect::<HashSet<_>>();
 					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();
-					for removed in expected_shared_set.difference(&shared_set) {
-						info!("generating secret: {removed}");
+					for missing in expected_shared_set.difference(&shared_set) {
 						let config_field = &config.config_unchecked_field;
 						let config_field = nix_go!(config_field.configUnchecked);
-						let secret = nix_go!(config_field.sharedSecrets[{ removed }]);
-						let shared = generate_shared(config, removed, secret).await?;
-						config.replace_shared(removed.to_string(), shared)
+						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);
+						let expected_owners: Option<Vec<String>> =
+							nix_go_json!(secret.expectedOwners);
+						let Some(expected_owners) = expected_owners else {
+							// TODO: Might still need to regenerate
+							continue;
+						};
+						info!("generating secret: {missing}");
+						let shared = generate_shared(config, missing, secret, expected_owners)
+							.in_current_span()
+							.await?;
+						config.replace_shared(missing.to_string(), shared)
+					}
+				}
+				for host in config.list_hosts().await? {
+					let _span = info_span!("host", host = host.name).entered();
+					let expected_set = host
+						.list_configured_secrets()
+						.in_current_span()
+						.await?
+						.into_iter()
+						.collect::<HashSet<_>>();
+					let stored_set = config
+						.list_secrets(&host.name)
+						.into_iter()
+						.collect::<HashSet<_>>();
+					for missing in expected_set.difference(&stored_set) {
+						info!("generating secret: {missing}");
+						let secret = host.secret_field(missing).in_current_span().await?;
+						let generated =
+							match generate(config, missing, secret, &[host.name.clone()])
+								.in_current_span()
+								.await
+							{
+								Ok(v) => v,
+								Err(e) => {
+									error!("{e}");
+									continue;
+								}
+							};
+						config.insert_secret(&host.name, missing.to_string(), generated)
 					}
 				}
 				let mut to_remove = Vec::new();
 				for name in &config.list_shared() {
 					info!("updating secret: {name}");
-					let mut data = config.shared_secret(name)?;
+					let data = config.shared_secret(name)?;
 					let config_field = &config.config_unchecked_field;
 					let config_field = nix_go!(config_field.configUnchecked);
 					let expected_owners: Vec<String> =
@@ -442,55 +569,20 @@
 						to_remove.push(name.to_string());
 						continue;
 					}
-					let set = data.owners.iter().collect::<HashSet<_>>();
-					let expected_set = expected_owners.iter().collect::<HashSet<_>>();
-					let should_remove = set.difference(&expected_set).next().is_some();
-					if set == expected_set {
-						info!("secret data is ok");
-						continue;
-					}
 
 					let secret = nix_go!(config_field.sharedSecrets[{ name }]);
-					let owner_dependent: bool = nix_go_json!(secret.ownerDependent);
-					let regenerate_on_remove: bool = nix_go_json!(secret.regenerateOnOwnerRemoved);
-					#[allow(clippy::nonminimal_bool)]
-					if !owner_dependent && !(should_remove && regenerate_on_remove) {
-						warn!("reencrypting secret '{name}' for new owner set");
-						// TODO: force regeneration
-						if should_remove {
-							warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
-						}
-
-						let identity_holder = if !prefer_identities.is_empty() {
-							prefer_identities
-								.iter()
-								.find(|i| data.owners.iter().any(|s| s == *i))
-						} else {
-							data.owners.first()
-						};
-						let Some(identity_holder) = identity_holder else {
-							bail!("no available holder found");
-						};
-
-						let target_recipients = futures::stream::iter(&expected_owners)
-							.then(|m| async { config.key(m).await })
-							.collect::<Vec<_>>()
-							.await;
-						let target_recipients =
-							target_recipients.into_iter().collect::<Result<Vec<_>>>()?;
-
-						if let Some(secret) = data.secret.secret {
-							let host = config.host(identity_holder).await?;
-							let encrypted = host.reencrypt(secret, target_recipients).await?;
-
-							data.secret.secret = Some(encrypted);
-						}
-						data.owners = expected_owners;
-						config.replace_shared(name.to_owned(), data);
-					} else {
-						let shared = generate_shared(config, name, secret).await?;
-						config.replace_shared(name.to_owned(), shared)
-					}
+					config.replace_shared(
+						name.to_owned(),
+						update_owner_set(
+							&name,
+							config,
+							data,
+							secret,
+							&expected_owners,
+							&prefer_identities,
+						)
+						.await?,
+					);
 				}
 				for k in to_remove {
 					config.remove_shared(&k);
modifiedcmds/fleet/src/command.rsdiffbeforeafterboth
--- a/cmds/fleet/src/command.rs
+++ b/cmds/fleet/src/command.rs
@@ -1,5 +1,3 @@
-use std::thread::sleep;
-use std::time::Duration;
 use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};
 
 use anyhow::{anyhow, Result};
@@ -9,7 +7,7 @@
 use openssh::{OverSsh, OwningCommand, Session};
 use tokio::{io::AsyncRead, process::Command, select};
 use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
-use tracing::{info, debug};
+use tracing::debug;
 
 fn escape_bash(input: &str, out: &mut String) {
 	const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";
@@ -162,6 +160,10 @@
 		self
 	}
 	pub fn sudo(mut self) -> Self {
+		// TODO: Multiple escalation strategies.
+		// Maybe escalation should be moved to ConfigHost, to also support cases
+		// when there is no sudo on remote machine, but instead we can reconnect
+		// as root using ssh?
 		if std::env::var_os("NO_SUDO").is_some() {
 			let mut out = Self::new("su");
 			out.ssh_session = self.ssh_session.take();
@@ -267,7 +269,7 @@
 ) -> Result<Option<Vec<u8>>> {
 	cmd.stderr(Stdio::piped());
 	cmd.stdout(Stdio::piped());
-	debug!("running command {cmd:?} on local");
+	debug!("running command {str:?} on local");
 	let mut child = cmd.spawn()?;
 	let mut stderr = child.stderr.take().unwrap();
 	let stdout = child.stdout.take().unwrap();
@@ -328,7 +330,7 @@
 	err_handler: &mut dyn Handler,
 	mut out_handler: Option<&mut dyn Handler>,
 ) -> Result<Option<Vec<u8>>> {
-	debug!("running command {cmd:?} over ssh");
+	debug!("running command {str:?} over ssh");
 	cmd.stderr(openssh::Stdio::piped());
 	cmd.stdout(openssh::Stdio::piped());
 	let mut child = cmd.spawn().await?;
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -14,6 +14,7 @@
 use openssh::SessionBuilder;
 use serde::de::DeserializeOwned;
 use tempfile::NamedTempFile;
+use tracing::instrument;
 
 use crate::{
 	better_nix_eval::{Field, NixSessionPool},
@@ -28,12 +29,13 @@
 	pub opts: FleetOpts,
 	pub data: Mutex<FleetData>,
 	pub nix_args: Vec<OsString>,
-	/// fleetConfigurations.<name>.<localSystem>
-	pub fleet_field: Field,
-	/// fleet_config.configUnchecked
+	/// fleet_config.config
 	pub config_field: Field,
-	/// fleet_config.unchecked
+	/// fleet_config.unchecked.config
 	pub config_unchecked_field: Field,
+
+	/// import nixpkgs {system = local};
+	pub default_pkgs: Field,
 }
 
 #[derive(Clone)]
@@ -48,9 +50,12 @@
 }
 
 pub struct ConfigHost {
+	config: Config,
 	pub name: String,
 	pub local: bool,
 	pub session: OnceLock<Arc<openssh::Session>>,
+
+	pub nixos_config: Field,
 }
 impl ConfigHost {
 	async fn open_session(&self) -> Result<Arc<openssh::Session>> {
@@ -64,7 +69,7 @@
 		let session = session
 			.connect(&self.name)
 			.await
-			.map_err(|e| anyhow!("ssh error: {e}"))?;
+			.map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;
 		let session = Arc::new(session);
 		self.session.set(session.clone()).expect("TOCTOU happened");
 		Ok(session)
@@ -119,7 +124,8 @@
 		let mut cmd = self.cmd("fleet-install-secrets").await?;
 		cmd.arg("reencrypt").eqarg("--secret", data.encode_z85());
 		for target in targets {
-			cmd.eqarg("--targets", target);
+			let key = self.config.key(&target).await?;
+			cmd.eqarg("--targets", key);
 		}
 		let encoded = cmd
 			.sudo()
@@ -139,7 +145,7 @@
 			.arg("--substitute-on-destination")
 			.comparg("--to", format!("ssh-ng://{}", self.name))
 			.arg(path);
-		nix.run_nix().await?;
+		nix.run_nix().await.context("nix copy")?;
 		Ok(path.to_owned())
 	}
 	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {
@@ -161,6 +167,25 @@
 		}
 		cmd.run().await
 	}
+
+	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {
+		let nixos = &self.nixos_config;
+		let secrets = nix_go!(nixos.secrets);
+		let mut out = Vec::new();
+		for name in secrets.list_fields().await? {
+			let secret = nix_go!(secrets[{ name }]);
+			let is_shared: bool = nix_go_json!(secret.shared);
+			if is_shared {
+				continue;
+			}
+			out.push(name);
+		}
+		Ok(out)
+	}
+	pub async fn secret_field(&self, name: &str) -> Result<Field> {
+		let nixos = &self.nixos_config;
+		Ok(nix_go!(nixos.secrets[{ name }]))
+	}
 }
 
 impl Config {
@@ -178,28 +203,28 @@
 	}
 
 	pub async fn host(&self, name: &str) -> Result<ConfigHost> {
+		let config = &self.config_unchecked_field;
+		let nixos_config = nix_go!(config.configuredSystems[{ name }].config);
 		Ok(ConfigHost {
+			config: self.clone(),
 			name: name.to_owned(),
 			local: self.is_local(name),
 			session: OnceLock::new(),
+			nixos_config,
 		})
 	}
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
-		let fleet_field = &self.fleet_field;
-		let names = nix_go!(fleet_field.configuredHosts).list_fields().await?;
+		let config = &self.config_unchecked_field;
+		let names = nix_go!(config.hosts).list_fields().await?;
 		let mut out = vec![];
 		for name in names {
-			out.push(ConfigHost {
-				local: self.is_local(&name),
-				name,
-				session: OnceLock::new(),
-			})
+			out.push(self.host(&name).await?);
 		}
 		Ok(out)
 	}
 	pub async fn system_config(&self, host: &str) -> Result<Field> {
-		let fleet_field = &self.fleet_field;
-		Ok(nix_go!(fleet_field.configuredSystems[{ host }].config))
+		let fleet_field = &self.config_unchecked_field;
+		Ok(nix_go!(fleet_field.hosts[{ host }].nixosSystem.config))
 	}
 
 	pub(super) fn data(&self) -> MutexGuard<FleetData> {
@@ -233,6 +258,14 @@
 		data.shared_secrets.remove(secret);
 	}
 
+	pub fn list_secrets(&self, host: &str) -> Vec<String> {
+		let data = self.data();
+		let Some(secrets) = data.host_secrets.get(host) else {
+			return Vec::new();
+		};
+		secrets.keys().cloned().collect()
+	}
+
 	pub fn has_secret(&self, host: &str, secret: &str) -> bool {
 		let data = self.data();
 		let Some(host_secrets) = data.host_secrets.get(host) else {
@@ -319,18 +352,27 @@
 		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;
 		let root_field = pool.get().await?;
 
+		let builtins_field = Field::field(root_field.clone(), "builtins").await?;
 		if self.local_system == "detect" {
-			let builtins_field = Field::field(root_field.clone(), "builtins").await?;
 			self.local_system = nix_go_json!(builtins_field.currentSystem);
 		}
 		let local_system = self.local_system.clone();
 
 		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;
-
 		let fleet_field = nix_go!(fleet_root.default);
-		let config_field = nix_go!(fleet_field.configUnchecked);
-		let config_unchecked_field = nix_go!(fleet_field.unchecked);
 
+		let config_field = nix_go!(fleet_field.config);
+		let config_unchecked_field = nix_go!(fleet_field.unchecked.config);
+
+		let import = nix_go!(builtins_field.import);
+		let overlays = nix_go!(fleet_field.overlays);
+		let nixpkgs = nix_go!(fleet_field.nixpkgs | import);
+
+		let default_pkgs = nix_go!(nixpkgs(Obj {
+			overlays,
+			system: { self.local_system.clone() },
+		}));
+
 		let mut fleet_data_path = directory.clone();
 		fleet_data_path.push("fleet.nix");
 		let bytes = std::fs::read_to_string(fleet_data_path)?;
@@ -342,9 +384,9 @@
 			data,
 			local_system,
 			nix_args,
-			fleet_field,
 			config_field,
 			config_unchecked_field,
+			default_pkgs,
 		})))
 	}
 }
modifiedcmds/fleet/src/keys.rsdiffbeforeafterboth
--- a/cmds/fleet/src/keys.rs
+++ b/cmds/fleet/src/keys.rs
@@ -43,9 +43,9 @@
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
 	}
 
-	pub async fn recipients(&self, hosts: &[&str]) -> Result<Vec<impl Recipient>> {
+	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {
 		futures::stream::iter(hosts.iter())
-			.then(|m| self.recipient(m))
+			.then(|m| self.recipient(m.as_ref()))
 			.try_collect::<Vec<_>>()
 			.await
 	}
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -12,6 +12,8 @@
 mod fleetdata;
 
 use std::ffi::OsString;
+use std::io::{stderr, stdout, Write};
+use std::process::exit;
 use std::time::Duration;
 
 use anyhow::{bail, Result};
@@ -24,7 +26,7 @@
 use host::{Config, FleetOpts};
 use human_repr::HumanCount;
 use indicatif::{ProgressState, ProgressStyle};
-use tracing::info;
+use tracing::{error, info};
 use tracing::{info_span, Instrument};
 use tracing_indicatif::IndicatifLayer;
 use tracing_subscriber::{prelude::*, EnvFilter};
@@ -81,7 +83,7 @@
 }
 
 #[derive(Parser)]
-#[clap(version = "1.0", author)]
+#[clap(version, author)]
 struct RootOpts {
 	#[clap(flatten)]
 	fleet_opts: FleetOpts,
@@ -136,13 +138,13 @@
 		),
 	);
 
-	let filter = EnvFilter::from_default_env();
+	let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
 
 	tracing_subscriber::registry()
 		.with(
 			tracing_subscriber::fmt::layer()
 				.without_time()
-				.with_target(false)
+				.with_target(true)
 				.with_writer(indicatif_layer.get_stderr_writer())
 				.with_filter(filter), // .withou,
 		)
@@ -151,8 +153,15 @@
 }
 
 #[tokio::main]
-async fn main() -> Result<()> {
+async fn main() {
 	setup_logging();
+	if let Err(e) = main_real().await {
+		error!("{e:#}");
+		exit(1);
+	}
+}
+
+async fn main_real() -> Result<()> {
 	let _ = better_nix_eval::TOKIO_RUNTIME.set(tokio::runtime::Handle::current());
 
 	let nix_args = std::env::var_os("NIX_ARGS")
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -38,11 +38,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1703705939,
-        "narHash": "sha256-9s2Ep3NyRDj9HUgfv2TQUwQEanRUAmeXkvKIr/o1XbY=",
+        "lastModified": 1703974965,
+        "narHash": "sha256-dvZjLuAcLnv25bqStTL2ZICC5YSs8aynF5amRM+I6UM=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "1ada32da4ba24d7310653c9ac54888bee463f455",
+        "rev": "9f434bd436e2bb5615827469ed651e30c26daada",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1703643208,
-        "narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=",
+        "lastModified": 1703902408,
+        "narHash": "sha256-qXdWvu+tlgNjeoz8yQMRKSom6QyRROfgpmeOhwbujqw=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "ce117f3e0de8262be8cd324ee6357775228687cf",
+        "rev": "319f57cd2c34348c55970a4bf2b35afe82088681",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -29,7 +29,7 @@
         llvmPkgs = pkgs.buildPackages.llvmPackages_11;
         rust =
           (pkgs.rustChannelOf {
-            date = "2023-12-26";
+            date = "2023-12-29";
             channel = "nightly";
           })
           .default
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -1,18 +1,31 @@
 {flake-utils}: {
   fleetConfiguration = {
+    # TODO: Provide by fleet, instead of requesting user to provide it.
+    # This is not good that user needs to provide it, as it becomes a flake data, and fleet arbitrarily rewriting it
+    # always dirnets the flake. Instead, fleetConfiguration should return function, parameters of which should be filled
+    # by fleet itself, which is possible since fleet moving to nix repl execution.
     data,
     nixpkgs,
+    overlays ? [],
     hosts,
-    ...
-  } @ allConfig: let
+    modules,
+    globalModules ? [],
+  }: let
     hostNames = nixpkgs.lib.attrNames hosts;
-    config = builtins.removeAttrs allConfig ["nixpkgs" "data"];
     fleetLib = import ./fleetLib.nix {
       inherit nixpkgs hostNames;
     };
   in let
     root = nixpkgs.lib.evalModules {
-      modules = (import ../modules/fleet/_modules.nix) ++ [config data];
+      modules =
+        (import ../modules/fleet/_modules.nix)
+        ++ [
+          data
+          ({...}: {
+            inherit globalModules hosts;
+          })
+        ]
+        ++ modules;
       specialArgs = {
         inherit nixpkgs fleetLib;
       };
@@ -25,84 +38,20 @@
     withData = {
       root,
       data,
-    }: rec {
+    }: {
       configuredHosts = root.config.hosts;
-      configuredUncheckedHosts = root.config.hosts;
-      configuredSystems = configuredSystemsWithExtraModules [];
-      configuredSystemsWithExtraModules = extraModules:
-        nixpkgs.lib.listToAttrs (
-          map
-          (
-            name: {
-              inherit name;
-              value = nixpkgs.lib.nixosSystem {
-                system = configuredHosts.${name}.system;
-                modules = configuredHosts.${name}.modules ++ extraModules;
-                specialArgs = {
-                  inherit fleetLib;
-                  fleet = fleetLib.hostsToAttrs (host: configuredSystems.${host}.config);
-                };
-              };
-            }
-          )
-          (builtins.attrNames root.config.hosts)
-        );
-      buildableSystems = {localSystem}: let
-        buildConfigurationModule = {config, ...}: {
-          # Equivalent to nixpkgs.localSystem
-          # nixpkgs.system = localSystem;
-          nixpkgs.buildPlatform.system = localSystem;
-        };
-      in
-        configuredSystemsWithExtraModules [
-          buildConfigurationModule
-        ];
-      buildSystems = {localSystem}: let
-        buildConfigurationModule = {config, ...}: {
-          # Equivalent to nixpkgs.localSystem
-          # nixpkgs.system = localSystem;
-          nixpkgs.buildPlatform.system = localSystem;
-        };
-      in {
-        toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [
-          buildConfigurationModule
-          ({...}: {
-            buildTarget = "toplevel";
-          })
-        ]);
-        sdImage = builtins.mapAttrs (_name: value: value.config.system.build.sdImage) (configuredSystemsWithExtraModules [
-          buildConfigurationModule
-          #(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix")
-          ({...}: {
-            buildTarget = "sd-image";
-          })
-        ]);
-        installationCd = builtins.mapAttrs (_name: value: value.config.system.build.isoImage) (configuredSystemsWithExtraModules [
-          buildConfigurationModule
-          (nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
-          ({lib, ...}: {
-            buildTarget = "installation-cd";
-            # Needed for https://github.com/NixOS/nixpkgs/issues/58959
-            boot.supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"];
-          })
-        ]);
-      };
-      configUnchecked = root.config;
+      config = root.config;
     };
     defaultData = withData {
       inherit data;
       root = checkedRoot;
     };
     uncheckedData = withData {inherit data root;};
-  in rec {
-    inherit (defaultData) configuredHosts configuredSystems buildSystems configUnchecked buildableSystems;
+  in {
+    inherit nixpkgs overlays;
+    inherit (defaultData) configuredHosts configuredSystems config buildableSystems;
     unchecked = {
-      inherit (uncheckedData) configuredHosts configuredSystems buildSystems configUnchecked buildableSystems;
-    };
-    injectData = data: let
-      injectedData = withData data;
-    in {
-      inherit (injectedData) configuredHosts configuredSystems buildSystems configUnchecked;
+      inherit (uncheckedData) configuredHosts configuredSystems config buildableSystems;
     };
   };
 }
modifiedmodules/fleet/meta.nixdiffbeforeafterboth
--- a/modules/fleet/meta.nix
+++ b/modules/fleet/meta.nix
@@ -1,49 +1,82 @@
-{ lib, fleetLib, config, ... }: with lib;
-let
-  host = with types; {
-    options = {
-      modules = mkOption {
-        type = listOf (mkOptionType {
-          name = "submodule";
-          inherit (submodule { }) check;
-          merge = lib.options.mergeOneOption;
-          description = "Nixos modules";
-        });
-        description = "List of nixos modules";
-        default = [ ];
-      };
-      system = mkOption {
-        type = str;
-        description = "Type of system";
+{
+  lib,
+  fleetLib,
+  config,
+  nixpkgs,
+  ...
+}:
+with lib; let
+  hostModule = with types;
+    {...} @ hostConfig: {
+      options = {
+        modules = mkOption {
+          type = listOf (mkOptionType {
+            name = "submodule";
+            inherit (submodule {}) check;
+            merge = lib.options.mergeOneOption;
+            description = "Nixos modules";
+          });
+          description = "List of nixos modules";
+          default = [];
+        };
+        system = mkOption {
+          type = str;
+          description = "Type of system";
+        };
+        encryptionKey = mkOption {
+          type = str;
+          description = "Encryption key";
+        };
+        nixosSystem = mkOption {
+          type = unspecified;
+          description = "Nixos configuration";
+        };
       };
-      encryptionKey = mkOption {
-        type = str;
-        description = "Encryption key";
+      config.nixosSystem = nixpkgs.lib.nixosSystem {
+        inherit (hostConfig.config) system modules;
+        specialArgs = {
+          inherit fleetLib;
+          fleet = fleetLib.hostsToAttrs (host: config.hosts.${host}.nixosSystem.config);
+        };
       };
     };
+  overlayType = mkOptionType {
+    name = "nixpkgs-overlay";
+    description = "nixpkgs overlay";
+    check = lib.isFunction;
+    merge = lib.mergeOneOption;
   };
-in
-{
+in {
   options = with types; {
     hosts = mkOption {
-      type = attrsOf (submodule host);
-      default = { };
+      type = attrsOf (submodule hostModule);
+      default = {};
       description = "Configurations of individual hosts";
     };
     globalModules = mkOption {
       type = listOf (mkOptionType {
         name = "submodule";
-        inherit (submodule { }) check;
+        inherit (submodule {}) check;
         merge = lib.options.mergeOneOption;
         description = "Nixos modules";
       });
       description = "Modules, which should be added to every system";
-      default = [ ];
+      default = [];
     };
+    overlays = mkOption {
+      default = [];
+      type = listOf overlayType;
+    };
   };
   config = {
     hosts = fleetLib.hostsToAttrs (host: {
-      modules = config.globalModules;
+      modules =
+        config.globalModules
+        ++ [
+          ({...}: {
+            nixpkgs.overlays = config.overlays;
+          })
+        ];
     });
     globalModules = import ../../nixos/modules/module-list.nix;
   };
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -1,69 +1,48 @@
 { lib, fleetLib, config, ... }: with lib; with fleetLib;
 let
-  sharedSecret = with types; {
+  sharedSecret = with types; ({config, ...}: {
     options = {
       expectedOwners = mkOption {
-        type = listOf str;
+        type = nullOr (listOf str);
         description = ''
-          List of hosts to encrypt secret for
+          List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)
 
           Secrets would be decrypted and stored to /run/secrets/$\{name} on owners
         '';
-        default = [ ];
       };
-      ownerDependent = mkOption {
+      # TODO: Aren't those options may be just desugared to data/expectedData?
+      regenerateOnOwnerAdded = mkOption {
         type = bool;
-        description = "Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted";
+        description = ''
+          Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.
+          
+          You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.
+        '';
       };
-      generateImpure = mkOption {
-        type = unspecified;
+      regenerateOnOwnerRemoved = mkOption {
+        default = config.regenerateOnOwnerAdded;
+        type = bool;
+        description = ''
+          Should this secret be removed on owner removal, or it may be just reencrypted
+          
+          Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.
+          Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked
+          in some other way than by this secret ownership, I.e by firewall/etc.
+        '';
       };
       generator = mkOption {
-        type = nullOr (submodule {
-          packages = mkOption {
-            type = attrsOf package;
-            description = ''
-              Derivation to execute for shared secret generation (key = system).
-              This derivation should produce directory, with exactly two files:
-                - publicData
-                - encryptedSecretData
-
-              If null - secret value may only be created manually.
-            '';
-          };
-          expectedData = mkOption {
-            type = types.unspecified;
-            description = "Data expected to be used for secret generation, if doesn't match specified - secret should be regenerated";
-          };
-          dependencies = mkOption {
-            type = listOf str;
-            description = ''
-              List of secrets, on which this secret depends.
-
-              During generation, generator command will be ran on host, which already has specified secrets generated.
-            '';
-            default = [];
-          };
-          data = mkOption {
-            type = types.unspecified;
-            description = "Data used for secret generation. Imported from fleet.nix";
-            default = null;
-            internal = true;
-          };
-        });
-        default = null;
-      };
-      expireIn = mkOption {
-        type = nullOr int;
-        description = "Time in hours, in which this secret should be regenerated";
+        type = nullOr unspecified;
+        description = "Derivation to evaluate for secret generation";
         default = null;
       };
       createdAt = mkOption {
         type = nullOr str;
+        description = "When this secret was (re)generated";
         default = null;
       };
       expiresAt = mkOption {
         type = nullOr str;
+        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
         default = null;
       };
 
@@ -78,6 +57,9 @@
         '';
         default = [ ];
       };
+      # TODO: Make secret generator generate arbitrary number of secret/public parts?
+      # Make it generate a folder, where all files except suffixed by .enc are public, and the rest are secret?
+      # How should modules refer to those files then?
       public = mkOption {
         type = nullOr str;
         description = "Secret public data. Imported from fleet.nix";
@@ -90,7 +72,7 @@
         internal = true;
       };
     };
-  };
+  });
   hostSecret = with types; {
     options = {
       createdAt = mkOption {
@@ -132,7 +114,7 @@
   config = {
     assertions = mapAttrsToList
       (name: secret: {
-        assertion = builtins.sort (a: b: a < b) secret.owners == builtins.sort (a: b: a < b) secret.expectedOwners;
+        assertion = secret.expectedOwners == null || builtins.sort (a: b: a < b) secret.owners == builtins.sort (a: b: a < b) secret.expectedOwners;
         message = "Shared secret ${name} is expected to be encrypted for ${builtins.toJSON secret.expectedOwners}, but it is encrypted for ${builtins.toJSON secret.owners}. Run fleet secrets regenerate to fix";
       })
       config.sharedSecrets;
@@ -141,6 +123,7 @@
         let
           cleanupSecret = (secretName: v: {
             inherit (v) public secret;
+            shared = true;
           });
         in
         [
modifiednixos/secrets.nixdiffbeforeafterboth
--- a/nixos/secrets.nix
+++ b/nixos/secrets.nix
@@ -5,7 +5,7 @@
 let
   sysConfig = config;
   secretType = types.submodule ({ config, ... }: {
-    config = let secretName = config._module.args.name; in rec {
+    config = let secretName = config._module.args.name; in {
       stableSecretPath = mkOptionDefault "/run/secrets/secret-stable-${secretName}";
       secretPath = mkOptionDefault "/run/secrets/secret-${config.secretHash}-${secretName}";
       secretHash = mkOptionDefault (if config.secret != null then (builtins.hashString "sha1" config.secret) else throw "secret is not defined for secret ${secretName}");
@@ -14,63 +14,74 @@
       publicPath = mkOptionDefault "/run/secrets/public-${config.publicHash}-${secretName}";
       publicHash = mkOptionDefault (if config.public != null then (builtins.hashString "sha1" config.public) else throw "public is not defined for secret ${secretName}");
     };
-    options = {
+    options = with types; {
+      shared = mkOption {
+        description = "Is this secret owned by this machine, or propagated from shared secrets";
+        default = false;
+      };
+
+      generator = mkOption {
+        type = nullOr unspecified;
+        description = "Derivation to evaluate for secret generation";
+        default = null;
+      };
+
       public = mkOption {
-        type = types.nullOr types.str;
+        type = nullOr str;
         description = "Secret public data";
         default = null;
       };
       secret = mkOption {
-        type = types.nullOr types.str;
+        type = nullOr str;
         description = "Encrypted secret data";
         default = null;
       };
       mode = mkOption {
-        type = types.str;
+        type = str;
         description = "Secret mode";
         default = "0440";
       };
       owner = mkOption {
-        type = types.str;
+        type = str;
         description = "Owner of the secret";
         default = "root";
       };
       group = mkOption {
-        type = types.str;
+        type = str;
         description = "Group of the secret";
         default = sysConfig.users.users.${config.owner}.group;
       };
 
       secretHash = mkOption {
-        type = types.str;
+        type = str;
         description = "Hash of .secret field";
       };
       publicHash = mkOption {
-        type = types.str;
+        type = str;
         description = "Hash of .public field";
       };
 
       stableSecretPath = mkOption {
-        type = types.str;
+        type = str;
         description = ''
           Use this, if target process supports re-reading of secret from disk,
           and doesn't needs to be restarted when secret is updated in file
         '';
       };
       secretPath = mkOption {
-        type = types.str;
+        type = str;
         description = "Path to decrypted secret, suffixed with contents hash";
       };
 
       stablePublicPath = mkOption {
-        type = types.str;
+        type = str;
         description = ''
           Use this, if target process supports re-reading of secret from disk,
           and doesn't needs to be restarted when secret is updated in file
         '';
       };
       publicPath = mkOption {
-        type = types.str;
+        type = str;
         description = "Path to the public part of secret";
       };
     };