git.delta.rocks / jrsonnet / refs/commits / 69498f520d8e

difftreelog

feat on-demand secret generation

rxzprzppYaroslav Bolyukin2026-01-22parent: #faec707.patch.diff
in: trunk

24 files changed

modifiedCargo.lockdiffbeforeafterboth
1149 "base64 0.22.1",1149 "base64 0.22.1",
1150 "serde",1150 "serde",
1151 "unicode_categories",1151 "unicode_categories",
1152 "z85",
1153]1152]
11541153
1155[[package]]1154[[package]]
2089 "serde_json",2088 "serde_json",
2090 "test-log",2089 "test-log",
2091 "thiserror 2.0.17",2090 "thiserror 2.0.17",
2091 "tokio",
2092 "tracing",2092 "tracing",
2093 "tracing-indicatif",2093 "tracing-indicatif",
2094 "vte 0.15.0",2094 "vte 0.15.0",
4639 "synstructure",4639 "synstructure",
4640]4640]
4641
4642[[package]]
4643name = "z85"
4644version = "3.0.6"
4645source = "registry+https://github.com/rust-lang/crates.io-index"
4646checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64"
46474641
4648[[package]]4642[[package]]
4649name = "zerocopy"4643name = "zerocopy"
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
7 host::{Config, DeployKind, GenerationStorage},7 host::{Config, DeployKind, GenerationStorage},
8 opts::FleetOpts,8 opts::FleetOpts,
9};9};
10use futures::{StreamExt as _, stream::FuturesUnordered};
10use nix_eval::nix_go;11use nix_eval::nix_go;
11use tokio::task::{LocalSet, spawn_blocking};12use tokio::task::spawn_blocking;
12use tracing::{Instrument, error, field, info, info_span, warn};13use tracing::{Instrument, error, field, info, info_span, warn};
1314
14#[derive(Parser)]15#[derive(Parser)]
47 "--profile",48 "--profile",
48 format!(49 format!(
49 "/nix/var/nix/profiles/{}-{hostname}",50 "/nix/var/nix/profiles/{}-{hostname}",
50 config.data().gc_root_prefix51 config.data.gc_root_prefix
51 ),52 ),
52 )53 )
53 .arg(&out_output);54 .arg(&out_output);
60impl BuildSystems {61impl BuildSystems {
61 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {62 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
62 let hosts = opts.filter_skipped(config.list_hosts()?)?;63 let hosts = opts.filter_skipped(config.list_hosts()?)?;
63 let set = LocalSet::new();64 let mut tasks = FuturesUnordered::new();
64 let build_attr = self.build_attr.clone();65 let build_attr = self.build_attr.clone();
65 for host in hosts {66 for host in hosts {
66 let config = config.clone();67 let config = config.clone();
67 let span = info_span!("build", host = field::display(&host.name));68 let span = info_span!("build", host = field::display(&host.name));
68 let hostname = host.name;69 let hostname = host.name;
69 let build_attr = build_attr.clone();70 let build_attr = build_attr.clone();
70 set.spawn_local(71 tasks.push(
71 (async move {72 (async move {
72 let built = match build_task(config, hostname.clone(), &build_attr).await {73 let built = match build_task(config, hostname.clone(), &build_attr).await {
73 Ok(path) => path,74 Ok(path) => path,
88 .instrument(span),89 .instrument(span),
89 );90 );
90 }91 }
91 set.await;92 for _task in tasks.next().await {}
92 Ok(())93 Ok(())
93 }94 }
94}95}
9596
96impl Deploy {97impl Deploy {
97 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {98 pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
98 let hosts = opts.filter_skipped(config.list_hosts()?)?;99 let hosts = opts.filter_skipped(config.list_hosts()?)?;
99 let set = LocalSet::new();100 let mut tasks = FuturesUnordered::new();
100 for host in hosts.into_iter() {101 for host in hosts.into_iter() {
101 let config = config.clone();102 let config = config.clone();
102 let span = info_span!("deploy", host = field::display(&host.name));103 let span = info_span!("deploy", host = field::display(&host.name));
112 host.set_legacy_ssh_store(legacy);113 host.set_legacy_ssh_store(legacy);
113 };114 };
114115
115 set.spawn_local(116 tasks.push(
116 (async move {117 (async move {
117 let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")118 let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")
118 .await119 .await
170 .instrument(span),171 .instrument(span),
171 );172 );
172 }173 }
173 set.await;174 for _task in tasks.next().await {}
174 Ok(())175 Ok(())
175 }176 }
176}177}
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
56 .collect::<HashSet<_>>();56 .collect::<HashSet<_>>();
57 let mut stored_locally = config57 let mut stored_locally = config
58 .local_host()58 .local_host()
59 .list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))59 .list_generations(&format!("{}-{}", config.data.gc_root_prefix, host.name))
60 .await60 .await
61 .inspect_err(|e| {61 .inspect_err(|e| {
62 warn!("failed to list generations available locally: {e}");62 warn!("failed to list generations available locally: {e}");
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
412 if opts.should_skip(&host)? {412 if opts.should_skip(&host)? {
413 continue;413 continue;
414 }414 }
415 config.key(&host.name).await?;415 config.host_key(&host.name).await?;
416 }416 }
417 }417 }
418 Secret::Read {418 Secret::Read {
421 part: part_name,421 part: part_name,
422 mut prefer_identities,422 mut prefer_identities,
423 } => {423 } => {
424 let Some(secret) = config.shared_secret(&name) else {424 /*
425 bail!("secret doesn't exists");425 let Some(secret) = config.shared_secret(&name) else {
426 };426 bail!("secret doesn't exists");
427427 };
428 let dist = if secret.len() == 1 {428
429 &secret[0]429 let dist = if secret.len() == 1 {
430 } else if let Some(machine) = machine {430 &secret[0]
431 let dist = secret.get(&machine);431 } else if let Some(machine) = machine {
432 let Some(dist) = dist else {432 let dist = secret.get(&machine);
433 bail!("machine {machine} has no distribution of secret {name}");433 let Some(dist) = dist else {
434 };434 bail!("machine {machine} has no distribution of secret {name}");
435 prefer_identities.push(machine);435 };
436 dist436 prefer_identities.push(machine);
437 } else {437 dist
438 bail!(438 } else {
439 "secret {name} has shares, but no --machine specified for specifing which do you need"439 bail!(
440 )440 "secret {name} has shares, but no --machine specified for specifing which do you need"
441 };441 )
442442 };
443 let Some(part) = dist.secret.parts.get(&part_name) else {443
444 bail!("no part {part_name} in secret {name}");444 let Some(part) = dist.secret.parts.get(&part_name) else {
445 };445 bail!("no part {part_name} in secret {name}");
446 let data = if part.raw.encrypted {446 };
447 let data = if part.raw.encrypted {
448 let identity_holder = if !prefer_identities.is_empty() {
449 prefer_identities
450 .iter()
451 .find(|i| dist.owners.iter().any(|s| s == *i))
452 } else {
453 dist.owners.first()
454 };
455 let Some(identity_holder) = identity_holder else {
456 bail!("no available holder found");
457 };
458 let host = config.host(identity_holder)?;
459 host.decrypt(part.raw.clone()).await?
460 } else {
461 part.raw.data.clone()
462 };
463 stdout().write_all(&data)?;
464 */
447 let identity_holder = if !prefer_identities.is_empty() {465 todo!()
448 prefer_identities
449 .iter()
450 .find(|i| dist.owners.iter().any(|s| s == *i))
451 } else {
452 dist.owners.first()
453 };
454 let Some(identity_holder) = identity_holder else {
455 bail!("no available holder found");
456 };
457 let host = config.host(identity_holder)?;
458 host.decrypt(part.raw.clone()).await?
459 } else {
460 part.raw.data.clone()
461 };
462 stdout().write_all(&data)?;
463 }466 }
464 Secret::Regenerate {467 Secret::Regenerate {
465 prefer_identities,468 prefer_identities,
605 todo!()608 todo!()
606 }609 }
607 Secret::List {} => {610 Secret::List {} => {
611 /*
612 let _span = info_span!("loading secrets").entered();
613 let configured = config.list_configured_shared()?;
614 #[derive(Tabled)]
615 struct SecretDisplay {
616 #[tabled(rename = "Name")]
617 name: String,
618 #[tabled(rename = "Owners")]
619 owners: String,
620 }
621 // let mut table = vec![];
622 for name in configured.iter().cloned() {
623 let config = config.clone();
624 let data = config.shared_secret(&name).expect("exists");
625 /*
626 let definition = config.shared_secret_definition(&name)?;
627 let expectations = definition.expectations()?;
628 let owners = data
629 .owners()
630 .map(|o| {
631 if expectations.owners.contains(o) {
632 o.green().to_string()
633 } else {
634 o.red().to_string()
635 }
636 })
637 .collect::<Vec<_>>();
638 table.push(SecretDisplay {
639 owners: owners.join(", "),
640 name,
641 })
642 */
643 }
644 // info!("loaded\n{}", Table::new(table).to_string())
645 */
608 let _span = info_span!("loading secrets").entered();646 todo!()
609 let configured = config.list_configured_shared()?;
610 #[derive(Tabled)]
611 struct SecretDisplay {
612 #[tabled(rename = "Name")]
613 name: String,
614 #[tabled(rename = "Owners")]
615 owners: String,
616 }
617 // let mut table = vec![];
618 for name in configured.iter().cloned() {
619 let config = config.clone();
620 let data = config.shared_secret(&name).expect("exists");
621 /*
622 let definition = config.shared_secret_definition(&name)?;
623 let expectations = definition.expectations()?;
624 let owners = data
625 .owners()
626 .map(|o| {
627 if expectations.owners.contains(o) {
628 o.green().to_string()
629 } else {
630 o.red().to_string()
631 }
632 })
633 .collect::<Vec<_>>();
634 table.push(SecretDisplay {
635 owners: owners.join(", "),
636 name,
637 })
638 */
639 }
640 // info!("loaded\n{}", Table::new(table).to_string())
641 }647 }
642 Secret::Edit {648 Secret::Edit {
643 name,649 name,
644 machine,650 machine,
645 part,651 part,
646 add,652 add,
647 } => {653 } => {
648 let secret = config654 /*let secret = config
649 .host_secret(&machine, &name)655 .host_secret(&machine, &name)
650 .context("secret not found")?;656 .context("secret not found")?;
651 if let Some(data) = secret.secret.parts.get(&part) {657 if let Some(data) = secret.secret.parts.get(&part) {
652 let host = config.host(&machine)?;658 let host = config.host(&machine)?;
653 let secret = host.decrypt(data.raw.clone()).await?;659 let secret = host.decrypt(data.raw.clone()).await?;
654 String::from_utf8(secret).context("secret is not utf8")?660 String::from_utf8(secret).context("secret is not utf8")?
655 } else if add {661 } else if add {
656 String::new()662 String::new()
657 } else {663 } else {
664 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");
665 };*/
658 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");666 todo!()
659 };
660 }667 }
661 }668 }
662 Ok(())669 Ok(())
modifiedcmds/fleet/src/cmds/tf.rsdiffbeforeafterboth
72 let tf_data: TfData = serde_json::from_slice(&data.stdout)72 let tf_data: TfData = serde_json::from_slice(&data.stdout)
73 .context("failed to parse terraform fleet output")?;73 .context("failed to parse terraform fleet output")?;
7474
75 debug!("synchronized done = {tf_data:?}");
75 let mut data = config.data();76 let mut extra = config.data.extra.write().expect("no poisoning");
76 debug!("synchronized done = {tf_data:?}");
77 data.extra.insert(77 extra.insert(
78 "terraformHosts".to_owned(),78 "terraformHosts".to_owned(),
79 serde_json::to_value(tf_data.hosts).expect("should be valid extra"),79 serde_json::to_value(tf_data.hosts).expect("should be valid extra"),
80 );80 );
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
4// pub(crate) mod command;4// pub(crate) mod command;
5pub(crate) mod extra_args;5pub(crate) mod extra_args;
66
7use std::{env, ffi::OsString, process::ExitCode};7use std::{env, ffi::OsString, process::ExitCode, sync::Arc};
88
9use anyhow::{Result, bail};9use anyhow::{Result, bail};
10use clap::{CommandFactory, Parser};10use clap::{CommandFactory, Parser};
24#[cfg(feature = "indicatif")]24#[cfg(feature = "indicatif")]
25use indicatif::{ProgressState, ProgressStyle};25use indicatif::{ProgressState, ProgressStyle};
26use nix_eval::{gc_register_my_thread, gc_unregister_my_thread, init_libraries};26use nix_eval::{
27 gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,
28};
27use tracing::{Instrument, error, info, info_span};29use tracing::{Instrument, error, info, info_span};
28#[cfg(feature = "indicatif")]30#[cfg(feature = "indicatif")]
39 info!("nothing to prefetch: no prefetch directory");41 info!("nothing to prefetch: no prefetch directory");
40 return Ok(());42 return Ok(());
41 }43 }
42 let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();44 let tasks = FuturesUnordered::new();
43 for entry in std::fs::read_dir(&prefetch_dir)? {45 for entry in std::fs::read_dir(&prefetch_dir)? {
44 tasks.push(Box::pin(async {46 tasks.push(async {
45 let entry = entry?;47 let entry = entry?;
46 if !entry.metadata()?.is_file() {48 if !entry.metadata()?.is_file() {
47 bail!("only files should exist in prefetch directory");49 bail!("only files should exist in prefetch directory");
59 status.arg("store").arg("prefetch-file").arg(path);61 status.arg("store").arg("prefetch-file").arg(path);
60 status.run_nix_string().instrument(span).await?;62 status.run_nix_string().instrument(span).await?;
61 Ok(())63 Ok(())
62 }));64 });
63 }65 }
64 tasks.try_collect::<Vec<()>>().await?;66 tasks.try_collect::<Vec<()>>().await?;
65 Ok(())67 Ok(())
190192
191 init_libraries();193 init_libraries();
192194
193 tokio::runtime::Builder::new_multi_thread()195 let runtime = tokio::runtime::Builder::new_multi_thread()
194 .enable_all()196 .enable_all()
195 .on_thread_start(|| {197 .on_thread_start(|| {
196 gc_register_my_thread();198 gc_register_my_thread();
199 gc_unregister_my_thread();201 gc_unregister_my_thread();
200 })202 })
201 .build()203 .build()
202 .expect("failed to build runtime")204 .expect("failed to build runtime");
205 let runtime = Arc::new(runtime);
206
207 init_tokio_for_nix(runtime.clone());
208
203 .block_on(async {209 runtime.block_on(async {
210 tokio::task::spawn(async move {
204 if let Err(e) = main_real(opts).await {211 if let Err(e) = main_real(opts).await {
205 error!("{e:#}");212 error!("{e:#}");
206 ExitCode::FAILURE213 ExitCode::FAILURE
207 } else {214 } else {
208 ExitCode::SUCCESS215 ExitCode::SUCCESS
209 }216 }
210 })217 })
218 .await
219 .expect("primary task panicked")
220 })
211 // async_main(opts)221 // async_main(opts)
212}222}
modifiedcrates/fleet-base/src/command.rsdiffbeforeafterboth
334 let mut stderr = child.stderr.take().unwrap();334 let mut stderr = child.stderr.take().unwrap();
335 let stdout = child.stdout.take().unwrap();335 let stdout = child.stdout.take().unwrap();
336 let mut err = FramedRead::new(&mut stderr, LinesCodec::new());336 let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
337 let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));337 let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
338 let mut ob = want_stdout338 let mut ob = want_stdout
339 .then(|| out.take().unwrap())339 .then(|| out.take().unwrap())
340 .unwrap_or_else(|| Box::new(EmptyAsyncRead));340 .unwrap_or_else(|| Box::new(EmptyAsyncRead));
397 let mut stderr = child.stderr().take().unwrap();397 let mut stderr = child.stderr().take().unwrap();
398 let stdout = child.stdout().take().unwrap();398 let stdout = child.stdout().take().unwrap();
399 let mut err = FramedRead::new(&mut stderr, LinesCodec::new());399 let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
400 let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));400 let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
401 let mut ob = want_stdout401 let mut ob = want_stdout
402 .then(|| out.take().unwrap())402 .then(|| out.take().unwrap())
403 .unwrap_or_else(|| Box::new(EmptyAsyncRead));403 .unwrap_or_else(|| Box::new(EmptyAsyncRead));
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
78 // unit name conflict in systemd-run78 // unit name conflict in systemd-run
79 // This code is tied to rollback.nix79 // This code is tied to rollback.nix
80 if !disable_rollback && action.should_create_rollback_marker() {80 if !disable_rollback && action.should_create_rollback_marker() {
81 let _span = info_span!("preparing").entered();81 // let _span = info_span!("preparing").entered();
82 info!("preparing for rollback");82 info!("preparing for rollback");
83 let generation = get_current_generation(host).await?;83 let generation = get_current_generation(host).await?;
84 info!(84 info!(
179 // FIXME: Connection might be disconnected after activation run179 // FIXME: Connection might be disconnected after activation run
180180
181 if action.should_activate() && !failed {181 if action.should_activate() && !failed {
182 let _span = info_span!("activating").entered();182 // let _span = info_span!("activating").entered();
183 info!("executing activation script");183 info!("executing activation script");
184 let specialised = if let Some(specialisation) = specialisation {184 let specialised = if let Some(specialisation) = specialisation {
185 let mut specialised = built.join("specialisation");185 let mut specialised = built.join("specialisation");
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
1use std::{1use std::{
2 cmp::Ordering,
2 collections::{3 collections::{
3 BTreeMap, BTreeSet,4 BTreeMap, BTreeSet,
4 btree_map::{self, Entry},5 btree_map::{self, Entry},
5 },6 },
7 fmt,
6 io::{self, Cursor},8 io::{self, Cursor},
7 ops::Deref,9 sync::RwLock,
8};10};
911
10use age::Recipient;12use age::Recipient;
77 pub manager_keys: Vec<ManagerKey>,79 pub manager_keys: Vec<ManagerKey>,
7880
79 #[serde(default)]81 #[serde(default)]
80 pub hosts: BTreeMap<String, HostData>,82 pub hosts: RwLock<BTreeMap<String, HostData>>,
8183
82 #[serde(default, alias = "shared_secrets")]84 #[serde(default, alias = "shared_secrets")]
83 pub secrets: FleetSecrets,85 pub secrets: RwLock<FleetSecrets>,
8486
85 // extra_name => anything87 // extra_name => anything
86 #[serde(default)]88 #[serde(default)]
87 #[serde(skip_serializing_if = "BTreeMap::is_empty")]89 pub extra: RwLock<BTreeMap<String, Value>>,
88 pub extra: BTreeMap<String, Value>,
8990
90 #[serde(default)]91 #[serde(default)]
91 #[serde(skip_serializing_if = "BTreeMap::is_empty")]92 #[serde(skip_serializing)]
92 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,93 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
93}94}
94impl FleetData {95impl FleetData {
95 pub fn from_str(s: &str) -> anyhow::Result<Self> {96 pub fn from_str(s: &str) -> anyhow::Result<Self> {
96 let mut data: Self = nixlike::parse_str(s)?;97 let mut data: Self = nixlike::parse_str(s)?;
97 if !data.host_secrets.is_empty() {98 if !data.host_secrets.is_empty() {
98 info!("migrating host secrets into shared secrets structure");99 info!("migrating host secrets into shared secrets structure");
99 data.secrets100 data.secrets
101 .write()
102 .expect("no poisoning")
100 .merge_from_hosts(std::mem::take(&mut data.host_secrets));103 .merge_from_hosts(std::mem::take(&mut data.host_secrets));
101 }104 }
102 Ok(data)105 Ok(data)
130#[serde(rename_all = "camelCase")]133#[serde(rename_all = "camelCase")]
131#[must_use]134#[must_use]
132pub struct FleetSecretData {135pub struct FleetSecretData {
133 #[serde(default = "Utc::now")]
134 pub created_at: DateTime<Utc>,136 pub created_at: DateTime<Utc>,
135 #[serde(default)]137 #[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]
136 #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]
137 pub expires_at: Option<DateTime<Utc>>,138 pub expires_at: Option<DateTime<Utc>>,
138139
139 #[serde(flatten)]140 #[serde(flatten)]
140 pub parts: BTreeMap<String, FleetSecretPart>,141 pub parts: BTreeMap<String, FleetSecretPart>,
141142
142 #[serde(default)]143 #[serde(default, skip_serializing_if = "Value::is_null")]
143 #[serde(skip_serializing_if = "Value::is_null")]
144 pub generation_data: Value,144 pub generation_data: Value,
145}145}
146146
147fn is_false(b: &bool) -> bool {
148 *b == false
149}
150
151#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
152#[repr(transparent)]
153pub struct SecretOwner(String);
154
155impl fmt::Display for SecretOwner {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 write!(f, "host:{}", self.0)
158 }
159}
160
161impl SecretOwner {
162 pub fn host(s: impl AsRef<str>) -> SecretOwner {
163 SecretOwner(s.as_ref().to_owned())
164 }
165 pub fn as_host(&self) -> Option<&str> {
166 Some(&self.0)
167 }
168}
169
147#[derive(Serialize, Deserialize, Clone, Debug)]170#[derive(Serialize, Deserialize, Clone, Debug)]
148#[serde(rename_all = "camelCase")]171#[serde(rename_all = "camelCase")]
149#[must_use]172#[must_use]
150pub struct FleetSecretDistribution {173pub struct FleetSecretDistribution {
151 #[serde(default)]174 #[serde(default)]
152 pub owners: BTreeSet<String>,175 owners: BTreeSet<SecretOwner>,
176 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
177 owners_pending_prune: BTreeMap<SecretOwner, String>,
178
153 #[serde(flatten)]179 #[serde(flatten)]
154 pub secret: FleetSecretData,180 pub secret: FleetSecretData,
155181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pending_prune: Option<String>,
156 #[serde(default, skip_serializing, alias = "managed")]184 #[serde(default, skip_serializing, alias = "managed")]
157 pub _deprecated_managed: bool,185 _deprecated_managed: bool,
158}186}
159187
160#[derive(Clone)]188const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();
161#[must_use]189impl FleetSecretDistribution {
162pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);190 pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {
191 assert!(
192 !owners.is_empty(),
193 "distribution should have at least one owner"
194 );
195 if let Some(expires_at) = &secret.expires_at {
196 assert!(
197 *expires_at > now,
198 "secret should not be expired on creation"
199 );
200 }
201 Self {
202 owners,
203 secret,
204 owners_pending_prune: BTreeMap::new(),
205 pending_prune: None,
206 _deprecated_managed: true,
207 }
208 }
163209
164impl Deref for FleetSecretDistributions {210 fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {
165 type Target = [FleetSecretDistribution];211 let pending_prune = if including_pruned {
212 &self.owners_pending_prune
213 } else {
214 EMPTY_PENDING_PRUNE
215 };
216 self.owners.iter().chain(pending_prune.keys())
217 }
218 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
219 self.owners_ex(false)
220 }
166221
167 fn deref(&self) -> &Self::Target {222 pub fn prune(&mut self, reason: String) {
168 self.0.as_slice()223 assert!(
224 self.pending_prune.is_none(),
225 "it shouldn't be possible to prune the same distribution twice using public api"
226 );
227 self.pending_prune = Some(reason);
169 }228 }
229 pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {
230 // if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {
231 // self.prune(format!("all owners were pruned: {reason}"));
232 // return;
233 // }
234 for owner in owners {
235 if self.owners.remove(owner) {
236 self.owners_pending_prune
237 .insert(owner.to_owned(), reason.clone());
238 }
239 }
240 // if self.owners.is_empty() {
241 // self.prune("no owners left".to_owned());
242 // }
243 }
244 pub fn unprune_owner(&mut self, owner: SecretOwner) {
245 if self.owners_pending_prune.remove(&owner).is_some() {
246 self.owners.insert(owner);
247 }
248 }
170}249}
171250
251#[derive(Clone, Debug, Default)]
252#[must_use]
253pub struct FleetSecretDistributions {
254 stored: Vec<FleetSecretDistribution>,
255}
256
257fn compare_dists(
258 a: &FleetSecretDistribution,
259 b: &FleetSecretDistribution,
260 prefer_identities: &BTreeSet<SecretOwner>,
261 include_pruned_owners: bool,
262) -> Ordering {
263 use Ordering::*;
264 if prefer_identities.is_empty() {
265 let a_has = a
266 .owners_ex(include_pruned_owners)
267 .any(|o| prefer_identities.contains(o));
268 let b_has = b
269 .owners_ex(include_pruned_owners)
270 .any(|o| prefer_identities.contains(o));
271 match (a_has, b_has) {
272 (true, false) => return Greater,
273 (false, true) => return Less,
274 _ => {}
275 }
276 }
277 match (a.secret.expires_at, b.secret.expires_at) {
278 (None, Some(_)) => return Greater,
279 (Some(_), None) => return Less,
280 (Some(a), Some(b)) => {
281 // Later is better
282 return a.cmp(&b);
283 }
284 (None, None) => {}
285 }
286
287 // Which one is easier to access
288 return a.owners.len().cmp(&b.owners.len());
289}
290
172impl FleetSecretDistributions {291impl FleetSecretDistributions {
173 pub fn owners(&self) -> impl Iterator<Item = &String> {292 /// Drop expired distributions
293 fn prune_expired(&mut self, now: DateTime<Utc>) {
174 self.0.iter().flat_map(|v| v.owners.iter())294 for ele in self.distributions_mut() {
295 if let Some(expires_at) = ele.secret.expires_at {
296 if expires_at < now {
297 ele.prune(format!("expired during check at {now}"));
298 }
299 }
300 }
175 }301 }
302 /// Perform all pruning relevant to shared secrets
303 /// Also see expected_owner_removed
304 pub fn prune_shared(
305 &mut self,
306 expected_owners: &BTreeSet<SecretOwner>,
307 unique: bool,
308 expected_parts: &BTreeMap<String, GeneratorPart>,
309 expected_generation_data: &Value,
310 regenerate_on_owner_removed: bool,
311 regenerate_on_owner_added: bool,
312 prefer_identities: &BTreeSet<SecretOwner>,
313 now: DateTime<Utc>,
314 ) {
315 self.prune_expired(now);
316 self.prune_generation_data(expected_generation_data, None);
317 self.prune_missing_parts(expected_parts, None);
318
319 let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();
320
321 let mut to_add = expected_owners.difference(&current_owners);
322 if to_add.next().is_some() && unique && regenerate_on_owner_added {
323 for dist in self.distributions_mut() {
324 dist.prune(format!(
325 "owners missing, can't add new distribution, regeneration preferred"
326 ));
327 }
328 return;
329 }
330
331 for to_remove in current_owners.difference(&expected_owners) {
332 self.entry(to_remove.clone()).remove(
333 regenerate_on_owner_removed,
334 "owner was removed from expected owners list, regenerate_on_owner_removed is set"
335 .to_string(),
336 );
337 }
338 if unique {
339 self.prune_nonunique(prefer_identities);
340 }
341 }
342 pub fn prune_host(
343 &mut self,
344 owner: SecretOwner,
345 expected_parts: &BTreeMap<String, GeneratorPart>,
346 expected_generation_data: &Value,
347 now: DateTime<Utc>,
348 ) {
349 self.prune_expired(now);
350 self.prune_generation_data(expected_generation_data, Some(&owner));
351 // TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)
352 self.prune_missing_parts(expected_parts, Some(&owner));
353 }
354 /// Position of best distributions as in iterator returned by distributions()
355 /// None if distributions not found
356 fn best_idx(
357 &self,
358 prefer_identities: &BTreeSet<SecretOwner>,
359 include_pruned_owners: bool,
360 ) -> Option<usize> {
361 self.distributions()
362 .enumerate()
363 .max_by(|(_, a), (_, b)| {
364 compare_dists(&a, &b, prefer_identities, include_pruned_owners)
365 })
366 .map(|(p, _)| p)
367 }
368 /// Secret wants to be the same on all hosts, leave only one unpruned version of it
369 fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {
370 if self.distributions().next().is_none() {
371 return;
372 }
373 let best = self.best_idx(prefer_identities, false).expect("not empty");
374 for (i, dist) in self.distributions_mut().enumerate() {
375 if i != best {
376 dist.prune(
377 "secret wants to be the same on all hosts, only the best one was left"
378 .to_owned(),
379 );
380 }
381 }
382 }
383
384 pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {
385 assert!(self.get(&owner).is_none(), "secret is not pruned for host");
386 if let Some(dist) = self
387 .distributions_mut()
388 .find(|v| v.owners_pending_prune.contains_key(&owner))
389 {
390 dist.unprune_owner(owner);
391 Some(dist)
392 } else {
393 None
394 }
395 }
396
397 pub fn best_distribution_for_reencryption(
398 &mut self,
399 prefer_identities: &BTreeSet<SecretOwner>,
400 ) -> Option<&mut FleetSecretDistribution> {
401 let best_idx = self.best_idx(prefer_identities, true)?;
402 self.distributions_mut().nth(best_idx)
403 }
404
405 fn prune_missing_parts(
406 &mut self,
407 expected_parts: &BTreeMap<String, GeneratorPart>,
408 filter_owner: Option<&SecretOwner>,
409 ) {
410 'dist: for ele in self.distributions_mut() {
411 if let Some(filter_owner) = filter_owner {
412 if !ele.owners.contains(filter_owner) {
413 continue;
414 }
415 // Note: secret still can have multiple owners even if it is host-owned
416 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them
417 }
418 for (name, part) in expected_parts {
419 let Some(stored_part) = ele.secret.parts.get(name) else {
420 ele.prune(format!("secret definition added new part: {name}"));
421 continue 'dist;
422 };
423 if part.encrypted != stored_part.raw.encrypted {
424 ele.prune(format!(
425 "secret definition now requires part to be {}",
426 if part.encrypted {
427 "encrypted"
428 } else {
429 "non-encrypted"
430 }
431 ));
432 continue 'dist;
433 }
434 }
435 }
436 }
437 fn prune_generation_data(
438 &mut self,
439 expected_generation_data: &Value,
440 filter_owner: Option<&SecretOwner>,
441 ) {
442 for ele in self.distributions_mut() {
443 if let Some(filter_owner) = filter_owner {
444 if !ele.owners.contains(filter_owner) {
445 continue;
446 }
447 // Note: secret still can have multiple owners even if it is host-owned
448 // in this case we expect that all owners using the same generator, so we can prune distribution for all of them
449 }
450 if ele.secret.generation_data != *expected_generation_data {
451 ele.prune(format!(
452 "expected generation data mismatch: {expected_generation_data:?}"
453 ));
454 }
455 }
456 }
457
458 /// Prune all distributions with no unpruned owners.
459 /// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and
460 /// can decrypt their secrets.
461 fn prune_dead(&mut self) {
462 for ele in self.distributions_mut() {
463 if ele.owners.is_empty() {
464 ele.prune("no owners left".to_owned());
465 }
466 }
467 }
468
469 pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {
470 self.stored.iter().filter(|v| v.pending_prune.is_none())
471 }
472 pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {
473 self.stored.iter_mut().filter(|v| v.pending_prune.is_none())
474 }
475 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
476 self.distributions().flat_map(|v| v.owners.iter())
477 }
176 #[allow(478 #[allow(
177 clippy::len_without_is_empty,479 clippy::len_without_is_empty,
178 reason = "should not be empty for a long time"480 reason = "should not be empty for a long time"
179 )]481 )]
180 pub fn len(&self) -> usize {482 pub fn len(&self) -> usize {
181 self.0.len()483 self.distributions().count()
182 }484 }
183485
184 pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {486 pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {
185 self.0.iter().find(|d| d.owners.contains(owner))487 self.distributions().find(|d| d.owners.contains(owner))
186 }488 }
187 fn entry(&mut self, owner: String) -> DistEntry<'_> {489 fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {
188 let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {490 let Some((idx, dist)) = self
491 .distributions()
492 .enumerate()
493 .find(|(_, d)| d.owners.contains(&owner))
494 else {
189 return DistEntry::Vacant(VacantDistEntry {495 return DistEntry::Vacant(VacantDistEntry {
190 distributions: self,496 distributions: self,
191 owner,497 owners: BTreeSet::from([owner]),
192 });498 });
193 };499 };
194 DistEntry::Occupied(OccupiedDistEntry {500 DistEntry::Occupied(OccupiedDistEntry {
501 owners: dist.owners.clone(),
195 distributions: self,502 distributions: self,
196 idx,503 idx,
197 owner,
198 })504 })
199 }505 }
200 fn extend(&mut self, dist: FleetSecretDistribution) {506 pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {
201 for owner in &dist.owners {507 for ele in self.distributions_mut() {
202 self.entry(owner.to_owned()).remove();508 ele.prune_owners(&dist.owners, reason.clone());
203 }509 }
204 self.0.push(dist);510 self.stored.push(dist);
205 }511 }
206 pub fn contains(&self, owner: &str) -> bool {512 pub fn contains(&self, owner: &SecretOwner) -> bool {
207 self.0.iter().any(|d| d.owners.contains(owner))513 self.distributions().any(|d| d.owners.contains(owner))
208 }514 }
209}515}
210516
211struct OccupiedDistEntry<'d> {517struct OccupiedDistEntry<'d> {
212 distributions: &'d mut FleetSecretDistributions,518 distributions: &'d mut FleetSecretDistributions,
213 idx: usize,519 idx: usize,
214 owner: String,520 owners: BTreeSet<SecretOwner>,
215}521}
216impl<'d> OccupiedDistEntry<'d> {522impl<'d> OccupiedDistEntry<'d> {
217 fn remove(self) -> VacantDistEntry<'d> {523 fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {
218 let dist = &mut self.distributions.0[self.idx];524 let dist = &mut self.distributions.stored[self.idx];
219 assert!(525 if whole_dist {
220 dist.owners.remove(&self.owner),526 dist.prune(reason);
221 "entry exists, as we have its reference"
222 );
223 if dist.owners.is_empty() {527 } else {
224 self.distributions.0.remove(self.idx);528 dist.prune_owners(&self.owners, reason);
225 }529 }
226 VacantDistEntry {530 VacantDistEntry {
227 distributions: self.distributions,531 distributions: self.distributions,
228 owner: self.owner,532 owners: self.owners,
229 }533 }
230 }534 }
231 fn set(self, secret: FleetSecretData) -> Self {535 fn set(self, secret: FleetSecretData, reason: String) -> Self {
232 self.remove().set(secret)536 self.remove(false, reason).set(secret)
233 }537 }
234}538}
235struct VacantDistEntry<'d> {539struct VacantDistEntry<'d> {
236 distributions: &'d mut FleetSecretDistributions,540 distributions: &'d mut FleetSecretDistributions,
237 owner: String,541 owners: BTreeSet<SecretOwner>,
238}542}
239impl<'d> VacantDistEntry<'d> {543impl<'d> VacantDistEntry<'d> {
240 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {544 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {
241 let Self {545 let Self {
242 distributions,546 distributions,
243 owner,547 owners,
244 } = self;548 } = self;
245 let idx = distributions.0.len();549 let idx = distributions.stored.len();
246 distributions.0.push(FleetSecretDistribution {550 distributions.stored.push(FleetSecretDistribution {
247 owners: BTreeSet::from_iter([owner.clone()]),551 owners: owners.clone(),
248 secret,552 secret,
249553
554 owners_pending_prune: BTreeMap::new(),
555 pending_prune: None,
250 _deprecated_managed: true,556 _deprecated_managed: true,
251 });557 });
252 OccupiedDistEntry {558 OccupiedDistEntry {
253 distributions,559 distributions,
254 owner,560 owners,
255 idx,561 idx,
256 }562 }
257 }563 }
262 Occupied(OccupiedDistEntry<'d>),568 Occupied(OccupiedDistEntry<'d>),
263}569}
264impl DistEntry<'_> {570impl DistEntry<'_> {
265 fn remove(self) -> Self {571 fn remove(self, whole_dist: bool, reason: String) -> Self {
266 match self {572 match self {
267 DistEntry::Vacant(_) => self,573 DistEntry::Vacant(_) => self,
268 DistEntry::Occupied(o) => Self::Vacant(o.remove()),574 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),
269 }575 }
270 }576 }
271 fn set(self, secret: FleetSecretData) -> Self {577 fn set(self, secret: FleetSecretData, reason: String) -> Self {
272 Self::Occupied(match self {578 Self::Occupied(match self {
273 DistEntry::Vacant(e) => e.set(secret),579 DistEntry::Vacant(e) => e.set(secret),
274 DistEntry::Occupied(e) => e.set(secret),580 DistEntry::Occupied(e) => e.set(secret, reason),
275 })581 })
276 }582 }
277}583}
281 where587 where
282 S: serde::Serializer,588 S: serde::Serializer,
283 {589 {
590 let mut v = self.clone();
591 v.prune_dead();
284 let mut found_hosts = BTreeSet::new();592 let mut found_hosts = BTreeSet::new();
285 for ele in self.0.iter() {593 for ele in v.distributions() {
594 if ele.pending_prune.is_some() {
595 continue;
596 }
286 if ele.owners.is_empty() {597 if ele.owners.is_empty() {
287 panic!("consistency: secret distribution has no defined owners");598 panic!("consistency: secret distribution has no defined owners");
288 }599 }
294 }605 }
295 }606 }
296 }607 }
297 match self.0.len() {608 match v.stored.len() {
298 0 => panic!("consistency: empty distributions"),609 0 => panic!("consistency: empty distributions"),
299 1 => self.0[0].serialize(serializer),610 1 => v.stored[0].serialize(serializer),
300 _ => self.0.serialize(serializer),611 _ => {
612 let mut sorted = v.stored.clone();
613 // Store outdated distributions last
614 sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);
615 sorted.serialize(serializer)
616 }
301 }617 }
302 }618 }
303}619}
313 Many(Vec<FleetSecretDistribution>),629 Many(Vec<FleetSecretDistribution>),
314 }630 }
315 let d = Distributions::deserialize(deserializer)?;631 let d = Distributions::deserialize(deserializer)?;
316 let ds = match d {632 let stored = match d {
317 Distributions::One(d) => vec![d],633 Distributions::One(d) => vec![d],
318 Distributions::Many(ds) => ds,634 Distributions::Many(ds) => ds,
319 };635 };
320 if ds.is_empty() {636 if stored.is_empty() {
321 return Err(de::Error::custom("consistency: empty distributions"));637 return Err(de::Error::custom("consistency: empty distributions"));
322 }638 }
323 let mut found_hosts = BTreeSet::new();639 let mut found_hosts = BTreeSet::new();
324 for ele in ds.iter() {640 for ele in stored.iter() {
641 if ele.pending_prune.is_some() {
642 continue;
643 }
325 if ele.owners.is_empty() {644 if ele.owners.is_empty() {
326 return Err(de::Error::custom(645 return Err(de::Error::custom(
327 "consistency: secret distribution has no defined owners",646 "consistency: secret distribution has no defined owners",
335 }654 }
336 }655 }
337 }656 }
338 Ok(Self(ds))657 Ok(Self { stored })
339 }658 }
340}659}
341660
342#[derive(Serialize, Deserialize, Default)]661#[derive(Deserialize, Default)]
343pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);662pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);
344663
664impl Serialize for FleetSecrets {
665 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
666 where
667 S: serde::Serializer,
668 {
669 let data: BTreeMap<String, FleetSecretDistributions> = self
670 .0
671 .iter()
672 .filter(|(_, v)| !v.stored.is_empty())
673 .map(|(k, v)| (k.clone(), v.clone()))
674 .collect();
675
676 data.serialize(serializer)
677 }
678}
679
345impl FleetSecrets {680impl FleetSecrets {
346 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {681 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {
347 self.0.keys()682 self.0.keys()
348 }683 }
349684
350 pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {685 pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {
351 self.0686 self.0
352 .iter()687 .iter()
353 .filter(|(_, d)| d.contains(owner))688 .filter(|(_, d)| d.contains(owner))
354 .map(|(n, _)| n)689 .map(|(n, _)| n)
355 }690 }
356691
357 pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {
358 let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {
359 return false;
360 };
361 let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {
362 return false;
363 };
364
365 dist.remove();
366
367 if dists.get().0.is_empty() {
368 dists.remove();
369 };
370
371 true
372 }
373 pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {
374 let e = self
375 .0
376 .entry(secret.to_owned())
377 .or_insert_with(|| FleetSecretDistributions(Default::default()));
378 e.entry(owner.to_owned()).set(data);
379 }
380 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {692 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {
381 match self.0.entry(secret) {693 match self.0.entry(secret) {
382 Entry::Vacant(e) => {694 Entry::Vacant(e) => {
383 e.insert(FleetSecretDistributions(vec![data]));695 e.insert(FleetSecretDistributions { stored: vec![data] });
384 }696 }
385 Entry::Occupied(mut e) => {697 Entry::Occupied(mut e) => {
386 let dists = e.get_mut();698 let dists = e.get_mut();
387 dists.extend(data)699 dists.extend(data, "secret data was replaced".to_owned())
388 }700 }
389 }701 }
390 }702 }
391 pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {
392 let secret = self.0.get(secret)?;
393 secret.get(owner)
394 }
395 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {703 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {
396 self.0.get(secret)704 self.0.get(secret)
397 }705 }
706 pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {
707 self.0.get_mut(secret)
708 }
398709
399 pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {710 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {
400 let Some(secret) = self.0.get(secret) else {711 self.0
712 .entry(secret.to_owned())
401 return false;713 .or_insert(FleetSecretDistributions::default())
402 };
403 secret.contains(owner)
404 }714 }
715
405 pub fn contains(&self, secret: &str) -> bool {716 pub fn contains(&self, secret: &str) -> bool {
406 self.0.contains_key(secret)717 self.0.contains_key(secret)
407 }718 }
411722
412 fn merge_from_hosts(723 fn merge_from_hosts(
413 &mut self,724 &mut self,
414 host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,725 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
415 ) {726 ) {
416 for (host, host_secrets) in host_secrets {727 for (host, host_secrets) in host_secrets {
417 for (secret_name, mut secret_data) in host_secrets {728 for (secret_name, mut secret_data) in host_secrets {
420 }731 }
421 }732 }
422 }733 }
734
735 pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {
736 for (name, dists) in self.0.iter_mut() {
737 if expected_nonshared.contains(name) {
738 continue;
739 }
740 for dist in dists.distributions_mut() {
741 if dist.owners.contains(host) {
742 dist.prune_owners(
743 &BTreeSet::from([host.to_owned()]),
744 "host no longer defines this secret".to_owned(),
745 );
746 }
747 }
748 }
749 }
423}750}
424751
425#[derive(Debug)]752#[derive(Debug, Clone)]
426pub struct Expectations {753pub struct Expectations {
427 pub owners: BTreeSet<String>,754 pub owners: BTreeSet<SecretOwner>,
428 pub generation_data: serde_json::Value,755 pub generation_data: serde_json::Value,
429 pub parts: BTreeMap<String, GeneratorPart>,756 pub parts: BTreeMap<String, GeneratorPart>,
430}757}
431#[derive(Deserialize, Debug, Clone)]758#[derive(Deserialize, Debug, Clone)]
432pub struct GeneratorPart {759pub struct GeneratorPart {
433 pub encrypted: bool,760 pub encrypted: bool,
761}
762
763#[derive(Debug, Clone, Copy)]
764pub struct RegenerationConstraints {
765 pub allow_different: bool,
766 pub regenerate_on_owner_added: bool,
767 pub regenerate_on_owner_removed: bool,
768}
769impl RegenerationConstraints {
770 pub fn host_personal() -> Self {
771 Self {
772 allow_different: false,
773 regenerate_on_owner_added: true,
774 regenerate_on_owner_removed: true,
775 }
776 }
777 pub fn without_preferences(self) -> Self {
778 Self {
779 allow_different: self.allow_different,
780 regenerate_on_owner_added: false,
781 regenerate_on_owner_removed: false,
782 }
783 }
434}784}
435785
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
1use std::{1use std::{
2 cell::OnceCell,
3 collections::BTreeSet,2 collections::{BTreeMap, BTreeSet},
4 ffi::{OsStr, OsString},3 ffi::{OsStr, OsString},
5 fmt::Display,4 fmt::Display,
6 io::Write,5 io::Write,
11};10};
1211
13use anyhow::{Context, Result, anyhow, bail, ensure};12use anyhow::{Context, Result, anyhow, bail, ensure};
13use chrono::{DateTime, Utc};
14use fleet_shared::SecretData;14use fleet_shared::SecretData;
15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
16use openssh::{ControlPersist, SessionBuilder};16use openssh::{ControlPersist, SessionBuilder};
2222
23use crate::{23use crate::{
24 command::MyCommand,24 command::MyCommand,
25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},25 fleetdata::{
26 FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,
27 },
26};28};
2729
28pub struct FleetConfigInternals {30pub struct FleetConfigInternals {
31 pub prefer_identities: BTreeSet<SecretOwner>,
32 pub now: DateTime<Utc>,
33
29 /// Fleet project directory, containing fleet.nix file.34 /// Fleet project directory, containing fleet.nix file.
30 pub directory: PathBuf,35 pub directory: PathBuf,
31 /// builtins.currentSystem36 /// builtins.currentSystem
32 pub local_system: String,37 pub local_system: String,
33 pub data: Arc<Mutex<FleetData>>,38 pub data: Arc<FleetData>,
34 pub nix_args: Vec<OsString>,39 pub nix_args: Vec<OsString>,
35 /// fleet_config.config40 /// fleet_config.config
36 pub config_field: Value,41 pub config_field: Value,
96pub struct ConfigHost {101pub struct ConfigHost {
97 config: Config,102 config: Config,
98 pub name: String,103 pub name: String,
99 groups: OnceCell<Vec<String>>,104 groups: OnceLock<Vec<String>>,
100105
101 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it106 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
102 deploy_kind: OnceCell<DeployKind>,107 deploy_kind: OnceLock<DeployKind>,
103 session_destination: OnceCell<String>,108 session_destination: OnceLock<String>,
104 legacy_ssh_store: OnceCell<bool>,109 legacy_ssh_store: OnceLock<bool>,
105110
106 pub host_config: Option<Value>,111 pub host_config: Option<Value>,
107 pub nixos_config: OnceCell<Value>,112 pub nixos_config: OnceLock<Value>,
108 pub nixos_unchecked_config: OnceCell<Value>,113 pub nixos_unchecked_config: OnceLock<Value>,
109 pub pkgs_override: Option<Value>,114 pub pkgs_override: Option<Value>,
110115
111 // TODO: Move command helpers away with connectivity refactor116 // TODO: Move command helpers away with connectivity refactor
397 ensure!(!data.encrypted, "secret came out encrypted");402 ensure!(!data.encrypted, "secret came out encrypted");
398 Ok(data.data)403 Ok(data.data)
399 }404 }
405 pub async fn reencrypt_distribution(
406 &self,
407 data: &FleetSecretDistribution,
408 targets: BTreeSet<SecretOwner>,
409 now: DateTime<Utc>,
410 ) -> Result<FleetSecretDistribution> {
411 let mut parts = BTreeMap::new();
412 for (part_name, part) in &data.secret.parts {
413 parts.insert(
414 part_name.clone(),
415 if part.raw.encrypted {
416 FleetSecretPart {
417 raw: self.reencrypt(part.raw.clone(), targets.clone()).await?,
418 }
419 } else {
420 part.clone()
421 },
422 );
423 }
424 let secret = FleetSecretData {
425 created_at: data.secret.created_at,
426 expires_at: data.secret.expires_at,
427 generation_data: data.secret.generation_data.clone(),
428 parts,
429 };
430 Ok(FleetSecretDistribution::new(targets, secret, now))
431 }
400 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {432 pub async fn reencrypt(
433 &self,
434 data: SecretData,
435 targets: BTreeSet<SecretOwner>,
436 ) -> Result<SecretData> {
401 ensure!(data.encrypted, "secret is not encrypted");437 ensure!(data.encrypted, "secret is not encrypted");
402 let mut cmd = self.cmd("fleet-install-secrets").await?;438 let mut cmd = self.cmd("fleet-install-secrets").await?;
537 }573 }
538}574}
539575
576#[derive(Clone)]
540pub struct SharedSecretDefinition(Value);577pub struct SharedSecretDefinition(Value);
541impl SharedSecretDefinition {578impl SharedSecretDefinition {
542 pub fn expected_owners(&self) -> Result<BTreeSet<String>> {579 pub fn expected_owners(&self) -> Result<BTreeSet<SecretOwner>> {
543 let secret = &self.0;580 let secret = &self.0;
544 Ok(nix_go_json!(secret.expectedOwners))581 Ok(nix_go_json!(secret.expectedOwners))
545 }582 }
583 pub fn allow_different(&self) -> Result<bool> {
584 let secret = &self.0;
585 Ok(nix_go_json!(secret.allowDifferent))
586 }
587 pub fn regenerate_on_owner_added(&self) -> Result<bool> {
588 let secret = &self.0;
589 Ok(nix_go_json!(secret.regenerateOnOwnerAdded))
590 }
591 pub fn regenerate_on_owner_removed(&self) -> Result<bool> {
592 let secret = &self.0;
593 Ok(nix_go_json!(secret.regenerateOnOwnerRemoved))
594 }
546 pub fn generator(&self) -> Result<Value> {595 pub fn generator(&self) -> Result<Value> {
547 let secret = &self.0;596 let secret = &self.0;
548 Ok(nix_go!(secret.generator))597 Ok(nix_go!(secret.generator))
572 config: self.clone(),621 config: self.clone(),
573 name: "<virtual localhost>".to_owned(),622 name: "<virtual localhost>".to_owned(),
574 host_config: None,623 host_config: None,
575 nixos_config: OnceCell::new(),624 nixos_config: OnceLock::new(),
576 nixos_unchecked_config: OnceCell::new(),625 nixos_unchecked_config: OnceLock::new(),
577 groups: {626 groups: {
578 let cell = OnceCell::new();627 let cell = OnceLock::new();
579 let _ = cell.set(vec![]);628 let _ = cell.set(vec![]);
580 cell629 cell
581 },630 },
582 pkgs_override: Some(self.default_pkgs.clone()),631 pkgs_override: Some(self.default_pkgs.clone()),
583632
584 local: true,633 local: true,
585 session: OnceLock::new(),634 session: OnceLock::new(),
586 deploy_kind: OnceCell::new(),635 deploy_kind: OnceLock::new(),
587 session_destination: OnceCell::new(),636 session_destination: OnceLock::new(),
588 legacy_ssh_store: OnceCell::new(),637 legacy_ssh_store: OnceLock::new(),
589 }638 }
590 }639 }
591640
597 config: self.clone(),646 config: self.clone(),
598 name: name.to_owned(),647 name: name.to_owned(),
599 host_config: Some(host_config),648 host_config: Some(host_config),
600 nixos_config: OnceCell::new(),649 nixos_config: OnceLock::new(),
601 nixos_unchecked_config: OnceCell::new(),650 nixos_unchecked_config: OnceLock::new(),
602 groups: OnceCell::new(),651 groups: OnceLock::new(),
603 pkgs_override: None,652 pkgs_override: None,
604653
605 // TODO: Remove with connectivit refactor654 // TODO: Remove with connectivit refactor
606 local: self.localhost == name,655 local: self.localhost == name,
607 session: OnceLock::new(),656 session: OnceLock::new(),
608 deploy_kind: OnceCell::new(),657 deploy_kind: OnceLock::new(),
609 session_destination: OnceCell::new(),658 session_destination: OnceLock::new(),
610 legacy_ssh_store: OnceCell::new(),659 legacy_ssh_store: OnceLock::new(),
611 })660 })
612 }661 }
613 pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {662 pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
625 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))674 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))
626 }675 }
627
628 /// Shared secrets configured in fleet.nix or in flake
629 pub fn list_configured_shared(&self) -> Result<Vec<String>> {
630 let config_field = &self.config_field;
631 nix_go!(config_field.sharedSecrets).list_fields()
632 }
633 pub fn has_shared(&self, name: &str) -> bool {
634 let data = self.data();
635 data.secrets.contains(name)
636 }
637 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {
638 let mut data = self.data_mut();
639 data.secrets.set_data(name, shared);
640 }
641 pub fn remove_shared(&self, secret: &str) {
642 let mut data = self.data_mut();
643 data.secrets.remove(secret);
644 }
645
646 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {
647 let data = self.data_mut();
648 data.secrets.keys_for_owner(host).cloned().collect()
649 }
650 pub fn list_secrets(&self) -> Vec<String> {
651 let data = self.data_mut();
652 data.secrets.keys().cloned().collect()
653 }
654
655 pub fn has_secret(&self, host: &str, secret: &str) -> bool {
656 let data = self.data();
657 data.secrets.contains_for_owner(secret, host)
658 }
659 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {
660 let mut data = self.data_mut();
661 data.secrets.set_single_data(secret, host, value);
662 }
663 pub fn remove_secret(&self, host: &str, secret: &str) {
664 let mut data = self.data_mut();
665 data.secrets.drop_owner_no_reencrypt(secret, host);
666 }
667
668 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {
669 let data = self.data();
670 data.secrets.get_single(secret, host).cloned()
671 }
672 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {
673 let data = self.data();
674 data.secrets.get(secret).cloned()
675 }
676676
677 pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {677 pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {
678 let config = &self.config_field;678 let config = &self.config_field;
685 ))))685 ))))
686 }686 }
687687
688 // TODO: Should this be something modifiable from other processes?
689 // E.g terraform provider might want to update FleetData (e.g secrets),
690 // and current implementation assumes only one process holds current fleet.nix
691 // Given that it is no longer needs to be a file for nix evaluation,
692 // maybe it can be a .nix file for persistence, but accessible only
693 // thru some shared state controller? Might it be stored in terraform
694 // state provider?
695 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {
696 self.data.lock().unwrap()
697 }
698 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {
699 self.data.lock().unwrap()
700 }
701 pub fn save(&self) -> Result<()> {688 pub fn save(&self) -> Result<()> {
702 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;689 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;
703 let data = nixlike::serialize(&self.data() as &FleetData)?;690 let data = nixlike::serialize(&*self.data)?;
704 tempfile.write_all(691 tempfile.write_all(
705 format!(692 format!(
706 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"693 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
1use std::str::FromStr as _;1use std::str::FromStr as _;
22
3use age::Recipient;3use age::Recipient;
4use anyhow::{Result, anyhow};4use anyhow::{Result, anyhow, bail};
5use futures::{StreamExt as _, TryStreamExt as _};5use futures::{StreamExt as _, TryStreamExt as _};
6use itertools::Itertools as _;6use itertools::Itertools as _;
7use tracing::warn;7use tracing::warn;
88
9use crate::host::Config;9use crate::{fleetdata::SecretOwner, host::Config};
1010
11impl Config {11impl Config {
12 pub fn cached_key(&self, host: &str) -> Option<String> {12 fn cached_host_key(&self, host: &str) -> Option<String> {
13 let data = self.data();13 let hosts = self.data.hosts.read().expect("no poisoning");
14 let key = data.hosts.get(host).map(|h| &h.encryption_key);14 let key = hosts.get(host).map(|h| &h.encryption_key);
15 if let Some(key) = key15 if let Some(key) = key
16 && key.is_empty()16 && key.is_empty()
17 {17 {
20 key.cloned()20 key.cloned()
21 }21 }
22 pub fn update_key(&self, host: &str, key: String) {22 pub fn update_key(&self, host: &str, key: String) {
23 let mut data = self.data_mut();23 let mut hosts = self.data.hosts.write().expect("no poisoning");
24 let host = data.hosts.entry(host.to_string()).or_default();24 let host = hosts.entry(host.to_string()).or_default();
25 host.encryption_key = key.trim().to_string();25 host.encryption_key = key.trim().to_string();
26 }26 }
2727
28 pub async fn key(&self, host: &str) -> anyhow::Result<String> {28 pub async fn host_key(&self, host: &str) -> anyhow::Result<String> {
29 if let Some(key) = self.cached_key(host) {29 if let Some(key) = self.cached_host_key(host) {
30 Ok(key)30 Ok(key)
31 } else {31 } else {
32 warn!("Loading key for {}", host);32 warn!("Loading key for {}", host);
38 Ok(key)38 Ok(key)
39 }39 }
40 }40 }
41 pub async fn key(&self, owner: &SecretOwner) -> anyhow::Result<String> {
42 if let Some(host) = owner.as_host() {
43 self.host_key(host).await
44 } else {
45 bail!("only host keys supported for now")
46 }
47 }
41 /// Insecure, requires root48 /// Insecure, requires root
42 pub async fn recipient(&self, host: &str) -> anyhow::Result<Box<dyn Recipient>> {49 pub async fn recipient(&self, host: &SecretOwner) -> anyhow::Result<Box<dyn Recipient>> {
43 let key = self.key(host).await?;50 let key = self.key(host).await?;
44 age::ssh::Recipient::from_str(&key)51 age::ssh::Recipient::from_str(&key)
45 .map_err(|e| anyhow!("parse recipient error: {e:?}"))52 .map_err(|e| anyhow!("parse recipient error: {e:?}"))
46 .map(|v| Box::new(v) as Box<dyn Recipient>)53 .map(|v| Box::new(v) as Box<dyn Recipient>)
47 }54 }
4855
49 pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {56 pub async fn recipients(&self, hosts: Vec<SecretOwner>) -> Result<Vec<Box<dyn Recipient>>> {
50 let hosts = self.expand_owner_set(hosts)?;
51 futures::stream::iter(hosts.iter())57 futures::stream::iter(hosts.iter())
52 .then(|m| self.recipient(m.as_ref()))58 .then(|m| self.recipient(m))
53 .try_collect::<Vec<_>>()59 .try_collect::<Vec<_>>()
54 .await60 .await
55 }61 }
58 pub async fn orphaned_data(&self) -> Result<Vec<String>> {64 pub async fn orphaned_data(&self) -> Result<Vec<String>> {
59 let mut out = Vec::new();65 let mut out = Vec::new();
60 let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();66 let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
67 let hosts = self.data.hosts.read().expect("no poisoning");
61 for hostname in self68 for hostname in hosts
62 .data()
63 .hosts
64 .iter()69 .iter()
65 .filter(|(_, host)| !host.encryption_key.is_empty())70 .filter(|(_, host)| !host.encryption_key.is_empty())
66 .map(|(n, _)| n)71 .map(|(n, _)| n)
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
5mod keys;5mod keys;
6pub mod opts;6pub mod opts;
7pub mod primops;7pub mod primops;
8pub mod secret;
9pub mod secret_storage;8pub mod secret_storage;
109
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
1use std::{1use std::{
2 collections::BTreeMap,2 collections::{BTreeMap, BTreeSet},
3 env::current_dir,3 env::current_dir,
4 ffi::OsString,4 ffi::OsString,
5 str::FromStr,5 str::FromStr,
6 sync::{Arc, Mutex},6 sync::{Arc, Mutex},
7};7};
88
9use anyhow::{Context, Result, bail};9use anyhow::{Context, Result, bail};
10use chrono::Utc;
10use nix_eval::{11use nix_eval::{
11 FetchSettings, FlakeLockFlags, FlakeReference, FlakeReferenceParseFlags, FlakeSettings, Value,12 FetchSettings, FlakeLockFlags, FlakeReference, FlakeReferenceParseFlags, FlakeSettings, Value,
12 gc_now, nix_go, util::assert_warn,13 gc_now, nix_go, util::assert_warn,
212 }213 }
213 let bytes =214 let bytes =
214 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;215 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
215 let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));216 let data = Arc::new(FleetData::from_str(&bytes)?);
216217
217 init_primops();218 init_primops();
218219
265 gc_now();266 gc_now();
266 }267 }
267 let config = Config(Arc::new(FleetConfigInternals {268 let config = Config(Arc::new(FleetConfigInternals {
269 // TODO: Load from somewhere
270 prefer_identities: BTreeSet::new(),
271 now: Utc::now(),
272
268 directory,273 directory,
269 data,274 data,
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
4use anyhow::{Context, bail, ensure};4use anyhow::{Context, bail, ensure};
5use fleet_shared::SecretData;5use fleet_shared::SecretData;
6use itertools::Itertools;6use itertools::Itertools;
7use nix_eval::{NativeFn, Value, nix_go, nix_go_json};7use nix_eval::{NativeFn, Value, await_in_nix, nix_go, nix_go_json};
8use serde::Deserialize;8use serde::Deserialize;
9use tracing::{info, warn};9use tracing::{info, warn};
1010
11use crate::fleetdata::{11use crate::fleetdata::{
12 Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,12 Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
13 RegenerationConstraints, SecretOwner,
13};14};
14use crate::host::{Config, ConfigHost};15use crate::host::{Config, ConfigHost};
15use crate::secret::{RegenerationReason, secret_needs_regeneration};
16use anyhow::{Result, anyhow};16use anyhow::{Result, anyhow};
17
18#[derive(thiserror::Error, Debug)]
19enum Error {}
2017
21pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();18pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
2219
28}25}
2926
30pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {27pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {
31 info!("get pkgs");
32 let pkgs = host_on.pkgs()?;28 let pkgs = host_on.pkgs()?;
33 let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);29 let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
34 let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));30 let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
57 Ok(default_generator_drv)53 Ok(default_generator_drv)
58}54}
55
56fn 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 }
77
78 Value::new_attrs(out)
79}
5980
60pub async fn generate(81pub async fn generate(
61 config: &Config,82 config: &Config,
76 } else {97 } else {
77 config.local_host()98 config.local_host()
78 };99 };
100 let mut recipients = Vec::new();
101 for owner in &expectations.owners {
102 recipients.push(config.key(owner).await?);
103 }
79 let pkgs_and_generators =104 let pkgs_and_generators = get_pkgs_and_generators(&host_on, recipients)
80 get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())
81 .context("failed to get pkgs for target host")?;105 .context("failed to get pkgs for target host")?;
82 let generator = call_package(config, &pkgs_and_generators, generator)106 let generator = call_package(config, &pkgs_and_generators, generator)
83 .context("failed to evaluate generator for target host")?;107 .context("failed to evaluate generator for target host")?;
147 generation_data: expectations.generation_data.clone(),171 generation_data: expectations.generation_data.clone(),
148 };172 };
149173
150 let new_data = FleetSecretDistribution {174 let new_data =
151 secret: new_data,
152 owners: expectations.owners.clone(),175 FleetSecretDistribution::new(expectations.owners.clone(), new_data, config.now);
153 _deprecated_managed: true,
154 };
155
156 if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {
157 bail!("newly generated secret needs to be regenerated: {reason}")
158 }
159176
160 Ok(new_data)177 Ok(new_data)
161 }178 }
166}183}
167184
168pub fn init_primops() {185pub fn init_primops() {
169 info!("initializing primops");186 NativeFn::new(
187 c"__fleetEnsureHostSecrets",
188 c"Ensure no extra secrets are stored for the host, pruning unknown",
189 [c"host", c"expectedNonshared", c"expectedShared", c"rest"],
190 |_es, [host, expected_nonshared, expected_shared, rest]| {
191 let host = SecretOwner::host(host.to_string()?);
192 let expected_nonshared: BTreeSet<String> = expected_nonshared.as_json()?;
193 let expected_shared: BTreeSet<String> = expected_shared.as_json()?;
194
195 let mut expected = expected_nonshared;
196 expected.extend(expected_shared);
197
198 let config = PRIMOPS_DATA
199 .get()
200 .expect("primops data should be set on init");
201
202 config
203 .data
204 .secrets
205 .write()
206 .expect("no poisoning")
207 .prune_host(&host, expected);
208
209 Ok(rest.clone())
210 },
211 )
212 .register();
170 NativeFn::new(213 NativeFn::new(
171 c"__fleetEnsureHostSecret",214 c"__fleetEnsureHostSecret",
172 c"Ensure secret existence for a host, regenerating it in case of some mismatch",215 c"Ensure secret existence for a host, regenerating it in case of some mismatch",
173 [c"host", c"secret", c"generator"],216 [c"host", c"secret", c"generator"],
174 |es, [host, secret, generator]| {217 |es, [host, secret, generator]| {
175 info!("get host");
176 let host = host.to_string()?;218 let host = SecretOwner::host(&host.to_string()?);
177 info!("get secret");
178 let secret = secret.to_string()?;219 let secret = secret.to_string()?;
179220
180 info!("get config");
181 let config = PRIMOPS_DATA221 let config = PRIMOPS_DATA
182 .get()222 .get()
183 .expect("primops data should be set on init");223 .expect("primops data should be set on init");
193233
194 ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");234 ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
195235
196 (true, shared_def.generator()?, expected_owners)236 (Some(shared_def.clone()), shared_def.generator()?, expected_owners)
197 } else {237 } else {
198 if shared_def.is_some() {238 if shared_def.is_some() {
199 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")239 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")
200 }240 }
201241
202 (false, generator.clone(), BTreeSet::from_iter([host.clone()]))242 (None, generator.clone(), BTreeSet::from_iter([host.clone()]))
203 };243 };
204244
205 let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;245 let default_generator_drv = get_default_generator_drv(config, &generator)?;
206 let expectations = Expectations {246 let mut expectations = Expectations {
207 parts: nix_go_json!(default_generator_drv.parts),247 parts: nix_go_json!(default_generator_drv.parts),
208 generation_data: nix_go_json!(default_generator_drv.generationData),248 generation_data: nix_go_json!(default_generator_drv.generationData),
209 owners: expected_owners,249 owners: expected_owners.clone(),
210 };250 };
251 let constraints = if let Some(shared) = &shared{
252 RegenerationConstraints {
253 allow_different: nix_go_json!(default_generator_drv.allowDifferent) && shared.allow_different()?,
254 regenerate_on_owner_added: shared.regenerate_on_owner_added()?,
255 regenerate_on_owner_removed: shared.regenerate_on_owner_added()?,
256 }
257 } else {
258 RegenerationConstraints::host_personal()
259 };
211260
212 let reason: RegenerationReason = 'regenerate: {261 let mut secrets = config.data.secrets.write().expect("no poisoning");
213 let Some(existing) = config262 let dists = secrets.get_or_create(&secret);
214 .host_secret(&host, &secret) else {263
215 break 'regenerate RegenerationReason::Missing;
216 };
217 if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {264 if shared.is_some() {
265 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);
266 } else {
218 break 'regenerate reason;267 dists.prune_host(host.clone(), &expectations.parts, &expectations.generation_data, config.now);
219 }268 };
220269
221 let mut parts = expectations.parts.clone();270 if let Some(dist) = dists.get(&host) {
271 return Ok(secret_to_parts(&secret, &dist.secret.parts, &expectations.parts));
272 };
222273
223 let mut out = HashMap::new();274 let mut reencrypt_targets = expectations.owners.clone();
275 for dist in dists.distributions() {
276 for own in dist.owners() {
277 reencrypt_targets.remove(own);
278 }
279 }
224 for (part_name, part) in &existing.secret.parts {280 if !constraints.regenerate_on_owner_added {
225 let Some(definition) = parts.remove(part_name) else {281 if let Some(unpruned) = dists.try_unprune(host.clone()) {
226 warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
227 continue;
228 };
229 assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");282 return Ok(secret_to_parts(&secret, &unpruned.secret.parts, &expectations.parts));
283 } else if let Some(best) = dists.best_distribution_for_reencryption(&config.prefer_identities) {
284 let new_owners = reencrypt_targets.clone();
285 let mut reencrypt_targets = reencrypt_targets;
286 reencrypt_targets.extend(best.owners().cloned());
287
288 let mut preferred = best.owners().collect_vec();
289 preferred.sort_by_key(|v| !config.prefer_identities.contains(*v));
290
291 warn!("reencrypting secret {secret} as it is missing for host {host}");
292
293 for owner in preferred {
230 out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));294 if let Some(hostname) = owner.as_host() && let Ok(host) = config.host(hostname) {
295 let best = best.clone();
296 let reencrypt_targets = reencrypt_targets.clone();
297 let reencrypted = match await_in_nix(async move {
298 host.reencrypt_distribution(&best, reencrypt_targets.clone(), config.now).await
299 }) {
300 Ok(r) => r,
301 Err(e) => {
302 warn!("reencryption failed on {hostname}: {e:?}");
303 continue;
304 }
305 };
306 dists.extend(reencrypted.clone(), format!("secret was reencrypted to extend with new owners: {new_owners:?}"));
307 return Ok(secret_to_parts(&secret, &reencrypted.secret.parts, &expectations.parts));
308 };
309 }
310 warn!("failed to reencrypt using any host")
311 };
231 }312 };
313
314 if constraints.allow_different {
315 for dist in dists.distributions() {
316 for own in dist.owners() {
317 expectations.owners.remove(own);
318 }
319 }
320 }
321 info!("secret {secret} is being generated for {:?}", expectations.owners);
322
323 let expectations_ = expectations.clone();
324 let generated = await_in_nix(async move {
325 generate(config, expectations_, &generator, &default_generator_drv).await
326 })?;
327
232 assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");328 dists.extend(generated.clone(), format!("secret was generated"));
233329
234 return Ok(Value::new_attrs(out))330 return Ok(secret_to_parts(&secret, &generated.secret.parts, &expectations.parts));
235 };
236
237 todo!()
238
239
240 },331 },
deletedcrates/fleet-base/src/secret.rsdiffbeforeafterboth

