git.delta.rocks / fleet / refs/commits / dbc4d8aea22e

difftreelog

source

crates/fleet-base/src/primops.rs11.6 KiBsourcehistory
1use std::collections::{BTreeMap, BTreeSet, HashMap};2use std::sync::OnceLock;34use anyhow::{Context, bail, ensure};5use fleet_shared::SecretData;6use itertools::Itertools;7use nix_eval::{NativeFn, Value, await_in_nix, nix_go, nix_go_json};8use serde::Deserialize;9use tracing::{info, warn};1011use crate::fleetdata::{12	Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,13	RegenerationConstraints, SecretOwner,14};15use crate::host::{Config, ConfigHost};16use anyhow::{Result, anyhow};1718pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();1920#[derive(Deserialize)]21#[serde(rename_all = "camelCase")]22enum GeneratorKind {23	Impure,24	Pure,25}2627pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {28	let pkgs = host_on.pkgs()?;29	let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);30	let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));31	Ok(pkgs.clone().attrs_update(generators)?)32}33pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {34	let host_on = config.local_host();35	get_pkgs_and_generators(&host_on, vec![])36}37pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result<Value> {38	ensure!(39		package.is_function(),40		"package should be a function to be called with callPackage"41	);42	// No need to use nixpkgs.buildUsing, as only nixpkgs-lib is used.43	let nixpkgs = &config.nixpkgs;44	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs));45	Ok(nix_go!(call_package(package)(Obj {})))46}4748pub fn get_default_generator_drv(config: &Config, generator: &Value) -> Result<Value> {49	let default_pkgs_and_generators = get_default_pkgs_and_generators(config)?;50	let default_generator_drv = call_package(config, &default_pkgs_and_generators, generator)51		.context("failed to initialize generator to get metadata")?;5253	Ok(default_generator_drv)54}5556fn secret_to_parts(57	secret_name: &str,58	secret: &BTreeMap<String, FleetSecretPart>,59	expected: &BTreeMap<String, GeneratorPart>,60) -> Value {61	let mut out = HashMap::new();62	for (part_name, part) in secret {63		if !expected.contains_key(part_name) {64			warn!(65				"secret {secret_name} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix"66			);67			continue;68		};69		out.insert(70			part_name.as_str(),71			Value::new_attrs(HashMap::from_iter([(72				"raw",73				Value::new_str(&part.raw.to_string()),74			)])),75		);76	}7778	Value::new_attrs(out)79}8081pub async fn generate(82	config: &Config,83	expectations: Expectations,84	generator: &Value,85	default_generator_drv: &Value,86) -> Result<FleetSecretDistribution> {87	let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind);8889	match kind {90		GeneratorKind::Impure => {91			let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);9293			let host_on = if let Some(on) = &impure_on {94				config95					.host(on)96					.context("failed to get secret generation target host")?97			} else {98				config.local_host()99			};100			let mut recipients = Vec::new();101			for owner in &expectations.owners {102				recipients.push(config.key(owner).await?);103			}104			let pkgs_and_generators = get_pkgs_and_generators(&host_on, recipients)105				.context("failed to get pkgs for target host")?;106			let generator = call_package(config, &pkgs_and_generators, generator)107				.context("failed to evaluate generator for target host")?;108109			let generator = generator110				.build("out")111				.context("failed to build generator for target host")?;112113			let generator = host_on114				.remote_derivation(&generator)115				.await116				.context("failed to copy generator to target host")?;117118			// TODO: Remove destdir after everything is done119			let out_parent = host_on120				.mktemp_dir()121				.await122				.context("failed to prepare generator output dir on target host")?;123			let out = format!("{out_parent}/out");124			let mut generator_cmd = host_on.cmd(generator).await?;125			generator_cmd.env("out", &out);126			if impure_on.is_none() {127				let project_path: String = config128					.directory129					.clone()130					.into_os_string()131					.into_string()132					.map_err(|e| anyhow!("fleet project path is not utf-8: {e:?}"))?;133				generator_cmd.env("FLEET_PROJECT", project_path);134			};135			generator_cmd136				.run()137				.await138				.context("failed to run impure generator")?;139140			{141				let marker = host_on.read_file_text(format!("{out}/marker")).await?;142				ensure!(143					marker == "SUCCESS",144					"impure generator ended prematurely, secret generation failed"145				);146			}147148			let mut missing_parts = expectations.parts.clone();149			let mut parts = BTreeMap::new();150			for part in host_on.read_dir(&out).await? {151				if part == "created_at" || part == "expires_at" || part == "marker" {152					continue;153				}154				let Some(part_def) = missing_parts.remove(&part) else {155					bail!("secret generator has produced an unexpected part: {part}");156				};157				let contents: SecretData = host_on158					.read_file_text(format!("{out}/{part}"))159					.await?160					.parse()161					.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;162163				ensure!(164					contents.encrypted == part_def.encrypted,165					"part {part} produced by generator is supposed to be {}, but it was not",166					if part_def.encrypted {167						"encrypted"168					} else {169						"plaintext"170					}171				);172173				parts.insert(part.to_owned(), FleetSecretPart { raw: contents });174			}175			if !missing_parts.is_empty() {176				bail!(177					"the secret generator has not produced the following expected parts: {missing_parts:?}"178				);179			}180181			let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;182			let expires_at = host_on183				.read_file_value(format!("{out}/expires_at"))184				.await185				.ok();186187			let new_data = FleetSecretData {188				created_at,189				expires_at,190				parts,191				generation_data: expectations.generation_data.clone(),192			};193194			let new_data =195				FleetSecretDistribution::new(expectations.owners.clone(), new_data, config.now);196197			Ok(new_data)198		}199		GeneratorKind::Pure => {200			bail!("pure generators are disabled for now")201		}202	}203}204205pub fn init_primops() {206	NativeFn::new(207		c"__fleetEnsureHostSecrets",208		c"Ensure no extra secrets are stored for the host, pruning unknown",209		[c"host", c"expectedNonshared", c"expectedShared", c"rest"],210		|_es, [host, expected_nonshared, expected_shared, rest]| {211			let host = SecretOwner::host(host.to_string()?);212			let expected_nonshared: BTreeSet<String> = expected_nonshared.as_json()?;213			let expected_shared: BTreeSet<String> = expected_shared.as_json()?;214215			let mut expected = expected_nonshared;216			expected.extend(expected_shared);217218			let config = PRIMOPS_DATA219				.get()220				.expect("primops data should be set on init");221222			config223				.data224				.secrets225				.write()226				.expect("no poisoning")227				.prune_host(&host, expected);228229			Ok(rest.clone())230		},231	)232	.register();233	NativeFn::new(234		c"__fleetEnsureHostSecret",235		c"Ensure secret existence for a host, regenerating it in case of some mismatch",236		[c"host", c"secret", c"generator"],237		|es, [host, secret, generator]| {238			let host = SecretOwner::host(&host.to_string()?);239			let secret = secret.to_string()?;240241			let config = PRIMOPS_DATA242				.get()243				.expect("primops data should be set on init");244245			let shared_def = config.secret_definition(&secret).context("failed to get shared secret definition")?;246247			let (shared, generator, expected_owners) = if generator.is_string() {248				assert_eq!(generator.to_string()?, "shared", "asserted by nixos type system");249				let Some(shared_def) = shared_def else {250					bail!("secret {secret} is defined on host {host} as shared, but there is no shared secret with same name defined at fleetConfiguration.secrets.{secret}.generator")251				};252				let expected_owners = shared_def.expected_owners()?;253254				ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");255256				(Some(shared_def.clone()), shared_def.generator()?, expected_owners)257			} else {258				if shared_def.is_some() {259					bail!("hosts can only have their own generators for non-shared secrets, either set host secret generator to \"shared\", or remove shared secret generator at fleetConfiguration.secrets.{secret}.generator")260				}261262				(None, generator.clone(), BTreeSet::from_iter([host.clone()]))263			};264265			let default_generator_drv = get_default_generator_drv(config, &generator)?;266			let mut expectations = Expectations {267				parts: nix_go_json!(default_generator_drv.parts),268				generation_data: nix_go_json!(default_generator_drv.generationData),269				owners: expected_owners.clone(),270			};271			let constraints = if let Some(shared) = &shared{272				RegenerationConstraints {273					allow_different: nix_go_json!(default_generator_drv.allowDifferent) && shared.allow_different()?,274					regenerate_on_owner_added: shared.regenerate_on_owner_added()?,275					regenerate_on_owner_removed: shared.regenerate_on_owner_added()?,276				}277			} else {278				RegenerationConstraints::host_personal()279			};280281			let mut secrets = config.data.secrets.write().expect("no poisoning");282			let dists = secrets.get_or_create(&secret);283284				if shared.is_some() {285					dists.prune_shared(&expected_owners, !constraints.allow_different, &expectations.parts, &expectations.generation_data, constraints.regenerate_on_owner_removed, constraints.regenerate_on_owner_added, &config.prefer_identities, config.now);286				} else {287					dists.prune_host(host.clone(), &expectations.parts, &expectations.generation_data, config.now);288				};289290				if let Some(dist) = dists.get(&host) {291					return Ok(secret_to_parts(&secret, &dist.secret.parts, &expectations.parts));292				};293294				let mut reencrypt_targets = expectations.owners.clone();295				for dist in dists.distributions() {296					for own in dist.owners() {297						reencrypt_targets.remove(own);298					}299				}300				if !constraints.regenerate_on_owner_added {301					if let Some(unpruned) = dists.try_unprune(host.clone()) {302						return Ok(secret_to_parts(&secret, &unpruned.secret.parts, &expectations.parts));303					} else if let Some(best) = dists.best_distribution_for_reencryption(&config.prefer_identities) {304						let new_owners = reencrypt_targets.clone();305						let mut reencrypt_targets = reencrypt_targets;306						reencrypt_targets.extend(best.owners().cloned());307308						let mut preferred = best.owners().collect_vec();309						preferred.sort_by_key(|v| !config.prefer_identities.contains(*v));310311						warn!("reencrypting secret {secret} as it is missing for host {host}");312313						for owner in preferred {314							if let Some(hostname) = owner.as_host() && let Ok(host) = config.host(hostname) {315								let best = best.clone();316								let reencrypt_targets = reencrypt_targets.clone();317								let reencrypted = match await_in_nix(async move {318										host.reencrypt_distribution(&best, reencrypt_targets.clone(), config.now).await319								}) {320									Ok(r) => r,321									Err(e) => {322										warn!("reencryption failed on {hostname}: {e:?}");323										continue;324									}325								};326								dists.extend(reencrypted.clone(), format!("secret was reencrypted to extend with new owners: {new_owners:?}"));327								return Ok(secret_to_parts(&secret, &reencrypted.secret.parts, &expectations.parts));328							};329						}330						warn!("failed to reencrypt using any host")331					};332				};333334			if constraints.allow_different {335				for dist in dists.distributions() {336					for own in dist.owners() {337						expectations.owners.remove(own);338					}339				}340			}341			info!("secret {secret} is being generated for {:?}", expectations.owners);342343			let expectations_ = expectations.clone();344			let generated = await_in_nix(async move {345				generate(config, expectations_, &generator, &default_generator_drv).await346			})?;347348			dists.extend(generated.clone(), "secret was generated".to_string());349350			Ok(secret_to_parts(&secret, &generated.secret.parts, &expectations.parts))351		},352	)353	.register();354}