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

difftreelog

feat use builtin for getting secret

lyunptusYaroslav Bolyukin2026-01-22parent: #c810e3a.patch.diff
in: trunk

12 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -723,15 +723,6 @@
 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
-name = "convert_case"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
 name = "cookie-factory"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -765,34 +756,6 @@
 ]
 
 [[package]]
-name = "crossterm"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "derive_more",
- "document-features",
- "filedescriptor",
- "mio",
- "parking_lot",
- "rustix 1.1.2",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "crypto-common"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -932,27 +895,6 @@
 dependencies = [
  "powerfmt",
  "serde_core",
-]
-
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "convert_case",
- "proc-macro2",
- "quote",
- "syn",
 ]
 
 [[package]]
@@ -976,15 +918,6 @@
  "proc-macro2",
  "quote",
  "syn",
-]
-
-[[package]]
-name = "document-features"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
-dependencies = [
- "litrs",
 ]
 
 [[package]]
@@ -1073,17 +1006,6 @@
 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
 
 [[package]]
-name = "filedescriptor"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
-dependencies = [
- "libc",
- "thiserror 1.0.69",
- "winapi",
-]
-
-[[package]]
 name = "find-crate"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1128,7 +1050,6 @@
  "chrono",
  "clap",
  "clap_complete",
- "crossterm",
  "fleet-base",
  "fleet-shared",
  "futures",
@@ -1142,7 +1063,6 @@
  "openssh",
  "opentelemetry",
  "opentelemetry_sdk",
- "owo-colors",
  "peg",
  "regex",
  "serde",
@@ -1503,12 +1423,6 @@
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
-name = "hermit-abi"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
-
-[[package]]
 name = "hex"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1961,24 +1875,7 @@
 dependencies = [
  "memchr",
  "serde",
-]
-
-[[package]]
-name = "is-terminal"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.59.0",
 ]
-
-[[package]]
-name = "is_ci"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
 
 [[package]]
 name = "is_terminal_polyfill"
@@ -2083,12 +1980,6 @@
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
-
-[[package]]
-name = "litrs"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
 
 [[package]]
 name = "lock_api"
@@ -2161,7 +2052,6 @@
 checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 dependencies = [
  "libc",
- "log",
  "wasi 0.11.1+wasi-snapshot-preview1",
  "windows-sys 0.59.0",
 ]
@@ -2428,16 +2318,6 @@
 ]
 
 [[package]]
-name = "owo-colors"
-version = "4.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
-dependencies = [
- "supports-color 2.1.0",
- "supports-color 3.0.2",
-]
-
-[[package]]
 name = "papergrid"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3335,27 +3215,6 @@
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
-[[package]]
-name = "signal-hook-mio"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
-dependencies = [
- "libc",
- "mio",
- "signal-hook",
-]
 
 [[package]]
 name = "signal-hook-registry"
@@ -3449,25 +3308,6 @@
 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
