1use std::{2 cmp::Ordering,3 collections::{4 BTreeMap, BTreeSet,5 btree_map::{self, Entry},6 },7 fmt,8 io::{self, Cursor},9 sync::RwLock,10};1112use age::Recipient;13use chrono::{DateTime, Utc};14use fleet_shared::SecretData;15use rand::{16 distr::{Alphanumeric, SampleString as _},17 rng,18};19use serde::{20 Deserialize, Serialize,21 de::{self, Error},22};23use serde_json::Value;24use tracing::info;2526#[derive(Serialize, Deserialize, Default)]27#[serde(rename_all = "camelCase")]28pub struct HostData {29 #[serde(default)]30 #[serde(skip_serializing_if = "String::is_empty")]31 pub encryption_key: String,32}3334const VERSION: &str = "0.1.0";35pub struct FleetDataVersion;36impl Serialize for FleetDataVersion {37 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>38 where39 S: serde::Serializer,40 {41 VERSION.serialize(serializer)42 }43}44impl<'de> Deserialize<'de> for FleetDataVersion {45 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>46 where47 D: serde::Deserializer<'de>,48 {49 let version = String::deserialize(deserializer)?;50 if version != VERSION {51 return Err(D::Error::custom(format!(52 "fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"53 )));54 }55 Ok(Self)56 }57}5859fn generate_gc_prefix() -> String {60 let id = Alphanumeric.sample_string(&mut rng(), 8);61 format!("fleet-gc-{id}")62}6364#[derive(Serialize, Deserialize)]65#[serde(rename_all = "camelCase")]66pub struct ManagerKey {67 pub name: String,68 pub key: String,69}7071#[derive(Serialize, Deserialize)]72#[serde(rename_all = "camelCase")]73pub struct FleetData {74 pub version: FleetDataVersion,75 #[serde(default = "generate_gc_prefix")]76 pub gc_root_prefix: String,7778 #[serde(default, skip_serializing_if = "Vec::is_empty")]79 pub manager_keys: Vec<ManagerKey>,8081 #[serde(default)]82 pub hosts: RwLock<BTreeMap<String, HostData>>,8384 #[serde(default, alias = "shared_secrets")]85 pub secrets: RwLock<FleetSecrets>,8687 88 #[serde(default)]89 pub extra: RwLock<BTreeMap<String, Value>>,9091 #[serde(default)]92 #[serde(skip_serializing)]93 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,94}95impl FleetData {96 pub fn from_str(s: &str) -> anyhow::Result<Self> {97 let mut data: Self = nixlike::parse_str(s)?;98 if !data.host_secrets.is_empty() {99 info!("migrating host secrets into shared secrets structure");100 data.secrets101 .write()102 .expect("no poisoning")103 .merge_from_hosts(std::mem::take(&mut data.host_secrets));104 }105 Ok(data)106 }107}108109110pub fn encrypt_secret_data<'r>(111 recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,112 data: Vec<u8>,113) -> Option<SecretData> {114 let mut encrypted = vec![];115 let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))116 .ok()?117 .wrap_output(&mut encrypted)118 .expect("in memory write");119 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");120 encryptor.finish().expect("in memory flush");121 Some(SecretData {122 data: encrypted,123 encrypted: true,124 })125}126127#[derive(Serialize, Deserialize, Clone, Debug)]128pub struct FleetSecretPart {129 pub raw: SecretData,130}131132#[derive(Serialize, Deserialize, Clone, Debug)]133#[serde(rename_all = "camelCase")]134#[must_use]135pub struct FleetSecretData {136 pub created_at: DateTime<Utc>,137 #[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]138 pub expires_at: Option<DateTime<Utc>>,139140 #[serde(flatten)]141 pub parts: BTreeMap<String, FleetSecretPart>,142143 #[serde(default, skip_serializing_if = "Value::is_null")]144 pub generation_data: Value,145}146147#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]148#[repr(transparent)]149pub struct SecretOwner(String);150151impl fmt::Display for SecretOwner {152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {153 write!(f, "host:{}", self.0)154 }155}156157impl SecretOwner {158 pub fn host(s: impl AsRef<str>) -> SecretOwner {159 SecretOwner(s.as_ref().to_owned())160 }161 pub fn as_host(&self) -> Option<&str> {162 Some(&self.0)163 }164}165166#[derive(Serialize, Deserialize, Clone, Debug)]167#[serde(rename_all = "camelCase")]168#[must_use]169pub struct FleetSecretDistribution {170 #[serde(default)]171 owners: BTreeSet<SecretOwner>,172 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]173 owners_pending_prune: BTreeMap<SecretOwner, String>,174175 #[serde(flatten)]176 pub secret: FleetSecretData,177178 #[serde(default, skip_serializing_if = "Option::is_none")]179 pending_prune: Option<String>,180 #[serde(default, skip_serializing, alias = "managed")]181 _deprecated_managed: bool,182}183184const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();185impl FleetSecretDistribution {186 pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {187 assert!(188 !owners.is_empty(),189 "distribution should have at least one owner"190 );191 if let Some(expires_at) = &secret.expires_at {192 assert!(193 *expires_at > now,194 "secret should not be expired on creation"195 );196 }197 Self {198 owners,199 secret,200 owners_pending_prune: BTreeMap::new(),201 pending_prune: None,202 _deprecated_managed: true,203 }204 }205206 fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {207 let pending_prune = if including_pruned {208 &self.owners_pending_prune209 } else {210 EMPTY_PENDING_PRUNE211 };212 self.owners.iter().chain(pending_prune.keys())213 }214 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {215 self.owners_ex(false)216 }217 pub fn owners_pending_prune(&self) -> impl Iterator<Item = &SecretOwner> {218 self.owners_pending_prune.keys()219 }220 pub fn is_pending_prune(&self) -> bool {221 self.pending_prune.is_some()222 }223224 pub fn prune(&mut self, reason: String) {225 assert!(226 self.pending_prune.is_none(),227 "it shouldn't be possible to prune the same distribution twice using public api"228 );229 self.pending_prune = Some(reason);230 }231 pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {232 233 234 235 236 for owner in owners {237 if self.owners.remove(owner) {238 self.owners_pending_prune239 .insert(owner.to_owned(), reason.clone());240 }241 }242 243 244 245 }246 pub fn unprune_owner(&mut self, owner: SecretOwner) {247 if self.owners_pending_prune.remove(&owner).is_some() {248 self.owners.insert(owner);249 }250 }251}252253#[derive(Clone, Debug, Default)]254#[must_use]255pub struct FleetSecretDistributions {256 stored: Vec<FleetSecretDistribution>,257}258259fn compare_dists(260 a: &FleetSecretDistribution,261 b: &FleetSecretDistribution,262 prefer_identities: &BTreeSet<SecretOwner>,263 include_pruned_owners: bool,264) -> Ordering {265 use Ordering::*;266 if prefer_identities.is_empty() {267 let a_has = a268 .owners_ex(include_pruned_owners)269 .any(|o| prefer_identities.contains(o));270 let b_has = b271 .owners_ex(include_pruned_owners)272 .any(|o| prefer_identities.contains(o));273 match (a_has, b_has) {274 (true, false) => return Greater,275 (false, true) => return Less,276 _ => {}277 }278 }279 match (a.secret.expires_at, b.secret.expires_at) {280 (None, Some(_)) => return Greater,281 (Some(_), None) => return Less,282 (Some(a), Some(b)) => {283 284 return a.cmp(&b);285 }286 (None, None) => {}287 }288289 290 a.owners.len().cmp(&b.owners.len())291}292293impl FleetSecretDistributions {294 295 fn prune_expired(&mut self, now: DateTime<Utc>) {296 for ele in self.distributions_mut() {297 if let Some(expires_at) = ele.secret.expires_at {298 if expires_at < now {299 ele.prune(format!("expired during check at {now}"));300 }301 }302 }303 }304 305 306 pub fn prune_shared(307 &mut self,308 expected_owners: &BTreeSet<SecretOwner>,309 unique: bool,310 expected_parts: &BTreeMap<String, GeneratorPart>,311 expected_generation_data: &Value,312 regenerate_on_owner_removed: bool,313 regenerate_on_owner_added: bool,314 prefer_identities: &BTreeSet<SecretOwner>,315 now: DateTime<Utc>,316 ) {317 self.prune_expired(now);318 self.prune_generation_data(expected_generation_data, None);319 self.prune_missing_parts(expected_parts, None);320321 let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();322323 let mut to_add = expected_owners.difference(¤t_owners);324 if to_add.next().is_some() && unique && regenerate_on_owner_added {325 for dist in self.distributions_mut() {326 dist.prune(format!(327 "owners missing, can't add new distribution, regeneration preferred"328 ));329 }330 return;331 }332333 for to_remove in current_owners.difference(&expected_owners) {334 self.entry(to_remove.clone()).remove(335 regenerate_on_owner_removed,336 "owner was removed from expected owners list, regenerate_on_owner_removed is set"337 .to_string(),338 );339 }340 if unique {341 self.prune_nonunique(prefer_identities);342 }343 }344 pub fn prune_host(345 &mut self,346 owner: SecretOwner,347 expected_parts: &BTreeMap<String, GeneratorPart>,348 expected_generation_data: &Value,349 now: DateTime<Utc>,350 ) {351 self.prune_expired(now);352 self.prune_generation_data(expected_generation_data, Some(&owner));353 354 self.prune_missing_parts(expected_parts, Some(&owner));355 }356 357 358 fn best_idx(359 &self,360 prefer_identities: &BTreeSet<SecretOwner>,361 include_pruned_owners: bool,362 ) -> Option<usize> {363 self.distributions()364 .enumerate()365 .max_by(|(_, a), (_, b)| {366 compare_dists(&a, &b, prefer_identities, include_pruned_owners)367 })368 .map(|(p, _)| p)369 }370 371 fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {372 if self.distributions().next().is_none() {373 return;374 }375 let best = self.best_idx(prefer_identities, false).expect("not empty");376 for (i, dist) in self.distributions_mut().enumerate() {377 if i != best {378 dist.prune(379 "secret wants to be the same on all hosts, only the best one was left"380 .to_owned(),381 );382 }383 }384 }385386 pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {387 assert!(self.get(&owner).is_none(), "secret is not pruned for host");388 if let Some(dist) = self389 .distributions_mut()390 .find(|v| v.owners_pending_prune.contains_key(&owner))391 {392 dist.unprune_owner(owner);393 Some(dist)394 } else {395 None396 }397 }398399 pub fn best_distribution_for_reencryption(400 &mut self,401 prefer_identities: &BTreeSet<SecretOwner>,402 ) -> Option<&mut FleetSecretDistribution> {403 let best_idx = self.best_idx(prefer_identities, true)?;404 self.distributions_mut().nth(best_idx)405 }406407 fn prune_missing_parts(408 &mut self,409 expected_parts: &BTreeMap<String, GeneratorPart>,410 filter_owner: Option<&SecretOwner>,411 ) {412 'dist: for ele in self.distributions_mut() {413 if let Some(filter_owner) = filter_owner {414 if !ele.owners.contains(filter_owner) {415 continue;416 }417 418 419 }420 for (name, part) in expected_parts {421 let Some(stored_part) = ele.secret.parts.get(name) else {422 ele.prune(format!("secret definition added new part: {name}"));423 continue 'dist;424 };425 if part.encrypted != stored_part.raw.encrypted {426 ele.prune(format!(427 "secret definition now requires part to be {}",428 if part.encrypted {429 "encrypted"430 } else {431 "non-encrypted"432 }433 ));434 continue 'dist;435 }436 }437 }438 }439 fn prune_generation_data(440 &mut self,441 expected_generation_data: &Value,442 filter_owner: Option<&SecretOwner>,443 ) {444 for ele in self.distributions_mut() {445 if let Some(filter_owner) = filter_owner {446 if !ele.owners.contains(filter_owner) {447 continue;448 }449 450 451 }452 if ele.secret.generation_data != *expected_generation_data {453 ele.prune(format!(454 "expected generation data mismatch: {expected_generation_data:?}"455 ));456 }457 }458 }459460 461 462 463 fn prune_dead(&mut self) {464 for ele in self.distributions_mut() {465 if ele.owners.is_empty() {466 ele.prune("no owners left".to_owned());467 }468 }469 }470471 pub fn all_distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {472 self.stored.iter()473 }474 pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {475 self.stored.iter().filter(|v| v.pending_prune.is_none())476 }477 pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {478 self.stored.iter_mut().filter(|v| v.pending_prune.is_none())479 }480 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {481 self.distributions().flat_map(|v| v.owners.iter())482 }483 #[allow(484 clippy::len_without_is_empty,485 reason = "should not be empty for a long time"486 )]487 pub fn len(&self) -> usize {488 self.distributions().count()489 }490491 pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {492 self.distributions().find(|d| d.owners.contains(owner))493 }494 fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {495 let Some((idx, dist)) = self496 .distributions()497 .enumerate()498 .find(|(_, d)| d.owners.contains(&owner))499 else {500 return DistEntry::Vacant(VacantDistEntry {501 distributions: self,502 owners: BTreeSet::from([owner]),503 });504 };505 DistEntry::Occupied(OccupiedDistEntry {506 owners: dist.owners.clone(),507 distributions: self,508 idx,509 })510 }511 pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {512 for ele in self.distributions_mut() {513 ele.prune_owners(&dist.owners, reason.clone());514 }515 self.stored.push(dist);516 }517 pub fn contains(&self, owner: &SecretOwner) -> bool {518 self.distributions().any(|d| d.owners.contains(owner))519 }520}521522struct OccupiedDistEntry<'d> {523 distributions: &'d mut FleetSecretDistributions,524 idx: usize,525 owners: BTreeSet<SecretOwner>,526}527impl<'d> OccupiedDistEntry<'d> {528 fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {529 let dist = &mut self.distributions.stored[self.idx];530 if whole_dist {531 dist.prune(reason);532 } else {533 dist.prune_owners(&self.owners, reason);534 }535 VacantDistEntry {536 distributions: self.distributions,537 owners: self.owners,538 }539 }540 fn set(self, secret: FleetSecretData, reason: String) -> Self {541 self.remove(false, reason).set(secret)542 }543}544struct VacantDistEntry<'d> {545 distributions: &'d mut FleetSecretDistributions,546 owners: BTreeSet<SecretOwner>,547}548impl<'d> VacantDistEntry<'d> {549 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {550 let Self {551 distributions,552 owners,553 } = self;554 let idx = distributions.stored.len();555 distributions.stored.push(FleetSecretDistribution {556 owners: owners.clone(),557 secret,558559 owners_pending_prune: BTreeMap::new(),560 pending_prune: None,561 _deprecated_managed: true,562 });563 OccupiedDistEntry {564 distributions,565 owners,566 idx,567 }568 }569}570571enum DistEntry<'d> {572 Vacant(VacantDistEntry<'d>),573 Occupied(OccupiedDistEntry<'d>),574}575impl DistEntry<'_> {576 fn remove(self, whole_dist: bool, reason: String) -> Self {577 match self {578 DistEntry::Vacant(_) => self,579 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),580 }581 }582 fn set(self, secret: FleetSecretData, reason: String) -> Self {583 Self::Occupied(match self {584 DistEntry::Vacant(e) => e.set(secret),585 DistEntry::Occupied(e) => e.set(secret, reason),586 })587 }588}589590impl Serialize for FleetSecretDistributions {591 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>592 where593 S: serde::Serializer,594 {595 let mut v = self.clone();596 v.prune_dead();597 let mut found_hosts = BTreeSet::new();598 for ele in v.distributions() {599 if ele.pending_prune.is_some() {600 continue;601 }602 if ele.owners.is_empty() {603 panic!("consistency: secret distribution has no defined owners");604 }605 for ele in ele.owners.iter() {606 if !found_hosts.insert(ele) {607 panic!(608 "consistency: secret distribution contains duplicate entry for the same host",609 );610 }611 }612 }613 match v.stored.len() {614 0 => panic!("consistency: empty distributions"),615 1 => v.stored[0].serialize(serializer),616 _ => {617 let mut sorted = v.stored.clone();618 619 sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);620 sorted.serialize(serializer)621 }622 }623 }624}625impl<'de> Deserialize<'de> for FleetSecretDistributions {626 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>627 where628 D: serde::Deserializer<'de>,629 {630 #[derive(Deserialize)]631 #[serde(untagged)]632 enum Distributions {633 One(FleetSecretDistribution),634 Many(Vec<FleetSecretDistribution>),635 }636 let d = Distributions::deserialize(deserializer)?;637 let stored = match d {638 Distributions::One(d) => vec![d],639 Distributions::Many(ds) => ds,640 };641 if stored.is_empty() {642 return Err(de::Error::custom("consistency: empty distributions"));643 }644 let mut found_hosts = BTreeSet::new();645 for ele in stored.iter() {646 if ele.pending_prune.is_some() {647 continue;648 }649 if ele.owners.is_empty() {650 return Err(de::Error::custom(651 "consistency: secret distribution has no defined owners",652 ));653 }654 for ele in ele.owners.iter() {655 if !found_hosts.insert(ele) {656 return Err(de::Error::custom(657 "consistency: secret distribution contains duplicate entry for the same host",658 ));659 }660 }661 }662 Ok(Self { stored })663 }664}665666#[derive(Deserialize, Default)]667pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);668669impl Serialize for FleetSecrets {670 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>671 where672 S: serde::Serializer,673 {674 let data: BTreeMap<String, FleetSecretDistributions> = self675 .0676 .iter()677 .filter(|(_, v)| !v.stored.is_empty())678 .map(|(k, v)| (k.clone(), v.clone()))679 .collect();680681 data.serialize(serializer)682 }683}684685impl FleetSecrets {686 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {687 self.0.keys()688 }689690 pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {691 self.0692 .iter()693 .filter(|(_, d)| d.contains(owner))694 .map(|(n, _)| n)695 }696697 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {698 match self.0.entry(secret) {699 Entry::Vacant(e) => {700 e.insert(FleetSecretDistributions { stored: vec![data] });701 }702 Entry::Occupied(mut e) => {703 let dists = e.get_mut();704 dists.extend(data, "secret data was replaced".to_owned())705 }706 }707 }708 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {709 self.0.get(secret)710 }711 pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {712 self.0.get_mut(secret)713 }714715 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {716 self.0717 .entry(secret.to_owned())718 .or_insert(FleetSecretDistributions::default())719 }720721 pub fn contains(&self, secret: &str) -> bool {722 self.0.contains_key(secret)723 }724 pub fn remove(&mut self, secret: &str) {725 self.0.remove(secret);726 }727728 fn merge_from_hosts(729 &mut self,730 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,731 ) {732 for (host, host_secrets) in host_secrets {733 for (secret_name, mut secret_data) in host_secrets {734 secret_data.owners.insert(host.clone());735 self.set_data(secret_name, secret_data);736 }737 }738 }739740 pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {741 for (name, dists) in self.0.iter_mut() {742 if expected_nonshared.contains(name) {743 continue;744 }745 for dist in dists.distributions_mut() {746 if dist.owners.contains(host) {747 dist.prune_owners(748 &BTreeSet::from([host.to_owned()]),749 "host no longer defines this secret".to_owned(),750 );751 }752 }753 }754 }755}756757#[derive(Debug, Clone)]758pub struct Expectations {759 pub owners: BTreeSet<SecretOwner>,760 pub generation_data: serde_json::Value,761 pub parts: BTreeMap<String, GeneratorPart>,762}763#[derive(Deserialize, Debug, Clone)]764pub struct GeneratorPart {765 pub encrypted: bool,766}767768#[derive(Debug, Clone, Copy)]769pub struct RegenerationConstraints {770 pub allow_different: bool,771 pub regenerate_on_owner_added: bool,772 pub regenerate_on_owner_removed: bool,773}774impl RegenerationConstraints {775 pub fn host_personal() -> Self {776 Self {777 allow_different: false,778 regenerate_on_owner_added: true,779 regenerate_on_owner_removed: true,780 }781 }782 pub fn without_preferences(self) -> Self {783 Self {784 allow_different: self.allow_different,785 regenerate_on_owner_added: false,786 regenerate_on_owner_removed: false,787 }788 }789}