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}146147fn is_false(b: &bool) -> bool {148 *b == false149}150151#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]152#[repr(transparent)]153pub struct SecretOwner(String);154155impl fmt::Display for SecretOwner {156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {157 write!(f, "host:{}", self.0)158 }159}160161impl 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}169170#[derive(Serialize, Deserialize, Clone, Debug)]171#[serde(rename_all = "camelCase")]172#[must_use]173pub struct FleetSecretDistribution {174 #[serde(default)]175 owners: BTreeSet<SecretOwner>,176 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]177 owners_pending_prune: BTreeMap<SecretOwner, String>,178179 #[serde(flatten)]180 pub secret: FleetSecretData,181182 #[serde(default, skip_serializing_if = "Option::is_none")]183 pending_prune: Option<String>,184 #[serde(default, skip_serializing, alias = "managed")]185 _deprecated_managed: bool,186}187188const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();189impl 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 }209210 fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {211 let pending_prune = if including_pruned {212 &self.owners_pending_prune213 } else {214 EMPTY_PENDING_PRUNE215 };216 self.owners.iter().chain(pending_prune.keys())217 }218 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {219 self.owners_ex(false)220 }221 pub fn owners_pending_prune(&self) -> impl Iterator<Item = &SecretOwner> {222 self.owners_pending_prune.keys()223 }224 pub fn is_pending_prune(&self) -> bool {225 self.pending_prune.is_some()226 }227228 pub fn prune(&mut self, reason: String) {229 assert!(230 self.pending_prune.is_none(),231 "it shouldn't be possible to prune the same distribution twice using public api"232 );233 self.pending_prune = Some(reason);234 }235 pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {236 237 238 239 240 for owner in owners {241 if self.owners.remove(owner) {242 self.owners_pending_prune243 .insert(owner.to_owned(), reason.clone());244 }245 }246 247 248 249 }250 pub fn unprune_owner(&mut self, owner: SecretOwner) {251 if self.owners_pending_prune.remove(&owner).is_some() {252 self.owners.insert(owner);253 }254 }255}256257#[derive(Clone, Debug, Default)]258#[must_use]259pub struct FleetSecretDistributions {260 stored: Vec<FleetSecretDistribution>,261}262263fn compare_dists(264 a: &FleetSecretDistribution,265 b: &FleetSecretDistribution,266 prefer_identities: &BTreeSet<SecretOwner>,267 include_pruned_owners: bool,268) -> Ordering {269 use Ordering::*;270 if prefer_identities.is_empty() {271 let a_has = a272 .owners_ex(include_pruned_owners)273 .any(|o| prefer_identities.contains(o));274 let b_has = b275 .owners_ex(include_pruned_owners)276 .any(|o| prefer_identities.contains(o));277 match (a_has, b_has) {278 (true, false) => return Greater,279 (false, true) => return Less,280 _ => {}281 }282 }283 match (a.secret.expires_at, b.secret.expires_at) {284 (None, Some(_)) => return Greater,285 (Some(_), None) => return Less,286 (Some(a), Some(b)) => {287 288 return a.cmp(&b);289 }290 (None, None) => {}291 }292293 294 return a.owners.len().cmp(&b.owners.len());295}296297impl FleetSecretDistributions {298 299 fn prune_expired(&mut self, now: DateTime<Utc>) {300 for ele in self.distributions_mut() {301 if let Some(expires_at) = ele.secret.expires_at {302 if expires_at < now {303 ele.prune(format!("expired during check at {now}"));304 }305 }306 }307 }308 309 310 pub fn prune_shared(311 &mut self,312 expected_owners: &BTreeSet<SecretOwner>,313 unique: bool,314 expected_parts: &BTreeMap<String, GeneratorPart>,315 expected_generation_data: &Value,316 regenerate_on_owner_removed: bool,317 regenerate_on_owner_added: bool,318 prefer_identities: &BTreeSet<SecretOwner>,319 now: DateTime<Utc>,320 ) {321 self.prune_expired(now);322 self.prune_generation_data(expected_generation_data, None);323 self.prune_missing_parts(expected_parts, None);324325 let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();326327 let mut to_add = expected_owners.difference(¤t_owners);328 if to_add.next().is_some() && unique && regenerate_on_owner_added {329 for dist in self.distributions_mut() {330 dist.prune(format!(331 "owners missing, can't add new distribution, regeneration preferred"332 ));333 }334 return;335 }336337 for to_remove in current_owners.difference(&expected_owners) {338 self.entry(to_remove.clone()).remove(339 regenerate_on_owner_removed,340 "owner was removed from expected owners list, regenerate_on_owner_removed is set"341 .to_string(),342 );343 }344 if unique {345 self.prune_nonunique(prefer_identities);346 }347 }348 pub fn prune_host(349 &mut self,350 owner: SecretOwner,351 expected_parts: &BTreeMap<String, GeneratorPart>,352 expected_generation_data: &Value,353 now: DateTime<Utc>,354 ) {355 self.prune_expired(now);356 self.prune_generation_data(expected_generation_data, Some(&owner));357 358 self.prune_missing_parts(expected_parts, Some(&owner));359 }360 361 362 fn best_idx(363 &self,364 prefer_identities: &BTreeSet<SecretOwner>,365 include_pruned_owners: bool,366 ) -> Option<usize> {367 self.distributions()368 .enumerate()369 .max_by(|(_, a), (_, b)| {370 compare_dists(&a, &b, prefer_identities, include_pruned_owners)371 })372 .map(|(p, _)| p)373 }374 375 fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {376 if self.distributions().next().is_none() {377 return;378 }379 let best = self.best_idx(prefer_identities, false).expect("not empty");380 for (i, dist) in self.distributions_mut().enumerate() {381 if i != best {382 dist.prune(383 "secret wants to be the same on all hosts, only the best one was left"384 .to_owned(),385 );386 }387 }388 }389390 pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {391 assert!(self.get(&owner).is_none(), "secret is not pruned for host");392 if let Some(dist) = self393 .distributions_mut()394 .find(|v| v.owners_pending_prune.contains_key(&owner))395 {396 dist.unprune_owner(owner);397 Some(dist)398 } else {399 None400 }401 }402403 pub fn best_distribution_for_reencryption(404 &mut self,405 prefer_identities: &BTreeSet<SecretOwner>,406 ) -> Option<&mut FleetSecretDistribution> {407 let best_idx = self.best_idx(prefer_identities, true)?;408 self.distributions_mut().nth(best_idx)409 }410411 fn prune_missing_parts(412 &mut self,413 expected_parts: &BTreeMap<String, GeneratorPart>,414 filter_owner: Option<&SecretOwner>,415 ) {416 'dist: for ele in self.distributions_mut() {417 if let Some(filter_owner) = filter_owner {418 if !ele.owners.contains(filter_owner) {419 continue;420 }421 422 423 }424 for (name, part) in expected_parts {425 let Some(stored_part) = ele.secret.parts.get(name) else {426 ele.prune(format!("secret definition added new part: {name}"));427 continue 'dist;428 };429 if part.encrypted != stored_part.raw.encrypted {430 ele.prune(format!(431 "secret definition now requires part to be {}",432 if part.encrypted {433 "encrypted"434 } else {435 "non-encrypted"436 }437 ));438 continue 'dist;439 }440 }441 }442 }443 fn prune_generation_data(444 &mut self,445 expected_generation_data: &Value,446 filter_owner: Option<&SecretOwner>,447 ) {448 for ele in self.distributions_mut() {449 if let Some(filter_owner) = filter_owner {450 if !ele.owners.contains(filter_owner) {451 continue;452 }453 454 455 }456 if ele.secret.generation_data != *expected_generation_data {457 ele.prune(format!(458 "expected generation data mismatch: {expected_generation_data:?}"459 ));460 }461 }462 }463464 465 466 467 fn prune_dead(&mut self) {468 for ele in self.distributions_mut() {469 if ele.owners.is_empty() {470 ele.prune("no owners left".to_owned());471 }472 }473 }474475 pub fn all_distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {476 self.stored.iter()477 }478 pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {479 self.stored.iter().filter(|v| v.pending_prune.is_none())480 }481 pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {482 self.stored.iter_mut().filter(|v| v.pending_prune.is_none())483 }484 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {485 self.distributions().flat_map(|v| v.owners.iter())486 }487 #[allow(488 clippy::len_without_is_empty,489 reason = "should not be empty for a long time"490 )]491 pub fn len(&self) -> usize {492 self.distributions().count()493 }494495 pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {496 self.distributions().find(|d| d.owners.contains(owner))497 }498 fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {499 let Some((idx, dist)) = self500 .distributions()501 .enumerate()502 .find(|(_, d)| d.owners.contains(&owner))503 else {504 return DistEntry::Vacant(VacantDistEntry {505 distributions: self,506 owners: BTreeSet::from([owner]),507 });508 };509 DistEntry::Occupied(OccupiedDistEntry {510 owners: dist.owners.clone(),511 distributions: self,512 idx,513 })514 }515 pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {516 for ele in self.distributions_mut() {517 ele.prune_owners(&dist.owners, reason.clone());518 }519 self.stored.push(dist);520 }521 pub fn contains(&self, owner: &SecretOwner) -> bool {522 self.distributions().any(|d| d.owners.contains(owner))523 }524}525526struct OccupiedDistEntry<'d> {527 distributions: &'d mut FleetSecretDistributions,528 idx: usize,529 owners: BTreeSet<SecretOwner>,530}531impl<'d> OccupiedDistEntry<'d> {532 fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {533 let dist = &mut self.distributions.stored[self.idx];534 if whole_dist {535 dist.prune(reason);536 } else {537 dist.prune_owners(&self.owners, reason);538 }539 VacantDistEntry {540 distributions: self.distributions,541 owners: self.owners,542 }543 }544 fn set(self, secret: FleetSecretData, reason: String) -> Self {545 self.remove(false, reason).set(secret)546 }547}548struct VacantDistEntry<'d> {549 distributions: &'d mut FleetSecretDistributions,550 owners: BTreeSet<SecretOwner>,551}552impl<'d> VacantDistEntry<'d> {553 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {554 let Self {555 distributions,556 owners,557 } = self;558 let idx = distributions.stored.len();559 distributions.stored.push(FleetSecretDistribution {560 owners: owners.clone(),561 secret,562563 owners_pending_prune: BTreeMap::new(),564 pending_prune: None,565 _deprecated_managed: true,566 });567 OccupiedDistEntry {568 distributions,569 owners,570 idx,571 }572 }573}574575enum DistEntry<'d> {576 Vacant(VacantDistEntry<'d>),577 Occupied(OccupiedDistEntry<'d>),578}579impl DistEntry<'_> {580 fn remove(self, whole_dist: bool, reason: String) -> Self {581 match self {582 DistEntry::Vacant(_) => self,583 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),584 }585 }586 fn set(self, secret: FleetSecretData, reason: String) -> Self {587 Self::Occupied(match self {588 DistEntry::Vacant(e) => e.set(secret),589 DistEntry::Occupied(e) => e.set(secret, reason),590 })591 }592}593594impl Serialize for FleetSecretDistributions {595 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>596 where597 S: serde::Serializer,598 {599 let mut v = self.clone();600 v.prune_dead();601 let mut found_hosts = BTreeSet::new();602 for ele in v.distributions() {603 if ele.pending_prune.is_some() {604 continue;605 }606 if ele.owners.is_empty() {607 panic!("consistency: secret distribution has no defined owners");608 }609 for ele in ele.owners.iter() {610 if !found_hosts.insert(ele) {611 panic!(612 "consistency: secret distribution contains duplicate entry for the same host",613 );614 }615 }616 }617 match v.stored.len() {618 0 => panic!("consistency: empty distributions"),619 1 => v.stored[0].serialize(serializer),620 _ => {621 let mut sorted = v.stored.clone();622 623 sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);624 sorted.serialize(serializer)625 }626 }627 }628}629impl<'de> Deserialize<'de> for FleetSecretDistributions {630 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>631 where632 D: serde::Deserializer<'de>,633 {634 #[derive(Deserialize)]635 #[serde(untagged)]636 enum Distributions {637 One(FleetSecretDistribution),638 Many(Vec<FleetSecretDistribution>),639 }640 let d = Distributions::deserialize(deserializer)?;641 let stored = match d {642 Distributions::One(d) => vec![d],643 Distributions::Many(ds) => ds,644 };645 if stored.is_empty() {646 return Err(de::Error::custom("consistency: empty distributions"));647 }648 let mut found_hosts = BTreeSet::new();649 for ele in stored.iter() {650 if ele.pending_prune.is_some() {651 continue;652 }653 if ele.owners.is_empty() {654 return Err(de::Error::custom(655 "consistency: secret distribution has no defined owners",656 ));657 }658 for ele in ele.owners.iter() {659 if !found_hosts.insert(ele) {660 return Err(de::Error::custom(661 "consistency: secret distribution contains duplicate entry for the same host",662 ));663 }664 }665 }666 Ok(Self { stored })667 }668}669670#[derive(Deserialize, Default)]671pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);672673impl Serialize for FleetSecrets {674 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>675 where676 S: serde::Serializer,677 {678 let data: BTreeMap<String, FleetSecretDistributions> = self679 .0680 .iter()681 .filter(|(_, v)| !v.stored.is_empty())682 .map(|(k, v)| (k.clone(), v.clone()))683 .collect();684685 data.serialize(serializer)686 }687}688689impl FleetSecrets {690 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {691 self.0.keys()692 }693694 pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {695 self.0696 .iter()697 .filter(|(_, d)| d.contains(owner))698 .map(|(n, _)| n)699 }700701 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {702 match self.0.entry(secret) {703 Entry::Vacant(e) => {704 e.insert(FleetSecretDistributions { stored: vec![data] });705 }706 Entry::Occupied(mut e) => {707 let dists = e.get_mut();708 dists.extend(data, "secret data was replaced".to_owned())709 }710 }711 }712 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {713 self.0.get(secret)714 }715 pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {716 self.0.get_mut(secret)717 }718719 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {720 self.0721 .entry(secret.to_owned())722 .or_insert(FleetSecretDistributions::default())723 }724725 pub fn contains(&self, secret: &str) -> bool {726 self.0.contains_key(secret)727 }728 pub fn remove(&mut self, secret: &str) {729 self.0.remove(secret);730 }731732 fn merge_from_hosts(733 &mut self,734 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,735 ) {736 for (host, host_secrets) in host_secrets {737 for (secret_name, mut secret_data) in host_secrets {738 secret_data.owners.insert(host.clone());739 self.set_data(secret_name, secret_data);740 }741 }742 }743744 pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {745 for (name, dists) in self.0.iter_mut() {746 if expected_nonshared.contains(name) {747 continue;748 }749 for dist in dists.distributions_mut() {750 if dist.owners.contains(host) {751 dist.prune_owners(752 &BTreeSet::from([host.to_owned()]),753 "host no longer defines this secret".to_owned(),754 );755 }756 }757 }758 }759}760761#[derive(Debug, Clone)]762pub struct Expectations {763 pub owners: BTreeSet<SecretOwner>,764 pub generation_data: serde_json::Value,765 pub parts: BTreeMap<String, GeneratorPart>,766}767#[derive(Deserialize, Debug, Clone)]768pub struct GeneratorPart {769 pub encrypted: bool,770}771772#[derive(Debug, Clone, Copy)]773pub struct RegenerationConstraints {774 pub allow_different: bool,775 pub regenerate_on_owner_added: bool,776 pub regenerate_on_owner_removed: bool,777}778impl RegenerationConstraints {779 pub fn host_personal() -> Self {780 Self {781 allow_different: false,782 regenerate_on_owner_added: true,783 regenerate_on_owner_removed: true,784 }785 }786 pub fn without_preferences(self) -> Self {787 Self {788 allow_different: self.allow_different,789 regenerate_on_owner_added: false,790 regenerate_on_owner_removed: false,791 }792 }793}