-name = "supports-color"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
-dependencies = [
- "is-terminal",
- "is_ci",
-]
-
-[[package]]
-name = "supports-color"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
-dependencies = [
- "is_ci",
-]
-
-[[package]]
 name = "syn"
 version = "2.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4170,12 +4010,6 @@
 checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
 
 [[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
 name = "unicode-width"
 version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4448,25 +4282,9 @@
  "home",
  "once_cell",
  "rustix 0.38.44",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
 ]
 
 [[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4474,12 +4292,6 @@
 dependencies = [
  "windows-sys 0.61.2",
 ]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-core"
modifiedREADME.adocdiffbeforeafterboth
--- a/README.adoc
+++ b/README.adoc
@@ -211,7 +211,7 @@
   ];
   # And finally, I have secrets, which are shared between machines.
   # Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
-  sharedSecrets = {
+  secrets = {
     "ca.pem" = {
       # This is just the public key, no need to regenerate it to change owner list
       regenerateOnOwnerAdded = false;
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -28,12 +28,10 @@
 async-trait = "0.1"
 base64 = "0.22.1"
 chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
 futures = "0.3"
 hostname = "0.4.1"
 itertools = "0.14"
 openssh = "0.11"
-owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
 peg = "0.8"
 regex = "1.11"
 shlex = "1.3"
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
before · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	io::{self, Read, Write, stdin, stdout},4	path::PathBuf,5};67use anyhow::{Context, Result, anyhow, bail, ensure};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11	fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},12	host::Config,13	opts::FleetOpts,14	secret::{Expectations, RegenerationReason, SharedSecretDefinition, secret_needs_regeneration},15};16use fleet_shared::SecretData;17use nix_eval::{NixType, Value, nix_go, nix_go_json};18use owo_colors::OwoColorize;19use serde::Deserialize;20use tabled::{Table, Tabled};21use tokio::{fs::read, task::spawn_blocking};22use tracing::{Instrument, error, info, info_span, warn};2324#[derive(Parser)]25pub enum Secret {26	AddManager,27	/// Force load host keys for all defined hosts28	ForceKeys,29	/// Read secret from remote host, requires sudo on one of the owning hosts30	Read {31		/// Secret name to read32		name: String,3334		/// Distribution with what machine to read35		/// If not shared between multiple - defaults to single owner36		#[clap(short = 'm', long)]37		machine: Option<String>,3839		/// Which private secret part to read40		#[clap(short = 'p', long, default_value = "secret")]41		part: String,4243		/// Which host should we use to decrypt, in case if reencryption is required, without44		/// regeneration45		#[clap(long)]46		prefer_identities: Vec<String>,47	},48	Regenerate {49		/// Which host should we use to decrypt, in case if reencryption is required, without50		/// regeneration51		#[clap(long)]52		prefer_identities: Vec<String>,53		/// Only regenerate shared secrets54		#[clap(long)]55		skip_hosts: bool,56	},57	List {},58	Edit {59		name: String,60		#[clap(short = 'm', long)]61		machine: String,6263		#[clap(long)]64		add: bool,6566		/// Which private secret part to read67		#[clap(short = 'p', long, default_value = "secret")]68		part: String,69	},70}7172#[allow(clippy::too_many_arguments)]73#[tracing::instrument(skip(config, secret, definition, prefer_identities))]74async fn maybe_regenerate_shared_secret(75	secret_name: &str,76	config: &Config,77	mut secret: FleetSecretDistribution,78	definition: SharedSecretDefinition,79	prefer_identities: &[String],80	expectations: &Expectations,81) -> Result<FleetSecretDistribution> {82	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);83	let value = definition.definition_value();8485	let (should_reencrypt, reason) = match reason {86		Some(RegenerationReason::OwnersAdded(_)) => {87			// Secret always needs to be reencrypted for new owners to be able to read it88			(89				true,90				if nix_go_json!(value.regenerateOnOwnerAdded) {91					reason92				} else {93					None94				},95			)96		}97		Some(RegenerationReason::OwnersRemoved(_)) => {98			// No need to reencrypt, we can just leave stanzas in place.99			if nix_go_json!(value.regenerateOnOwnerRemoved) {100				(true, reason)101			} else {102				(false, None)103			}104		}105		Some(_) => (true, reason),106		None => (false, None),107	};108109	if let Some(reason) = reason {110		info!("secret needs to be regenerated: {reason}");111		let generated = generate_shared(config, secret_name, definition, expectations).await?;112		Ok(generated)113	} else if should_reencrypt {114		info!("secret needs to be reencrypted");115		let identity_holder = if !prefer_identities.is_empty() {116			prefer_identities117				.iter()118				.find(|i| secret.owners.iter().any(|s| s == *i))119		} else {120			secret.owners.first()121		};122		let Some(identity_holder) = identity_holder else {123			bail!("no available holder found");124		};125126		for (part_name, part) in secret.secret.parts.iter_mut() {127			let _span = info_span!("part reencryption", part_name);128			if !part.raw.encrypted {129				continue;130			}131			let host = config.host(identity_holder).await?;132			let encrypted = host133				.reencrypt(134					part.raw.clone(),135					expectations.owners.iter().cloned().collect(),136				)137				.await?;138			part.raw = encrypted;139		}140		secret.owners = expectations.owners.clone();141		Ok(secret)142	} else {143		Ok(secret)144	}145}146147#[derive(Deserialize)]148#[serde(rename_all = "camelCase")]149enum GeneratorKind {150	Impure,151	Pure,152}153154async fn generate_pure(155	_config: &Config,156	_display_name: &str,157	_secret: Value,158	_default_generator: Value,159	_expectations: &Expectations,160) -> Result<FleetSecretData> {161	bail!("pure generators are broken for now")162}163async fn generate_impure(164	config: &Config,165	_display_name: &str,166	secret: Value,167	default_generator: Value,168	expectations: &Expectations,169) -> Result<FleetSecretData> {170	let generator = nix_go!(secret.generator);171	let on: Option<String> = nix_go_json!(default_generator.impureOn);172173	let nixpkgs = &config.nixpkgs;174175	let host = if let Some(on) = &on {176		config.host(on).await?177	} else {178		config.local_host()179	};180	let on_pkgs = host.pkgs().await?;181	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);182183	let mut recipients = Vec::new();184	for owner in &expectations.owners {185		let key = config.key(owner).await?;186		recipients.push(key);187	}188	let generators = nix_go!(mk_secret_generators(Obj { recipients }));189	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;190191	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));192193	let generator = nix_go!(call_package(generator)(Obj {}));194195	let generator = spawn_blocking(move || generator.build("out"))196		.await197		.expect("nix build shouldn't fail")?;198	let generator = host.remote_derivation(&generator).await?;199200	let out_parent = host.mktemp_dir().await?;201	let out = format!("{out_parent}/out");202203	let mut r#gen = host.cmd(generator).await?;204	r#gen.env("out", &out);205	if on.is_none() {206		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.207		let project_path: String = config208			.directory209			.clone()210			.into_os_string()211			.into_string()212			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;213		r#gen.env("FLEET_PROJECT", project_path);214	}215	r#gen.run().await.context("impure generator")?;216217	{218		let marker = host.read_file_text(format!("{out}/marker")).await?;219		ensure!(marker == "SUCCESS", "generation not succeeded");220	}221222	let mut parts = BTreeMap::new();223	for part in host.read_dir(&out).await? {224		if part == "created_at" || part == "expires_at" || part == "marker" {225			continue;226		}227		let contents: SecretData = host228			.read_file_text(format!("{out}/{part}"))229			.await?230			.parse()231			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;232		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });233	}234235	let created_at = host.read_file_value(format!("{out}/created_at")).await?;236	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();237238	let new_data = FleetSecretData {239		created_at,240		expires_at,241		parts,242		generation_data: expectations.generation_data.clone(),243	};244245	if let Some(reason) = secret_needs_regeneration(&new_data, &expectations.owners, expectations) {246		bail!("newly generated secret needs to be regenerated: {reason}")247	}248249	Ok(new_data)250}251252async fn generate(253	config: &Config,254	display_name: &str,255	secret: Value,256	expectations: &Expectations,257) -> Result<FleetSecretData> {258	let generator = nix_go!(secret.generator);259	// Can't properly check on nix module system level260	{261		let gen_ty = generator.type_of();262		if matches!(gen_ty, NixType::Null) {263			bail!("secret has no generator defined, can't automatically generate it.");264		}265		if matches!(gen_ty, NixType::Attrs) {266			if !generator.has_field("__functor")? {267				bail!("generator should be functor, got {gen_ty:?}");268			}269		} else if matches!(gen_ty, NixType::Function) {270			bail!("generator should be functor, got {gen_ty:?}");271		}272	}273	let nixpkgs = &config.nixpkgs;274	let default_pkgs = &config.default_pkgs;275	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);276	// Generators provide additional information in passthru, to access277	// passthru we should call generator, but information about where this generator is supposed to build278	// is located in passthru... Thus evaluating generator on host.279	//280	// Maybe it is also possible to do some magic with __functor?281	//282	// I don't want to make modules always responsible for additional secret data anyway,283	// so it should be in derivation, and not in the secret data itself.284	let generators = nix_go!(default_mk_secret_generators(Obj {285		recipients: <Vec<String>>::new(),286	}));287	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;288289	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));290	let default_generator = nix_go!(call_package(generator)(Obj {}));291292	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);293294	match kind {295		GeneratorKind::Impure => {296			generate_impure(297				config,298				display_name,299				secret,300				default_generator,301				expectations,302			)303			.await304		}305		GeneratorKind::Pure => {306			generate_pure(307				config,308				display_name,309				secret,310				default_generator,311				expectations,312			)313			.await314		}315	}316}317async fn generate_shared(318	config: &Config,319	display_name: &str,320	secret: SharedSecretDefinition,321	expectations: &Expectations,322) -> Result<FleetSecretDistribution> {323	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);324	Ok(FleetSecretDistribution {325		managed: Some(true),326		secret: generate(327			config,328			display_name,329			secret.definition_value(),330			expectations,331		)332		.await?,333		owners: expectations.owners.clone(),334	})335}336337async fn parse_public(338	public: Option<String>,339	public_file: Option<PathBuf>,340) -> Result<Option<SecretData>> {341	Ok(match (public, public_file) {342		(Some(v), None) => Some(SecretData {343			data: v.into(),344			encrypted: false,345		}),346		(None, Some(v)) => Some(SecretData {347			data: read(v).await?,348			encrypted: false,349		}),350		(Some(_), Some(_)) => {351			bail!("only public or public_file should be set")352		}353		(None, None) => None,354	})355}356357async fn parse_secret() -> Result<Option<Vec<u8>>> {358	let mut input = vec![];359	stdin().read_to_end(&mut input)?;360	if input.is_empty() {361		Ok(None)362	} else {363		Ok(Some(input))364	}365}366367fn parse_machines(368	initial: BTreeSet<String>,369	machines: Option<Vec<String>>,370	mut add_machines: Vec<String>,371	mut remove_machines: Vec<String>,372) -> Result<BTreeSet<String>> {373	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {374		bail!("no operation");375	}376377	let initial_machines = initial.clone();378	let mut target_machines = initial;379	info!("Currently encrypted for {initial_machines:?}");380381	if let Some(machines) = machines {382		ensure!(383			add_machines.is_empty() && remove_machines.is_empty(),384			"can't combine --machines and --add-machines/--remove-machines"385		);386		let target = initial_machines.iter().collect::<HashSet<_>>();387		let source = machines.iter().collect::<HashSet<_>>();388		for removed in target.difference(&source) {389			remove_machines.push((*removed).clone());390		}391		for added in source.difference(&target) {392			add_machines.push((*added).clone());393		}394	}395396	for machine in &remove_machines {397		if !target_machines.remove(machine) {398			warn!("secret is not enabled for {machine}");399		}400	}401	for machine in &add_machines {402		if !target_machines.insert(machine.to_owned()) {403			warn!("secret is already added to {machine}");404		}405	}406	if !remove_machines.is_empty() {407		// TODO: maybe force secret regeneration?408		// Not that useful without revokation.409		warn!(410			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"411		);412	}413	Ok(target_machines)414}415impl Secret {416	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {417		match self {418			Secret::AddManager => {419				todo!("part of fleet-pusher")420			}421			Secret::ForceKeys => {422				for host in config.list_hosts().await? {423					if opts.should_skip(&host).await? {424						continue;425					}426					config.key(&host.name).await?;427				}428			}429			Secret::Read {430				name,431				machine,432				part: part_name,433				mut prefer_identities,434			} => {435				let Some(secret) = config.shared_secret(&name) else {436					bail!("secret doesn't exists");437				};438439				let dist = if secret.len() == 1 {440					&secret[0]441				} else if let Some(machine) = machine {442					let dist = secret.get(&machine);443					let Some(dist) = dist else {444						bail!("machine {machine} has no distribution of secret {name}");445					};446					prefer_identities.push(machine);447					dist448				} else {449					bail!(450						"secret {name} has shares, but no --machine specified for specifing which do you need"451					)452				};453454				let Some(part) = dist.secret.parts.get(&part_name) else {455					bail!("no part {part_name} in secret {name}");456				};457				let data = if part.raw.encrypted {458					let identity_holder = if !prefer_identities.is_empty() {459						prefer_identities460							.iter()461							.find(|i| dist.owners.iter().any(|s| s == *i))462					} else {463						dist.owners.first()464					};465					let Some(identity_holder) = identity_holder else {466						bail!("no available holder found");467					};468					let host = config.host(identity_holder).await?;469					host.decrypt(part.raw.clone()).await?470				} else {471					part.raw.data.clone()472				};473				stdout().write_all(&data)?;474			}475			Secret::Regenerate {476				prefer_identities,477				skip_hosts,478			} => {479				/*480								info!("checking for secrets to regenerate");481								let expected_shared_set = config482									.list_configured_shared()483									.await?484									.into_iter()485									.collect::<HashSet<_>>();486								let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();487								{488									// Generate missing shared489									let _span = info_span!("shared").entered();490									for missing in expected_shared_set.difference(&stored_shared_set) {491										let definition = config.shared_secret_definition(missing)?;492										if !definition.is_managed()? {493											info!("skipping unmanaged secret: {missing}");494											continue;495										}496										let expectations = definition497											.expectations()498											.with_context(|| format!("expectations for shared {missing:?}"))?;499										info!("generating secret: {missing}");500										let shared = generate_shared(config, missing, definition, &expectations)501											.in_current_span()502											.await?;503										config.replace_shared(missing.to_string(), shared)504									}505								}506								if !skip_hosts {507									for host in config.list_hosts().await? {508										if opts.should_skip(&host).await? {509											continue;510										}511512										let _span = info_span!("host", host = host.name).entered();513										let expected_set = host514											.list_defined_secrets()?515											.into_iter()516											.collect::<HashSet<_>>();517										let stored_set = config518											.list_secrets_for_owner(&host.name)519											.into_iter()520											.collect::<HashSet<_>>();521										for missing_secret in expected_set.difference(&stored_set) {522											let secret = host.secret_definition(missing_secret)?;523											if secret.is_shared()? {524												continue;525											}526											info!("generating missing secret: {missing_secret}");527											let expectations = secret.expectations().with_context(|| {528												format!("expectations for {missing_secret:?} of {:?}", host.name)529											})?;530											let generated = match generate(531												config,532												missing_secret,533												secret.definition_value()?,534												&expectations,535											)536											.in_current_span()537											.await538											{539												Ok(v) => v,540												Err(e) => {541													error!("{e:?}");542													continue;543												}544											};545											config.insert_secret(host.name, missing_secret.to_string(), generated)546										}547										for known_secret in stored_set.intersection(&expected_set) {548											let secret = host.secret_definition(known_secret)?;549											if secret.is_shared()? {550												continue;551											}552											info!("updating secret: {known_secret}");553											let data = config.host_secret(&host.name, known_secret)?;554											let expectations = secret.expectations()?;555											if let Some(regen_reason) = data.needs_regeneration(&expectations) {556												info!("needs regeneration: {regen_reason}");557												let generated = match generate(558													config,559													known_secret,560													secret.definition_value()?,561													&expectations,562												)563												.in_current_span()564												.await565												{566													Ok(v) => v,567													Err(e) => {568														error!("{e:?}");569														continue;570													}571												};572												config.insert_secret(573													&host.name,574													known_secret.to_string(),575													FleetLegacyHostSecret {576														managed: Some(true),577														secret: generated,578													},579												)580											}581										}582										for removed_secret in stored_set.difference(&expected_set) {583											let definition = host.secret_definition(removed_secret)?;584											if definition.is_shared()? {585												continue;586											}587											info!("removing secret: {removed_secret}");588											config.remove_secret(&host.name, removed_secret);589										}590									}591								}592								for known_secret in stored_shared_set.intersection(&expected_shared_set) {593									info!("updating shared secret: {known_secret}");594									let data = config.shared_secret(known_secret)?.expect("exists");595596									let definition = config.shared_secret_definition(known_secret)?;597									let expectations = definition.expectations()?;598									config.replace_shared(599										known_secret.to_owned(),600										maybe_regenerate_shared_secret(601											known_secret,602											config,603											data,604											definition,605											&prefer_identities,606											&expectations,607										)608										.await?,609									);610								}611								for removed_secret in stored_shared_set.difference(&expected_shared_set) {612									info!("removing shared secret: {removed_secret}");613									config.remove_shared(removed_secret);614								}615				*/616				todo!()617			}618			Secret::List {} => {619				let _span = info_span!("loading secrets").entered();620				let configured = config.list_configured_shared().await?;621				#[derive(Tabled)]622				struct SecretDisplay {623					#[tabled(rename = "Name")]624					name: String,625					#[tabled(rename = "Owners")]626					owners: String,627				}628				let mut table = vec![];629				for name in configured.iter().cloned() {630					let config = config.clone();631					let data = config.shared_secret(&name).expect("exists");632					let definition = config.shared_secret_definition(&name)?;633					let expectations = definition.expectations()?;634					let owners = data635						.owners()636						.map(|o| {637							if expectations.owners.contains(o) {638								o.green().to_string()639							} else {640								o.red().to_string()641							}642						})643						.collect::<Vec<_>>();644					table.push(SecretDisplay {645						owners: owners.join(", "),646						name,647					})648				}649				info!("loaded\n{}", Table::new(table).to_string())650			}651			Secret::Edit {652				name,653				machine,654				part,655				add,656			} => {657				let secret = config658					.host_secret(&machine, &name)659					.context("secret not found")?;660				if let Some(data) = secret.secret.parts.get(&part) {661					let host = config.host(&machine).await?;662					let secret = host.decrypt(data.raw.clone()).await?;663					String::from_utf8(secret).context("secret is not utf8")?664				} else if add {665					String::new()666				} else {667					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");668				};669			}670		}671		Ok(())672	}673}674675/*676async fn edit_temp_file(677	builder: tempfile::Builder<'_, '_>,678	r: Vec<u8>,679	header: &str,680	comment: &str,681) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {682	if !stdin().is_tty() {683		// TODO: Also try to open /dev/tty directly?684		bail!("stdin is not tty, can't open editor");685	}686687	use std::fmt::Write;688	let mut file = builder.tempfile()?;689690	let mut full_header = String::new();691	let mut had = false;692	for line in header.trim_end().lines() {693		had = true;694		writeln!(&mut full_header, "{comment}{line}")?;695	}696	if had {697		writeln!(&mut full_header, "{}", comment.trim_end())?;698	}699	writeln!(700		&mut full_header,701		"{comment}Do not touch this header! It will be removed automatically"702	)?;703704	file.write_all(full_header.as_bytes())?;705	file.write_all(&r)?;706707	let abs_path = file.into_temp_path();708	let editor = std::env::var_os("VISUAL")709		.or_else(|| std::env::var_os("EDITOR"))710		.unwrap_or_else(|| "vi".into());711	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())712		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;713	let editor_args = editor_args714		.into_iter()715		.map(|v| {716			// Only ASCII subsequences are replaced717			unsafe { OsString::from_encoded_bytes_unchecked(v) }718		})719		.collect_vec();720	let Some((editor, args)) = editor_args.split_first() else {721		bail!("EDITOR env var has no command");722	};723	let mut command = Command::new(editor);724	command.args(args);725726	let path_arg = abs_path.canonicalize()?;727728	// TODO: Save full state, using tcget/_getmode/_setmode729	let was_raw = terminal::is_raw_mode_enabled()?;730	terminal::enable_raw_mode()?;731732	let status = command.arg(path_arg).status().await;733734	if !was_raw {735		terminal::disable_raw_mode()?;736	}737738	let success = match status {739		Ok(s) => s.success(),740		Err(e) if e.kind() == io::ErrorKind::NotFound => {741			bail!("editor not found")742		}743		Err(e) => bail!("editor spawn error: {e}"),744	};745746	let mut file = std::fs::read(&abs_path).context("read editor output")?;747	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {748		todo!();749	};750	todo!();751752	// Ok((success, abs_path))753}754*/
after · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	io::{self, Read, Write, stdin, stdout},4	path::PathBuf,5};67use anyhow::{Context, Result, anyhow, bail, ensure};8use chrono::{DateTime, Utc};9use clap::Parser;10use fleet_base::{11	fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},12	host::Config,13	opts::FleetOpts,14	secret::{Expectations, RegenerationReason, secret_needs_regeneration},15};16use fleet_shared::SecretData;17use nix_eval::{NixType, Value, nix_go, nix_go_json};18use serde::Deserialize;19use tabled::{Table, Tabled};20use tokio::{fs::read, task::spawn_blocking};21use tracing::{Instrument, error, info, info_span, warn};2223#[derive(Parser)]24pub enum Secret {25	AddManager,26	/// Force load host keys for all defined hosts27	ForceKeys,28	/// Read secret from remote host, requires sudo on one of the owning hosts29	Read {30		/// Secret name to read31		name: String,3233		/// Distribution with what machine to read34		/// If not shared between multiple - defaults to single owner35		#[clap(short = 'm', long)]36		machine: Option<String>,3738		/// Which private secret part to read39		#[clap(short = 'p', long, default_value = "secret")]40		part: String,4142		/// Which host should we use to decrypt, in case if reencryption is required, without43		/// regeneration44		#[clap(long)]45		prefer_identities: Vec<String>,46	},47	Regenerate {48		/// Which host should we use to decrypt, in case if reencryption is required, without49		/// regeneration50		#[clap(long)]51		prefer_identities: Vec<String>,52		/// Only regenerate shared secrets53		#[clap(long)]54		skip_hosts: bool,55	},56	List {},57	Edit {58		name: String,59		#[clap(short = 'm', long)]60		machine: String,6162		#[clap(long)]63		add: bool,6465		/// Which private secret part to read66		#[clap(short = 'p', long, default_value = "secret")]67		part: String,68	},69}7071/*72#[allow(clippy::too_many_arguments)]73#[tracing::instrument(skip(config, secret, definition, prefer_identities))]74async fn maybe_regenerate_shared_secret(75	secret_name: &str,76	config: &Config,77	mut secret: FleetSecretDistribution,78	definition: SharedSecretDefinition,79	prefer_identities: &[String],80	expectations: &Expectations,81) -> Result<FleetSecretDistribution> {82	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);83	let value = definition.definition_value();8485	let (should_reencrypt, reason) = match reason {86		Some(RegenerationReason::OwnersAdded(_)) => {87			// Secret always needs to be reencrypted for new owners to be able to read it88			(89				true,90				if nix_go_json!(value.regenerateOnOwnerAdded) {91					reason92				} else {93					None94				},95			)96		}97		Some(RegenerationReason::OwnersRemoved(_)) => {98			// No need to reencrypt, we can just leave stanzas in place.99			if nix_go_json!(value.regenerateOnOwnerRemoved) {100				(true, reason)101			} else {102				(false, None)103			}104		}105		Some(_) => (true, reason),106		None => (false, None),107	};108109	if let Some(reason) = reason {110		info!("secret needs to be regenerated: {reason}");111		let generated = generate_shared(config, secret_name, definition, expectations).await?;112		Ok(generated)113	} else if should_reencrypt {114		info!("secret needs to be reencrypted");115		let identity_holder = if !prefer_identities.is_empty() {116			prefer_identities117				.iter()118				.find(|i| secret.owners.iter().any(|s| s == *i))119		} else {120			secret.owners.first()121		};122		let Some(identity_holder) = identity_holder else {123			bail!("no available holder found");124		};125126		for (part_name, part) in secret.secret.parts.iter_mut() {127			let _span = info_span!("part reencryption", part_name);128			if !part.raw.encrypted {129				continue;130			}131			let host = config.host(identity_holder).await?;132			let encrypted = host133				.reencrypt(134					part.raw.clone(),135					expectations.owners.iter().cloned().collect(),136				)137				.await?;138			part.raw = encrypted;139		}140		secret.owners = expectations.owners.clone();141		Ok(secret)142	} else {143		Ok(secret)144	}145}146*/147148#[derive(Deserialize)]149#[serde(rename_all = "camelCase")]150enum GeneratorKind {151	Impure,152	Pure,153}154155async fn generate_pure(156	_config: &Config,157	_display_name: &str,158	_secret: Value,159	_default_generator: Value,160	_expectations: &Expectations,161) -> Result<FleetSecretData> {162	bail!("pure generators are broken for now")163}164async fn generate_impure(165	config: &Config,166	_display_name: &str,167	secret: Value,168	default_generator: Value,169	expectations: &Expectations,170) -> Result<FleetSecretData> {171	let generator = nix_go!(secret.generator);172	let on: Option<String> = nix_go_json!(default_generator.impureOn);173174	let nixpkgs = &config.nixpkgs;175176	let host = if let Some(on) = &on {177		config.host(on).await?178	} else {179		config.local_host()180	};181	let on_pkgs = host.pkgs().await?;182	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);183184	let mut recipients = Vec::new();185	for owner in &expectations.owners {186		let key = config.key(owner).await?;187		recipients.push(key);188	}189	let generators = nix_go!(mk_secret_generators(Obj { recipients }));190	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;191192	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));193194	let generator = nix_go!(call_package(generator)(Obj {}));195196	let generator = spawn_blocking(move || generator.build("out"))197		.await198		.expect("nix build shouldn't fail")?;199	let generator = host.remote_derivation(&generator).await?;200201	let out_parent = host.mktemp_dir().await?;202	let out = format!("{out_parent}/out");203204	let mut r#gen = host.cmd(generator).await?;205	r#gen.env("out", &out);206	if on.is_none() {207		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.208		let project_path: String = config209			.directory210			.clone()211			.into_os_string()212			.into_string()213			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;214		r#gen.env("FLEET_PROJECT", project_path);215	}216	r#gen.run().await.context("impure generator")?;217218	{219		let marker = host.read_file_text(format!("{out}/marker")).await?;220		ensure!(marker == "SUCCESS", "generation not succeeded");221	}222223	let mut parts = BTreeMap::new();224	for part in host.read_dir(&out).await? {225		if part == "created_at" || part == "expires_at" || part == "marker" {226			continue;227		}228		let contents: SecretData = host229			.read_file_text(format!("{out}/{part}"))230			.await?231			.parse()232			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;233		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });234	}235236	let created_at = host.read_file_value(format!("{out}/created_at")).await?;237	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();238239	let new_data = FleetSecretData {240		created_at,241		expires_at,242		parts,243		generation_data: expectations.generation_data.clone(),244	};245246	if let Some(reason) = secret_needs_regeneration(&new_data, &expectations.owners, expectations) {247		bail!("newly generated secret needs to be regenerated: {reason}")248	}249250	Ok(new_data)251}252253async fn generate(254	config: &Config,255	display_name: &str,256	secret: Value,257	expectations: &Expectations,258) -> Result<FleetSecretData> {259	let generator = nix_go!(secret.generator);260	// Can't properly check on nix module system level261	{262		let gen_ty = generator.type_of();263		if matches!(gen_ty, NixType::Null) {264			bail!("secret has no generator defined, can't automatically generate it.");265		}266		if matches!(gen_ty, NixType::Attrs) {267			if !generator.has_field("__functor")? {268				bail!("generator should be functor, got {gen_ty:?}");269			}270		} else if matches!(gen_ty, NixType::Function) {271			bail!("generator should be functor, got {gen_ty:?}");272		}273	}274	let nixpkgs = &config.nixpkgs;275	let default_pkgs = &config.default_pkgs;276	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);277	// Generators provide additional information in passthru, to access278	// passthru we should call generator, but information about where this generator is supposed to build279	// is located in passthru... Thus evaluating generator on host.280	//281	// Maybe it is also possible to do some magic with __functor?282	//283	// I don't want to make modules always responsible for additional secret data anyway,284	// so it should be in derivation, and not in the secret data itself.285	let generators = nix_go!(default_mk_secret_generators(Obj {286		recipients: <Vec<String>>::new(),287	}));288	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;289290	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));291	let default_generator = nix_go!(call_package(generator)(Obj {}));292293	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);294295	match kind {296		GeneratorKind::Impure => {297			generate_impure(298				config,299				display_name,300				secret,301				default_generator,302				expectations,303			)304			.await305		}306		GeneratorKind::Pure => {307			generate_pure(308				config,309				display_name,310				secret,311				default_generator,312				expectations,313			)314			.await315		}316	}317}318/*319async fn generate_shared(320	config: &Config,321	display_name: &str,322	secret: SharedSecretDefinition,323	expectations: &Expectations,324) -> Result<FleetSecretDistribution> {325	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);326	Ok(FleetSecretDistribution {327		managed: Some(true),328		secret: generate(329			config,330			display_name,331			secret.definition_value(),332			expectations,333		)334		.await?,335		owners: expectations.owners.clone(),336	})337}*/338339async fn parse_public(340	public: Option<String>,341	public_file: Option<PathBuf>,342) -> Result<Option<SecretData>> {343	Ok(match (public, public_file) {344		(Some(v), None) => Some(SecretData {345			data: v.into(),346			encrypted: false,347		}),348		(None, Some(v)) => Some(SecretData {349			data: read(v).await?,350			encrypted: false,351		}),352		(Some(_), Some(_)) => {353			bail!("only public or public_file should be set")354		}355		(None, None) => None,356	})357}358359async fn parse_secret() -> Result<Option<Vec<u8>>> {360	let mut input = vec![];361	stdin().read_to_end(&mut input)?;362	if input.is_empty() {363		Ok(None)364	} else {365		Ok(Some(input))366	}367}368369fn parse_machines(370	initial: BTreeSet<String>,371	machines: Option<Vec<String>>,372	mut add_machines: Vec<String>,373	mut remove_machines: Vec<String>,374) -> Result<BTreeSet<String>> {375	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {376		bail!("no operation");377	}378379	let initial_machines = initial.clone();380	let mut target_machines = initial;381	info!("Currently encrypted for {initial_machines:?}");382383	if let Some(machines) = machines {384		ensure!(385			add_machines.is_empty() && remove_machines.is_empty(),386			"can't combine --machines and --add-machines/--remove-machines"387		);388		let target = initial_machines.iter().collect::<HashSet<_>>();389		let source = machines.iter().collect::<HashSet<_>>();390		for removed in target.difference(&source) {391			remove_machines.push((*removed).clone());392		}393		for added in source.difference(&target) {394			add_machines.push((*added).clone());395		}396	}397398	for machine in &remove_machines {399		if !target_machines.remove(machine) {400			warn!("secret is not enabled for {machine}");401		}402	}403	for machine in &add_machines {404		if !target_machines.insert(machine.to_owned()) {405			warn!("secret is already added to {machine}");406		}407	}408	if !remove_machines.is_empty() {409		// TODO: maybe force secret regeneration?410		// Not that useful without revokation.411		warn!(412			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"413		);414	}415	Ok(target_machines)416}417impl Secret {418	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {419		match self {420			Secret::AddManager => {421				todo!("part of fleet-pusher")422			}423			Secret::ForceKeys => {424				for host in config.list_hosts().await? {425					if opts.should_skip(&host).await? {426						continue;427					}428					config.key(&host.name).await?;429				}430			}431			Secret::Read {432				name,433				machine,434				part: part_name,435				mut prefer_identities,436			} => {437				let Some(secret) = config.shared_secret(&name) else {438					bail!("secret doesn't exists");439				};440441				let dist = if secret.len() == 1 {442					&secret[0]443				} else if let Some(machine) = machine {444					let dist = secret.get(&machine);445					let Some(dist) = dist else {446						bail!("machine {machine} has no distribution of secret {name}");447					};448					prefer_identities.push(machine);449					dist450				} else {451					bail!(452						"secret {name} has shares, but no --machine specified for specifing which do you need"453					)454				};455456				let Some(part) = dist.secret.parts.get(&part_name) else {457					bail!("no part {part_name} in secret {name}");458				};459				let data = if part.raw.encrypted {460					let identity_holder = if !prefer_identities.is_empty() {461						prefer_identities462							.iter()463							.find(|i| dist.owners.iter().any(|s| s == *i))464					} else {465						dist.owners.first()466					};467					let Some(identity_holder) = identity_holder else {468						bail!("no available holder found");469					};470					let host = config.host(identity_holder).await?;471					host.decrypt(part.raw.clone()).await?472				} else {473					part.raw.data.clone()474				};475				stdout().write_all(&data)?;476			}477			Secret::Regenerate {478				prefer_identities,479				skip_hosts,480			} => {481				/*482								info!("checking for secrets to regenerate");483								let expected_shared_set = config484									.list_configured_shared()485									.await?486									.into_iter()487									.collect::<HashSet<_>>();488								let stored_shared_set = config.list_secrets().into_iter().collect::<HashSet<_>>();489								{490									// Generate missing shared491									let _span = info_span!("shared").entered();492									for missing in expected_shared_set.difference(&stored_shared_set) {493										let definition = config.shared_secret_definition(missing)?;494										if !definition.is_managed()? {495											info!("skipping unmanaged secret: {missing}");496											continue;497										}498										let expectations = definition499											.expectations()500											.with_context(|| format!("expectations for shared {missing:?}"))?;501										info!("generating secret: {missing}");502										let shared = generate_shared(config, missing, definition, &expectations)503											.in_current_span()504											.await?;505										config.replace_shared(missing.to_string(), shared)506									}507								}508								if !skip_hosts {509									for host in config.list_hosts().await? {510										if opts.should_skip(&host).await? {511											continue;512										}513514										let _span = info_span!("host", host = host.name).entered();515										let expected_set = host516											.list_defined_secrets()?517											.into_iter()518											.collect::<HashSet<_>>();519										let stored_set = config520											.list_secrets_for_owner(&host.name)521											.into_iter()522											.collect::<HashSet<_>>();523										for missing_secret in expected_set.difference(&stored_set) {524											let secret = host.secret_definition(missing_secret)?;525											if secret.is_shared()? {526												continue;527											}528											info!("generating missing secret: {missing_secret}");529											let expectations = secret.expectations().with_context(|| {530												format!("expectations for {missing_secret:?} of {:?}", host.name)531											})?;532											let generated = match generate(533												config,534												missing_secret,535												secret.definition_value()?,536												&expectations,537											)538											.in_current_span()539											.await540											{541												Ok(v) => v,542												Err(e) => {543													error!("{e:?}");544													continue;545												}546											};547											config.insert_secret(host.name, missing_secret.to_string(), generated)548										}549										for known_secret in stored_set.intersection(&expected_set) {550											let secret = host.secret_definition(known_secret)?;551											if secret.is_shared()? {552												continue;553											}554											info!("updating secret: {known_secret}");555											let data = config.host_secret(&host.name, known_secret)?;556											let expectations = secret.expectations()?;557											if let Some(regen_reason) = data.needs_regeneration(&expectations) {558												info!("needs regeneration: {regen_reason}");559												let generated = match generate(560													config,561													known_secret,562													secret.definition_value()?,563													&expectations,564												)565												.in_current_span()566												.await567												{568													Ok(v) => v,569													Err(e) => {570														error!("{e:?}");571														continue;572													}573												};574												config.insert_secret(575													&host.name,576													known_secret.to_string(),577													FleetLegacyHostSecret {578														managed: Some(true),579														secret: generated,580													},581												)582											}583										}584										for removed_secret in stored_set.difference(&expected_set) {585											let definition = host.secret_definition(removed_secret)?;586											if definition.is_shared()? {587												continue;588											}589											info!("removing secret: {removed_secret}");590											config.remove_secret(&host.name, removed_secret);591										}592									}593								}594								for known_secret in stored_shared_set.intersection(&expected_shared_set) {595									info!("updating shared secret: {known_secret}");596									let data = config.shared_secret(known_secret)?.expect("exists");597598									let definition = config.shared_secret_definition(known_secret)?;599									let expectations = definition.expectations()?;600									config.replace_shared(601										known_secret.to_owned(),602										maybe_regenerate_shared_secret(603											known_secret,604											config,605											data,606											definition,607											&prefer_identities,608											&expectations,609										)610										.await?,611									);612								}613								for removed_secret in stored_shared_set.difference(&expected_shared_set) {614									info!("removing shared secret: {removed_secret}");615									config.remove_shared(removed_secret);616								}617				*/618				todo!()619			}620			Secret::List {} => {621				let _span = info_span!("loading secrets").entered();622				let configured = config.list_configured_shared().await?;623				#[derive(Tabled)]624				struct SecretDisplay {625					#[tabled(rename = "Name")]626					name: String,627					#[tabled(rename = "Owners")]628					owners: String,629				}630				// let mut table = vec![];631				for name in configured.iter().cloned() {632					let config = config.clone();633					let data = config.shared_secret(&name).expect("exists");634					/*635					let definition = config.shared_secret_definition(&name)?;636					let expectations = definition.expectations()?;637					let owners = data638						.owners()639						.map(|o| {640							if expectations.owners.contains(o) {641								o.green().to_string()642							} else {643								o.red().to_string()644							}645						})646						.collect::<Vec<_>>();647					table.push(SecretDisplay {648						owners: owners.join(", "),649						name,650					})651*/652				}653				// info!("loaded\n{}", Table::new(table).to_string())654			}655			Secret::Edit {656				name,657				machine,658				part,659				add,660			} => {661				let secret = config662					.host_secret(&machine, &name)663					.context("secret not found")?;664				if let Some(data) = secret.secret.parts.get(&part) {665					let host = config.host(&machine).await?;666					let secret = host.decrypt(data.raw.clone()).await?;667					String::from_utf8(secret).context("secret is not utf8")?668				} else if add {669					String::new()670				} else {671					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");672				};673			}674		}675		Ok(())676	}677}678679/*680async fn edit_temp_file(681	builder: tempfile::Builder<'_, '_>,682	r: Vec<u8>,683	header: &str,684	comment: &str,685) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {686	if !stdin().is_tty() {687		// TODO: Also try to open /dev/tty directly?688		bail!("stdin is not tty, can't open editor");689	}690691	use std::fmt::Write;692	let mut file = builder.tempfile()?;693694	let mut full_header = String::new();695	let mut had = false;696	for line in header.trim_end().lines() {697		had = true;698		writeln!(&mut full_header, "{comment}{line}")?;699	}700	if had {701		writeln!(&mut full_header, "{}", comment.trim_end())?;702	}703	writeln!(704		&mut full_header,705		"{comment}Do not touch this header! It will be removed automatically"706	)?;707708	file.write_all(full_header.as_bytes())?;709	file.write_all(&r)?;710711	let abs_path = file.into_temp_path();712	let editor = std::env::var_os("VISUAL")713		.or_else(|| std::env::var_os("EDITOR"))714		.unwrap_or_else(|| "vi".into());715	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())716		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;717	let editor_args = editor_args718		.into_iter()719		.map(|v| {720			// Only ASCII subsequences are replaced721			unsafe { OsString::from_encoded_bytes_unchecked(v) }722		})723		.collect_vec();724	let Some((editor, args)) = editor_args.split_first() else {725		bail!("EDITOR env var has no command");726	};727	let mut command = Command::new(editor);728	command.args(args);729730	let path_arg = abs_path.canonicalize()?;731732	// TODO: Save full state, using tcget/_getmode/_setmode733	let was_raw = terminal::is_raw_mode_enabled()?;734	terminal::enable_raw_mode()?;735736	let status = command.arg(path_arg).status().await;737738	if !was_raw {739		terminal::disable_raw_mode()?;740	}741742	let success = match status {743		Ok(s) => s.success(),744		Err(e) if e.kind() == io::ErrorKind::NotFound => {745			bail!("editor not found")746		}747		Err(e) => bail!("editor spawn error: {e}"),748	};749750	let mut file = std::fs::read(&abs_path).context("read editor output")?;751	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {752		todo!();753	};754	todo!();755756	// Ok((success, abs_path))757}758*/
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -23,7 +23,6 @@
 use crate::{
 	command::MyCommand,
 	fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},