no changes

modifiedcrates/fleet-shared/Cargo.tomldiffbeforeafterboth
8base64 = "0.22.1"8base64 = "0.22.1"
9serde = "1.0.219"9serde = "1.0.219"
10unicode_categories = "0.1.1"10unicode_categories = "0.1.1"
11z85 = "3.0.6"
1211
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
1use std::{1use std::{
2 collections::BTreeMap,
3 fmt::{self, Display},2 fmt::{self, Display},
4 str::FromStr,3 str::FromStr,
5};4};
15}14}
1615
17const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";16const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";
18const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";
19// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.17// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.
20const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";18const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";
21const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";19const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";
54 STANDARD_NO_PAD52 STANDARD_NO_PAD
55 .decode(unprefixed.replace(['\n', '\t', ' '], ""))53 .decode(unprefixed.replace(['\n', '\t', ' '], ""))
56 .map_err(|e| format!("base64-encoded failed: {e}"))?54 .map_err(|e| format!("base64-encoded failed: {e}"))?
57 } else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {55 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {
58 z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))
59 .map_err(|e| format!("z85-encoded failed: {e}"))?
60 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {
61 unprefixed.as_bytes().to_owned()56 unprefixed.as_bytes().to_owned()
62 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {57 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {
63 unprefixed.as_bytes().to_owned()58 unprefixed.as_bytes().to_owned()
64 } else {59 } else {
65 let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");
66 return Err(format!(60 return Err(format!("unknown secret encoding"));
67 "unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"
68 ));
69 };61 };
70 Ok(Self { data, encrypted })62 Ok(Self { data, encrypted })
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
18test-log = { version = "0.2.18", features = ["trace"] }18test-log = { version = "0.2.18", features = ["trace"] }
19tracing-indicatif = { version = "0.3.13", optional = true }19tracing-indicatif = { version = "0.3.13", optional = true }
20vte = { version = "0.15.0", features = ["ansi"] }20vte = { version = "0.15.0", features = ["ansi"] }
21tokio.workspace = true
2122
22[build-dependencies]23[build-dependencies]
23bindgen = "0.72.0"24bindgen = "0.72.0"
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
2use std::cell::RefCell;2use std::cell::RefCell;
3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};
4use std::ptr::{null, null_mut};4use std::ptr::{null, null_mut};
5use std::sync::LazyLock;5use std::sync::{Arc, LazyLock, OnceLock};
6use std::{array, fmt, slice};6use std::{array, fmt, slice};
7use std::{collections::HashMap, path::PathBuf};7use std::{collections::HashMap, path::PathBuf};
88
9use anyhow::{Context, anyhow, bail};9use anyhow::{Context, anyhow, bail};
10use itertools::Itertools;10use itertools::Itertools;
11use serde::Serialize;11use serde::Serialize;
12use serde::de::DeserializeOwned;12use serde::de::DeserializeOwned;
13use std::mem::transmute;
1314
14pub use anyhow::Result;15pub use anyhow::Result;
15use tracing::{info, instrument, warn};16use tracing::{Instrument, info, instrument, warn};
1617
17use self::logging::{ErrorInfoBuilder, nix_logging_cxx};18use self::logging::{ErrorInfoBuilder, nix_logging_cxx};
18use self::nix_cxx::set_fetcher_setting;19use self::nix_cxx::set_fetcher_setting;
172#[repr(transparent)]173#[repr(transparent)]
173pub struct NixContext(*mut c_context);174pub struct NixContext(*mut c_context);
174impl NixContext {175impl NixContext {
175 pub fn set_err(&mut self, err: NixErrorKind, msg: &CStr) {176 pub fn set_err_raw(&mut self, err: NixErrorKind, msg: &CStr) {
176 unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };177 unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };
177 }178 }
179 pub fn set_err(&mut self, err: anyhow::Error) {
180 let mut fmt = format!("{err:?}").replace("\0", "\\0");
181 self.set_err_raw(
182 NixErrorKind::Generic,
183 &CString::new(fmt).expect("NUL bytes were just replaced"),
184 );
185 }
178 pub fn new() -> Self {186 pub fn new() -> Self {
179 let ctx = unsafe { c_context_create() };187 let ctx = unsafe { c_context_create() };
180 Self(ctx)188 Self(ctx)
931 }939 }
932}940}
941
942static TOKIO_FOR_NIX: OnceLock<Arc<tokio::runtime::Runtime>> = OnceLock::new();
933943
934pub fn init_libraries() {944pub fn init_libraries() {
935 unsafe { GC_allow_register_threads() };945 unsafe { GC_allow_register_threads() };
945 nix_logging_cxx::apply_tracing_logger();955 nix_logging_cxx::apply_tracing_logger();
946}956}
957
958pub fn init_tokio_for_nix(tokio: Arc<tokio::runtime::Runtime>) {
959 TOKIO_FOR_NIX
960 .set(tokio)
961 .expect("tokio for nix should only be initialized once");
962}
963
964pub fn await_in_nix<F: Send + 'static>(f: impl Future<Output = F> + Send + 'static) -> F {
965 // It should be possible to do Handle::current(), but some of the planned features don't work well with that
966 let runtime = TOKIO_FOR_NIX
967 .get()
968 .expect("init_tokio_for_nix was not called");
969 std::thread::spawn(move || runtime.block_on(f)).join().expect("await_in_nix inner thread panicked")
970}
947971
948unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(972unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(
949 user_data: *mut c_void,973 user_data: *mut c_void,
950 context: *mut c_context,974 mut context: *mut c_context,
951 state: *mut nix_raw::EvalState,975 state: *mut nix_raw::EvalState,
952 args: *mut *mut value,976 args: *mut *mut value,
953 ret: *mut value,977 ret: *mut value,
954) {978) {
955 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };979 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };
956 let mut e = None;
957 let args: [&Value; N] = array::from_fn(|i| {980 let args: [&Value; N] = array::from_fn(|i| {
958 let v: &mut Value = unsafe { &mut *args.add(i).cast() };981 let v: &mut Value = unsafe { &mut *args.add(i).cast() };
959
960 info!("forcing arg");
961 if matches!(v.type_of(), NixType::Thunk)
962 && let Err(err) = v.force(state)
963 {
964 e = Some(err);
965 };
966 v as &Value982 v as &Value
967 });983 });
968 info!("args forced");
969 let ctx: &mut NixContext = unsafe { &mut *context.cast() };984 let ctx: &mut NixContext = unsafe { transmute(&mut context) };
970
971 if let Some(e) = e {
972 warn!("set err = {e}");
973 unsafe { init_int(context, ret, 0) };
974 return ctx.set_err(
975 NixErrorKind::Unknown,
976 &CString::new(e.to_string()).expect("forcing argument value failed"),
977 );
978 }
979985
980 let state: &EvalState = unsafe { std::mem::transmute(&state) };986 let state: &EvalState = unsafe { std::mem::transmute(&state) };
981987
984 unsafe { copy_value(context, ret, v.0) };990 unsafe { copy_value(context, ret, v.0) };
985 }991 }
986 Err(e) => {992 Err(e) => {
987 unsafe { init_int(context, ret, 0) };
988 warn!("set err = {e:#?}");
989 ctx.set_err(993 ctx.set_err(e);
990 NixErrorKind::Unknown,
991 &CString::new(e.to_string()).expect("error should not contain internal nuls"),
992 );
993 }994 }
994 }995 }
modifiedcrates/nix-eval/src/macros.rsdiffbeforeafterboth
68#[macro_export]68#[macro_export]
69macro_rules! nix_go {69macro_rules! nix_go {
70 (@o($o:expr, $path:expr) . $var:ident $($tt:tt)*) => {{70 (@o($o:expr, $path:expr) . $var:ident $($tt:tt)*) => {{
71 nix_go!(@o($o.get_field(stringify!($var)).context(concat!("getting nested ", $path))?, $path) $($tt)*)71 nix_go!(@o(tokio::task::block_in_place(|| $o.get_field(stringify!($var))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
72 }};72 }};
73 (@o($o:expr, $path:expr) [ $v:expr ] $($tt:tt)*) => {{73 (@o($o:expr, $path:expr) [ $v:expr ] $($tt:tt)*) => {{
74 nix_go!(@o($o.get_field($v).context(concat!("getting nested ", $path))?, $path) $($tt)*)74 nix_go!(@o(tokio::task::block_in_place(|| $o.get_field($v)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
75 }};75 }};
76 (@o($o:expr, $path:expr) ($($var:tt)*) $($tt:tt)*) => {76 (@o($o:expr, $path:expr) ($($var:tt)*) $($tt:tt)*) => {
77 nix_go!(@o($o.call($crate::nix_expr_inner!($($var)+)).context(concat!("getting nested ", $path))?, $path) $($tt)*)77 nix_go!(@o(tokio::task::block_in_place(|| $o.call($crate::nix_expr_inner!($($var)+))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
78 };78 };
79 (@o($o:expr, $path:expr)) => {$o};79 (@o($o:expr, $path:expr)) => {$o};
80 ($field:ident $($tt:tt)+) => {{80 ($field:ident $($tt:tt)+) => {{
87#[macro_export]87#[macro_export]
88macro_rules! nix_go_json {88macro_rules! nix_go_json {
89 ($($tt:tt)*) => {{89 ($($tt:tt)*) => {{
90 $crate::nix_go!($($tt)*).as_json()?90 tokio::task::block_in_place(|| $crate::nix_go!($($tt)*).as_json())?
91 }};91 }};
92}92}
9393
modifiedflake.lockdiffbeforeafterboth
2 "nodes": {2 "nodes": {
3 "crane": {3 "crane": {
4 "locked": {4 "locked": {
5 "lastModified": 1767461147,5 "lastModified": 1768700043,
6 "owner": "ipetkov",6 "owner": "ipetkov",
7 "repo": "crane",7 "repo": "crane",
8 "rev": "7d59256814085fd9666a2ae3e774dc5ee216b630",8 "rev": "935de8bd6838d940988bb065be2a2034259327b9",
9 "type": "github"9 "type": "github"
10 },10 },
11 "original": {11 "original": {
37 ]37 ]
38 },38 },
39 "locked": {39 "locked": {
40 "lastModified": 1767609335,40 "lastModified": 1768135262,
41 "owner": "hercules-ci",41 "owner": "hercules-ci",
42 "repo": "flake-parts",42 "repo": "flake-parts",
43 "rev": "250481aafeb741edfe23d29195671c19b36b6dca",43 "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
44 "type": "github"44 "type": "github"
45 },45 },
46 "original": {46 "original": {
111 "nixpkgs-regression": "nixpkgs-regression"111 "nixpkgs-regression": "nixpkgs-regression"
112 },112 },
113 "locked": {113 "locked": {
114 "lastModified": 1767670640,114 "lastModified": 1768702010,
115 "owner": "deltarocks",115 "owner": "deltarocks",
116 "repo": "nix",116 "repo": "nix",
117 "rev": "2181cd07134c9049bd77b7f48c3b1ea8647267de",117 "rev": "b05b52670b9c7affff5b9be3edb539a1603c39e6",
118 "type": "github"118 "type": "github"
119 },119 },
120 "original": {120 "original": {
126 },126 },
127 "nixpkgs": {127 "nixpkgs": {
128 "locked": {128 "locked": {
129 "lastModified": 1767657734,129 "lastModified": 1768697925,
130 "owner": "nixos",130 "owner": "nixos",
131 "repo": "nixpkgs",131 "repo": "nixpkgs",
132 "rev": "d4ccebf51ee4dbeb9df364dce1fe9848635c1258",132 "rev": "665062f7df2c7db8fdbbec4f1b730091143828a3",
133 "type": "github"133 "type": "github"
134 },134 },
135 "original": {135 "original": {
190 ]190 ]
191 },191 },
192 "locked": {192 "locked": {
193 "lastModified": 1767667566,193 "lastModified": 1768617670,
194 "owner": "oxalica",194 "owner": "oxalica",
195 "repo": "rust-overlay",195 "repo": "rust-overlay",
196 "rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e",196 "rev": "56d0fbdd732f3686e8414b857cf885038fc17d57",
197 "type": "github"197 "type": "github"
198 },198 },
199 "original": {199 "original": {
223 ]223 ]
224 },224 },
225 "locked": {225 "locked": {
226 "lastModified": 1767468822,226 "lastModified": 1768158989,
227 "owner": "numtide",227 "owner": "numtide",
228 "repo": "treefmt-nix",228 "repo": "treefmt-nix",
229 "rev": "d56486eb9493ad9c4777c65932618e9c2d0468fc",229 "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
230 "type": "github"230 "type": "github"
231 },231 },
232 "original": {232 "original": {
modifiedmodules/nixos.nixdiffbeforeafterboth
10let10let
11 inherit (lib.attrsets) mapAttrs;11 inherit (lib.attrsets) mapAttrs;
12 inherit (lib.options) mkOption;12 inherit (lib.options) mkOption;
13 inherit (lib.types) deferredModule unspecified;13 inherit (lib.types) deferredModule unspecified uniq str;
14 inherit (lib.strings) escapeNixIdentifier;14 inherit (lib.strings) escapeNixIdentifier;
15 inherit (fleetLib.options) mkHostsOption;15 inherit (fleetLib.options) mkHostsOption;
1616
24 '';24 '';
25 type = deferredModule;25 type = deferredModule;
26 };26 };
27 hosts = mkHostsOption (hostArgs: {27 hosts = mkHostsOption (hostArgs: let
28 hostName = hostArgs.config._module.args.name;
29 in {
28 inherit _file;30 inherit _file;
29 options = {31 options = {
32 name = mkOption {
33 description = ''
34 Host name (alias)
35 '';
36 type = uniq str;
37 default = hostName;
38 };
30 nixos = mkOption {39 nixos = mkOption {
31 description = ''40 description = ''
32 Nixos configuration for the current host.41 Nixos configuration for the current host.
42 prefix = [51 prefix = [
43 "fleetConfiguration"52 "fleetConfiguration"
44 "hosts"53 "hosts"
45 hostArgs.config._module.args.name54 hostName
46 "nixos"55 "nixos"
47 ];56 ];
48 modules = (import "${modulesPath}/module-list.nix") ++ [57 modules = (import "${modulesPath}/module-list.nix") ++ [
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
18 inherit (lib.attrsets) mapAttrs mapAttrsToList;18 inherit (lib.attrsets)
19 mapAttrs
20 mapAttrsToList
21 filterAttrs
22 attrNames
23 ;
19 inherit (lib.modules) mkIf;24 inherit (lib.modules) mkIf;
20 inherit (lib.types)25 inherit (lib.types)
25 uniq30 uniq
26 functionTo31 functionTo
27 package32 package
28 bool
29 enum33 enum
30 either34 either
35 listOf
31 ;36 ;
32 inherit (fleetLib.strings) decodeRawSecret;37 inherit (fleetLib.strings) decodeRawSecret;
3338
132in137in
133{138{
134 options = {139 options = {
140 _providedSharedSecrets = mkOption {
141 description = ''
142 List of shared secrets, for which the current host was specified as `expectedOwners`
143 '';
144 type = listOf str;
145 default = [];
146 internal = true;
147 };
135 secrets = mkOption {148 secrets = mkOption {
136 type = attrsOf secretType;149 type = attrsOf secretType;
137 default = { };150 default = { };
138 apply = mapAttrs (_: secret: secret.parts // { definition = secret; });151 apply =
152 secrets:
153 mapAttrs (_: secret: secret.parts // { definition = secret; })
154
155 (
156 let
157 hostName = host.name;
158 expectedNonshared = attrNames (filterAttrs (_: def: def.generator != "shared") secrets);
159 expectedShared = config._providedSharedSecrets;
160 in
161 builtins.deepSeq [
162 hostName
163 expectedNonshared
164 expectedShared
165 ] (builtins.fleetEnsureHostSecrets hostName expectedNonshared expectedShared secrets)
166 );
139 description = "Host-local secrets";167 description = "Host-local secrets";
140 };168 };
141 system.secretsData = mkOption {169 system.secretsData = mkOption {
163 (secret.definition.generator == "shared") == hasSharedDefinition191 (secret.definition.generator == "shared") == hasSharedDefinition
164 && (192 && (
165 hasSharedDefinition193 hasSharedDefinition
166 -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners)194 -> (elem host.name fleetConfiguration.secrets.${name}.expectedOwners)
167 );195 );
168 message =196 message =
169 if hasSharedDefinition then197 if hasSharedDefinition then
modifiedmodules/secrets.nixdiffbeforeafterboth
1{1{
2 lib,2 lib,
3 config,
3 ...4 ...
4}:5}:
5let6let
6 inherit (lib.options) mkOption literalExpression;7 inherit (lib.options) mkOption;
7 inherit (lib.types)8 inherit (lib.types)
8 nullOr9 nullOr
9 listOf10 listOf
16 uniq17 uniq
17 ;18 ;
18 inherit (lib.strings) concatStringsSep;19 inherit (lib.strings) concatStringsSep;
20 inherit (lib.lists) elem filter;
21 inherit (lib.attrsets) attrNames;
1922
20 sharedSecret =23 sharedSecret =
21 { config, ... }:24 { config, ... }:
29 };32 };
30 regenerateOnOwnerAdded = mkOption {33 regenerateOnOwnerAdded = mkOption {
31 type = bool;34 type = bool;
32 description = ''35 description = ''
33 Controls whether the secret must be regenerated when new owners are added.36 Whether the secret prefers to be rotated when new owners are added.
3437
35 Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).38 Note that this is only a security measure, if the secret needs to be regenerated due to e.g X.509 SANs
36 When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.39 changes - then you most likely want to use generationData for that instead.
37 '';40 '';
41 default = false;
38 };42 };
39 regenerateOnOwnerRemoved = mkOption {43 regenerateOnOwnerRemoved = mkOption {
40 default = config.regenerateOnOwnerAdded;
41 defaultText = literalExpression "regenerateOnOwnerAdded";
42 type = bool;44 type = bool;
43 description = ''45 description = ''
44 Determines secret behavior when owners are removed from the configuration.46 Whether the secret prefers to be rotated when the owners are removed, so the encrypted data
4547 stored in fleet state can't be decrypted by those. Note that the secrets are still present in encrypted
46 Typically mirrors regenerateOnOwnerAdded. Override cautiously.48 form on those hosts until gc happens.
47 Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
48 '';49 '';
50 default = false;
49 };51 };
50 allowDifferent = mkOption {52 allowDifferent = mkOption {
51 type = bool;53 type = bool;
52 description = ''54 description = ''
53 When adding owner, do not update secret value for other owners, instead creating a new distribution55 When adding owner, do not update secret value for other owners, instead creating a new distribution.
56
57 Defaults to true, since all secrets might differ on hosts on some point of deployment process.
58
59 Secret generator might also have opinion on this, like it makes little sense for askPass/synchronizing
60 generators to keep old data.
54 '';61 '';
62 default = true;
55 };63 };
56 generator = mkOption {64 generator = mkOption {
57 type = uniq (nullOr (functionTo package));65 type = uniq (nullOr (functionTo package));
75 };83 };
76 };84 };
77 config = {85 config = {
86 nixos = {host, ...}: {
87 _providedSharedSecrets = filter (name: elem host.name config.secrets.${name}.expectedOwners) (attrNames config.secrets);
88 };
78 nixpkgs.overlays = [89 nixpkgs.overlays = [
79 (final: prev: {90 (final: prev: {
80 mkSecretGenerators =91 mkSecretGenerators =
90 # (Some secrets-encryption-in-git/managed PKI solution is expected)101 # (Some secrets-encryption-in-git/managed PKI solution is expected)
91 impureOn ? null,102 impureOn ? null,
92 generationData ? null,103 generationData ? null,
104 allowDifferent ? true,
93 parts,105 parts,
94 }:106 }:
95 (prev.writeShellScript "impureGenerator.sh" ''107 (prev.writeShellScript "impureGenerator.sh" ''
134 impureOn
135 parts
136 generationData
137 allowDifferent
138 ;
122 generatorKind = "impure";139 generatorKind = "impure";
123 };140 };