-	secret::{HostSecretDefinition, SharedSecretDefinition},
 };
 
 pub struct FleetConfigInternals {
@@ -31,7 +30,7 @@
 	pub directory: PathBuf,
 	/// builtins.currentSystem
 	pub local_system: String,
-	pub data: Mutex<FleetData>,
+	pub data: Arc<Mutex<FleetData>>,
 	pub nix_args: Vec<OsString>,
 	/// fleet_config.config
 	pub config_field: Value,
@@ -520,13 +519,6 @@
 		let nixos = self.nixos_unchecked_config()?;
 		let secrets = nix_go!(nixos.secrets);
 		secrets.list_fields()
-	}
-	pub fn secret_definition(&self, name: &str) -> Result<HostSecretDefinition> {
-		let nixos = self.nixos_unchecked_config()?;
-		Ok(HostSecretDefinition(
-			self.name.clone(),
-			nix_go!(nixos.secrets[{ name }]),
-		))
 	}
 
 	/// Packages for this host, resolved with nixpkgs overlays
@@ -665,12 +657,6 @@
 	pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {
 		let data = self.data();
 		data.secrets.get(secret).cloned()
-	}
-	pub fn shared_secret_definition(&self, secret: &str) -> Result<SharedSecretDefinition> {
-		let config_field = &self.config_field;
-		Ok(SharedSecretDefinition(nix_go!(
-			config_field.sharedSecrets[{ secret }]
-		)))
 	}
 
 	// TODO: Should this be something modifiable from other processes?
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -211,7 +211,7 @@
 		}
 		let bytes =
 			std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
-		let data = Mutex::new(FleetData::from_str(&bytes)?);
+		let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));
 
 		let mut fetch_settings = FetchSettings::new();
 		fetch_settings.set(c"warn-dirty", c"false");
@@ -239,8 +239,7 @@
 		let builtins_field = Value::eval("builtins")?;
 
 		let fleet_root = flake.get_field("fleetConfigurations")?;
-		let data_val = Value::serialized(&data)?;
-		let fleet_field = nix_go!(fleet_root.default(data_val));
+		let fleet_field = nix_go!(fleet_root.default(Obj {}));
 
 		let config_field = nix_go!(fleet_field.config);
 
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,4 +1,9 @@
-use nix_eval::NativeFn;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+use nix_eval::{NativeFn, Value};
+
+use crate::fleetdata::{FleetData, FleetSecrets};
 
 #[derive(thiserror::Error, Debug)]
 enum Error {}
@@ -15,30 +20,32 @@
 	fn host_parts(&self, host: &str, name: &str) -> Parts;
 }
 
-struct FsSecretsBackend {
+struct FsSecretsBackend {}
 
-}
-
-pub fn init_primops() {
+pub fn init_primops(secrets: Arc<Mutex<FleetData>>) {
 	NativeFn::new(
-		c"fleet_ensure_secret",
+		c"fleet_ensure_host_secret",
 		c"Ensure secret existence for a host, regenerating it in case of some mismatch",
-		[
-			c"host",
-			c"secret",
-			c"expected_parts",
-			c"expected_encrypted_parts",
-			c"generator",
-		],
-		|[
-			host,
-			secret,
-			expected_parts,
-			expected_encrypted_parts,
-			generator,
-		]| { 
-
-			todo!()
+		[c"host", c"secret", c"generator"],
+		|[host, secret, generator]| {
+			todo!("ensure secret");
+			Ok(Value::new_attrs(HashMap::from_iter([(
+				"raw",
+				Value::new_str("rawData"),
+			)])))
+		},
+	)
+	.register();
+	NativeFn::new(
+		c"fleet_ensure_host_secret",
+		c"Ensure secret existence for a host, regenerating it in case of some mismatch",
+		[c"host", c"secret", c"generator"],
+		|[host, secret, generator]| {
+			todo!("ensure secret");
+			Ok(Value::new_attrs(HashMap::from_iter([(
+				"raw",
+				Value::new_str("rawData"),
+			)])))
 		},
 	)
 	.register();
modifiedcrates/fleet-base/src/secret.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,8 +1,6 @@
 use std::collections::BTreeSet;
 
-use anyhow::Result;
 use chrono::{DateTime, Utc};
-use nix_eval::{Value, nix_go, nix_go_json};
 
 use crate::fleetdata::FleetSecretData;
 
@@ -12,63 +10,6 @@
 	pub generation_data: serde_json::Value,
 	pub public_parts: BTreeSet<String>,
 	pub private_parts: BTreeSet<String>,
-}
-
-pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);
-impl HostSecretDefinition {
-	pub fn is_managed(&self) -> Result<bool> {
-		let def = self.definition_value()?;
-		Ok(!nix_go!(def.generator).is_null())
-	}
-	pub fn is_shared(&self) -> Result<bool> {
-		let def = self.definition_value()?;
-		Ok(nix_go_json!(def.shared))
-	}
-	pub fn expectations(&self) -> Result<Expectations> {
-		let def = self.definition_value()?;
-		let parts = nix_go!(def.parts);
-
-		let mut public_parts = BTreeSet::new();
-		let mut private_parts = BTreeSet::new();
-		for part in parts.list_fields()? {
-			if nix_go_json!(parts[&part].encrypted) {
-				private_parts.insert(part.clone());
-			} else {
-				public_parts.insert(part.clone());
-			}
-		}
-
-		Ok(Expectations {
-			owners: BTreeSet::from([self.0.clone()]),
-			generation_data: nix_go_json!(def.expectedGenerationData),
-			public_parts,
-			private_parts,
-		})
-	}
-	pub fn definition_value(&self) -> Result<Value> {
-		let value = &self.1;
-		Ok(nix_go!(value.definition))
-	}
-}
-
-pub struct SharedSecretDefinition(pub(crate) Value);
-impl SharedSecretDefinition {
-	pub fn is_managed(&self) -> Result<bool> {
-		let value = &self.0;
-		Ok(!nix_go!(value.generator).is_null())
-	}
-	pub fn expectations(&self) -> Result<Expectations> {
-		let value = &self.0;
-		Ok(Expectations {
-			owners: nix_go_json!(value.expectedOwners),
-			generation_data: nix_go_json!(value.expectedGenerationData),
-			public_parts: nix_go_json!(value.expectedPublicParts),
-			private_parts: nix_go_json!(value.expectedPrivateParts),
-		})
-	}
-	pub fn definition_value(&self) -> Value {
-		self.0.clone()
-	}
 }
 
 #[derive(thiserror::Error, Debug)]
modifiedmodules/module-list.nixdiffbeforeafterboth
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -6,5 +6,4 @@
   ./nixos.nix
   ./nixpkgs.nix
   ./secrets.nix
-  ./secrets-data.nix
 ]
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -8,50 +8,26 @@
 let
   inherit (builtins)
     hashString
-    elemAt
-    length
     toJSON
-    filter
     ;
   inherit (lib.stringsWithDeps) stringAfter;
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.lists) optional;
-  inherit (lib.attrsets) mapAttrs mapAttrsToList;
-  inherit (lib.modules) mkIf mkMerge;
+  inherit (lib.attrsets) mapAttrs;
+  inherit (lib.modules) mkIf;
   inherit (lib.types)
     submodule
     str
     attrsOf
     nullOr
     unspecified
-    lazyAttrsOf
     uniq
     functionTo
     package
-    listOf
-    bool
     ;
   inherit (fleetLib.strings) decodeRawSecret;
 
   sysConfig = config;
-  secretPartDataType = submodule {
-    options = {
-      raw = mkOption {
-        type = str;
-        internal = true;
-        description = "Encoded & Encrypted secret part data, passed from fleet.nix";
-      };
-    };
-  };
-  secretDataType = submodule {
-    freeformType = lazyAttrsOf secretPartDataType;
-    options = {
-      shared = mkOption {
-        description = "Is this secret owned by this machine, or propagated from shared secrets";
-        default = false;
-      };
-    };
-  };
   secretPartType =
     secretName:
     submodule (
@@ -61,11 +37,6 @@
       in
       {
         options = {
-          encrypted = mkOption {
-            type = bool;
-            description = "Is this secret part supposed to be encrypted?";
-          };
-
           hash = mkOption {
             type = str;
             description = "Hash of secret in encoded format";
@@ -82,17 +53,17 @@
             type = str;
             description = "Secret public data (only available for plaintext)";
           };
+          raw = mkOption {
+            type = str;
+            description = "Raw (encoded/encrypted secret part data)";
+          };
+        };
+        config = {
+          hash = hashString "sha1" config.raw;
+          data = decodeRawSecret config.raw;
+          path = "/run/secrets/${secretName}/${config.hash}-${partName}";
+          stablePath = "/run/secrets/${secretName}/${partName}";
         };
-        config =
-          let
-            raw = sysConfig.data.secrets.${secretName}.${partName}.raw;
-          in
-          {
-            hash = hashString "sha1" raw;
-            data = decodeRawSecret raw;
-            path = "/run/secrets/${secretName}/${config.hash}-${partName}";
-            stablePath = "/run/secrets/${secretName}/${partName}";
-          };
       }
     );
   secretType = submodule (
@@ -105,14 +76,9 @@
     in
     {
       options = {
-        shared = mkOption {
-          type = bool;
-          description = "Was this secret propagated from a shared secret?";
-        };
         parts = mkOption {
-          type = lazyAttrsOf (secretPartType secretName);
+          type = attrsOf (secretPartType secretName);
           description = "Definition of secret parts";
-          default = { };
         };
         generator = mkOption {
           type = uniq (nullOr (functionTo package));
@@ -134,47 +100,14 @@
           description = "Group of the secret";
           default = sysConfig.users.users.${config.owner}.group;
           defaultText = literalExpression "config.users.users.$${owner}.group";
-        };
-        expectedGenerationData = mkOption {
-          type = unspecified;
-          description = "Data that gets embedded into secret part";
-          default = null;
         };
       };
       config = {
-        shared = (sysConfig.data.secrets.${secretName} or { shared = false; }).shared;
-        parts = mkMerge [
-          (mkIf (config.generator != null)
-            (
-              # Get fake derivation body, in future it should be implemented the same way as in Rust.
-              lib.callPackageWith (
-                pkgs
-                // {
-                  mkSecretGenerator = pkgs.stdenv.mkDerivation;
-                  mkImpureSecretGenerator = pkgs.stdenv.mkDerivation;
-                }
-              ) config.generator { }
-            ).parts
-          )
-          (mapAttrs (_: _: { }) (
-            removeAttrs (sysConfig.data.secrets.${secretName} or { }) [
-              "shared"
-              "managed"
-            ]
-          ))
-        ];
+        parts = builtins.fleet_ensure_host_secret sysConfig.networking.hostName secretName config.generator;
       };
     }
   );
-  processPart = secretName: partName: part: {
-    inherit (part) path stablePath;
-    raw = config.data.secrets.${secretName}.${partName}.raw;
-  };
-  processSecret = secretName: secret: {
-    inherit (secret.definition) group mode owner;
-    parts = (mapAttrs (processPart secretName) (secret.definition.parts));
-  };
-  secretsData = (mapAttrs (processSecret) config.secrets);
+  secretsData = (mapAttrs (_: s: s.definition) config.secrets);
   secretsFile = pkgs.writeTextFile {
     name = "secrets.json";
     text = toJSON secretsData;
@@ -185,11 +118,6 @@
 in
 {
   options = {
-    data.secrets = mkOption {
-      type = attrsOf secretDataType;
-      default = { };
-      description = "Host-local secret data";
-    };
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
deletedmodules/secrets-data.nixdiffbeforeafterboth
--- a/modules/secrets-data.nix
+++ /dev/null
@@ -1,95 +0,0 @@
-{
-  lib,
-  fleetLib,
-  ...
-}:
-let
-  inherit (fleetLib.options) mkDataOption;
-  inherit (lib.options) mkOption;
-  inherit (lib.types)
-    nullOr
-    listOf
-    str
-    attrsOf
-    submodule
-    bool
-    unspecified
-    ;
-
-  secretDataValue = {
-    options = {
-      raw = mkOption {
-        type = nullOr str;
-        description = "Raw secret data in unspecified encoded and optionally encrypted format.";
-        default = null;
-      };
-    };
-  };
-
-  sharedSecretData = {
-    freeformType = attrsOf (submodule secretDataValue);
-    options = {
-      managed = mkOption {
-        type = nullOr bool;
-        description = "Is current fleet data value is generated by generator";
-        default = null;
-      };
-
-      createdAt = mkOption {
-        type = str;
-        description = "Timestamp of secret generation/last rotation.";
-        default = null;
-      };
-      expiresAt = mkOption {
-        type = nullOr str;
-        description = "Expiration timestamp triggering mandatory secret rotation.";
-        default = null;
-      };
-
-      owners = mkOption {
-        type = listOf str;
-        description = ''
-          List of hosts currently authorized to decrypt this shared secret.
-
-          If owners differ from expected owners, the secret is considered outdated
-          and requires regeneration or re-encryption.
-        '';
-        default = [ ];
-      };
-      generationData = mkOption {
-        type = unspecified;
-        description = "Contextual metadata associated with secret part.";
-        default = null;
-      };
-    };
-  };
-
-  managerKey = {
-    options = {
-      name = mkOption {
-        type = str;
-        description = "Who does this manager key belongs to.";
-      };
-      key = mkOption {
-        type = str;
-        description = "Age-compatible key";
-      };
-    };
-    config = { };
-  };
-in
-{
-  options.data = mkDataOption ({ config, ... }:
-    {
-      options = {
-        managerKeys = mkOption {
-          type = listOf (submodule managerKey);
-        };
-        secrets = mkOption {
-          type = attrsOf (listOf submodule sharedSecretData);
-          default = { };
-          description = "Shared secret data.";
-        };
-      };
-    });
-}
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -5,7 +5,6 @@
 let
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.types)
-    unspecified
     nullOr
     listOf
     str
@@ -66,22 +65,7 @@
             An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,
             use `mkSecretGenerator` helpers to implement own generators.
           '';
-          default = null;
-        };
-        expectedGenerationData = mkOption {
-          type = unspecified;
-          description = "Contextual metadata embedded within the secret part value";
           default = null;
-        };
-        expectedPrivateParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be encrypted";
-        };
-        expectedPublicParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be public";
         };
       };
     };