1use std::{2 cmp::Ordering,3 collections::{4 BTreeMap, BTreeSet,5 btree_map::{self, Entry},6 },7 fmt,8 io::{self, Cursor},9 str::FromStr,10 sync::RwLock,11};1213use age::Recipient;14use chrono::{DateTime, Utc};15use fleet_shared::SecretData;16use rand::{17 distr::{Alphanumeric, SampleString as _},18 rng,19};20use serde::{21 Deserialize, Serialize,22 de::{self, Error},23};24use serde_json::Value;25use tracing::info;2627#[derive(Serialize, Deserialize, Default)]28#[serde(rename_all = "camelCase")]29pub struct HostData {30 #[serde(default)]31 #[serde(skip_serializing_if = "String::is_empty")]32 pub encryption_key: String,33}3435const VERSION: &str = "0.1.0";36pub struct FleetDataVersion;37impl Serialize for FleetDataVersion {38 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>39 where40 S: serde::Serializer,41 {42 VERSION.serialize(serializer)43 }44}45impl<'de> Deserialize<'de> for FleetDataVersion {46 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>47 where48 D: serde::Deserializer<'de>,49 {50 let version = String::deserialize(deserializer)?;51 if version != VERSION {52 return Err(D::Error::custom(format!(53 "fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"54 )));55 }56 Ok(Self)57 }58}5960fn generate_gc_prefix() -> String {61 let id = Alphanumeric.sample_string(&mut rng(), 8);62 format!("fleet-gc-{id}")63}6465#[derive(Serialize, Deserialize)]66#[serde(rename_all = "camelCase")]67pub struct ManagerKey {68 pub name: String,69 pub key: String,70}7172#[derive(Serialize, Deserialize)]73#[serde(rename_all = "camelCase")]74pub struct FleetData {75 pub version: FleetDataVersion,76 #[serde(default = "generate_gc_prefix")]77 pub gc_root_prefix: String,7879 #[serde(default, skip_serializing_if = "Vec::is_empty")]80 pub manager_keys: Vec<ManagerKey>,8182 #[serde(default)]83 pub hosts: RwLock<BTreeMap<String, HostData>>,8485 #[serde(default, alias = "shared_secrets")]86 pub secrets: RwLock<FleetSecrets>,8788 89 #[serde(default)]90 pub extra: RwLock<BTreeMap<String, Value>>,9192 #[serde(default)]93 #[serde(skip_serializing)]94 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,95}96impl FromStr for FleetData {97 type Err = anyhow::Error;98 fn from_str(s: &str) -> anyhow::Result<Self> {99 let mut data: Self = nixlike::parse_str(s)?;100 if !data.host_secrets.is_empty() {101 info!("migrating host secrets into shared secrets structure");102 data.secrets103 .write()104 .expect("no poisoning")105 .merge_from_hosts(std::mem::take(&mut data.host_secrets));106 }107 Ok(data)108 }109}110111112pub fn encrypt_secret_data<'r>(113 recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,114 data: Vec<u8>,115) -> Option<SecretData> {116 let mut encrypted = vec![];117 let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))118 .ok()?119 .wrap_output(&mut encrypted)120 .expect("in memory write");121 io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");122 encryptor.finish().expect("in memory flush");123 Some(SecretData {124 data: encrypted,125 encrypted: true,126 })127}128129#[derive(Serialize, Deserialize, Clone, Debug)]130pub struct FleetSecretPart {131 pub raw: SecretData,132}133134#[derive(Serialize, Deserialize, Clone, Debug)]135#[serde(rename_all = "camelCase")]136#[must_use]137pub struct FleetSecretData {138 pub created_at: DateTime<Utc>,139 #[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]140 pub expires_at: Option<DateTime<Utc>>,141142 #[serde(flatten)]143 pub parts: BTreeMap<String, FleetSecretPart>,144145 #[serde(default, skip_serializing_if = "Value::is_null")]146 pub generation_data: Value,147}148149#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]150#[repr(transparent)]151pub struct SecretOwner(String);152153impl fmt::Display for SecretOwner {154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {155 write!(f, "host:{}", self.0)156 }157}158159impl SecretOwner {160 pub fn host(s: impl AsRef<str>) -> SecretOwner {161 SecretOwner(s.as_ref().to_owned())162 }163 pub fn as_host(&self) -> Option<&str> {164 Some(&self.0)165 }166}167168#[derive(Serialize, Deserialize, Clone, Debug)]169#[serde(rename_all = "camelCase")]170#[must_use]171pub struct FleetSecretDistribution {172 #[serde(default)]173 owners: BTreeSet<SecretOwner>,174 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]175 owners_pending_prune: BTreeMap<SecretOwner, String>,176177 #[serde(flatten)]178 pub secret: FleetSecretData,179180 #[serde(default, skip_serializing_if = "Option::is_none")]181 pending_prune: Option<String>,182 #[serde(default, skip_serializing, alias = "managed")]183 _deprecated_managed: bool,184}185186const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();187impl FleetSecretDistribution {188 pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {189 assert!(190 !owners.is_empty(),191 "distribution should have at least one owner"192 );193 if let Some(expires_at) = &secret.expires_at {194 assert!(195 *expires_at > now,196 "secret should not be expired on creation"197 );198 }199 Self {200 owners,201 secret,202 owners_pending_prune: BTreeMap::new(),203 pending_prune: None,204 _deprecated_managed: true,205 }206 }207208 fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {209 let pending_prune = if including_pruned {210 &self.owners_pending_prune211 } else {212 EMPTY_PENDING_PRUNE213 };214 self.owners.iter().chain(pending_prune.keys())215 }216 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {217 self.owners_ex(false)218 }219 pub fn owners_pending_prune(&self) -> impl Iterator<Item = &SecretOwner> {220 self.owners_pending_prune.keys()221 }222 pub fn is_pending_prune(&self) -> bool {223 self.pending_prune.is_some()224 }225226 pub fn prune(&mut self, reason: String) {227 assert!(228 self.pending_prune.is_none(),229 "it shouldn't be possible to prune the same distribution twice using public api"230 );231 self.pending_prune = Some(reason);232 }233 pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {234 235 236 237 238 for owner in owners {239 if self.owners.remove(owner) {240 self.owners_pending_prune241 .insert(owner.to_owned(), reason.clone());242 }243 }244 245 246 247 }248 pub fn unprune_owner(&mut self, owner: SecretOwner) {249 if self.owners_pending_prune.remove(&owner).is_some() {250 self.owners.insert(owner);251 }252 }253}254255#[derive(Clone, Debug, Default)]256#[must_use]257pub struct FleetSecretDistributions {258 stored: Vec<FleetSecretDistribution>,259}260261fn compare_dists(262 a: &FleetSecretDistribution,263 b: &FleetSecretDistribution,264 prefer_identities: &BTreeSet<SecretOwner>,265 include_pruned_owners: bool,266) -> Ordering {267 use Ordering::*;268 if prefer_identities.is_empty() {269 let a_has = a270 .owners_ex(include_pruned_owners)271 .any(|o| prefer_identities.contains(o));272 let b_has = b273 .owners_ex(include_pruned_owners)274 .any(|o| prefer_identities.contains(o));275 match (a_has, b_has) {276 (true, false) => return Greater,277 (false, true) => return Less,278 _ => {}279 }280 }281 match (a.secret.expires_at, b.secret.expires_at) {282 (None, Some(_)) => return Greater,283 (Some(_), None) => return Less,284 (Some(a), Some(b)) => {285 286 return a.cmp(&b);287 }288 (None, None) => {}289 }290291 292 a.owners.len().cmp(&b.owners.len())293}294295impl FleetSecretDistributions {296 297 fn prune_expired(&mut self, now: DateTime<Utc>) {298 for ele in self.distributions_mut() {299 if let Some(expires_at) = ele.secret.expires_at300 && expires_at < now301 {302 ele.prune(format!("expired during check at {now}"));303 }304 }305 }306 307 308 #[allow(clippy::too_many_arguments)]309 pub fn prune_shared(310 &mut self,311 expected_owners: &BTreeSet<SecretOwner>,312 unique: bool,313 expected_parts: &BTreeMap<String, GeneratorPart>,314 expected_generation_data: &Value,315 regenerate_on_owner_removed: bool,316 regenerate_on_owner_added: bool,317 prefer_identities: &BTreeSet<SecretOwner>,318 now: DateTime<Utc>,319 ) {320 self.prune_expired(now);321 self.prune_generation_data(expected_generation_data, None);322 self.prune_missing_parts(expected_parts, None);323324 let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();325326 let mut to_add = expected_owners.difference(¤t_owners);327 if to_add.next().is_some() && unique && regenerate_on_owner_added {328 for dist in self.distributions_mut() {329 dist.prune(330 "owners missing, can't add new distribution, regeneration preferred"331 .to_string(),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)| compare_dists(a, b, prefer_identities, include_pruned_owners))370 .map(|(p, _)| p)371 }372 373 fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {374 if self.distributions().next().is_none() {375 return;376 }377 let best = self.best_idx(prefer_identities, false).expect("not empty");378 for (i, dist) in self.distributions_mut().enumerate() {379 if i != best {380 dist.prune(381 "secret wants to be the same on all hosts, only the best one was left"382 .to_owned(),383 );384 }385 }386 }387388 pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {389 assert!(self.get(&owner).is_none(), "secret is not pruned for host");390 if let Some(dist) = self391 .distributions_mut()392 .find(|v| v.owners_pending_prune.contains_key(&owner))393 {394 dist.unprune_owner(owner);395 Some(dist)396 } else {397 None398 }399 }400401 pub fn best_distribution_for_reencryption(402 &mut self,403 prefer_identities: &BTreeSet<SecretOwner>,404 ) -> Option<&mut FleetSecretDistribution> {405 let best_idx = self.best_idx(prefer_identities, true)?;406 self.distributions_mut().nth(best_idx)407 }408409 fn prune_missing_parts(410 &mut self,411 expected_parts: &BTreeMap<String, GeneratorPart>,412 filter_owner: Option<&SecretOwner>,413 ) {414 'dist: for ele in self.distributions_mut() {415 if let Some(filter_owner) = filter_owner416 && !ele.owners.contains(filter_owner)417 {418 continue;419 420 421 }422 for (name, part) in expected_parts {423 let Some(stored_part) = ele.secret.parts.get(name) else {424 ele.prune(format!("secret definition added new part: {name}"));425 continue 'dist;426 };427 if part.encrypted != stored_part.raw.encrypted {428 ele.prune(format!(429 "secret definition now requires part to be {}",430 if part.encrypted {431 "encrypted"432 } else {433 "non-encrypted"434 }435 ));436 continue 'dist;437 }438 }439 }440 }441 fn prune_generation_data(442 &mut self,443 expected_generation_data: &Value,444 filter_owner: Option<&SecretOwner>,445 ) {446 for ele in self.distributions_mut() {447 if let Some(filter_owner) = filter_owner448 && !ele.owners.contains(filter_owner)449 {450 continue;451 452 453 }454 if ele.secret.generation_data != *expected_generation_data {455 ele.prune(format!(456 "expected generation data mismatch: {expected_generation_data:?}"457 ));458 }459 }460 }461462 463 464 465 fn prune_dead(&mut self) {466 for ele in self.distributions_mut() {467 if ele.owners.is_empty() {468 ele.prune("no owners left".to_owned());469 }470 }471 }472473 pub fn all_distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {474 self.stored.iter()475 }476 pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {477 self.stored.iter().filter(|v| v.pending_prune.is_none())478 }479 pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {480 self.stored.iter_mut().filter(|v| v.pending_prune.is_none())481 }482 pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {483 self.distributions().flat_map(|v| v.owners.iter())484 }485 #[allow(486 clippy::len_without_is_empty,487 reason = "should not be empty for a long time"488 )]489 pub fn len(&self) -> usize {490 self.distributions().count()491 }492493 pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {494 self.distributions().find(|d| d.owners.contains(owner))495 }496 fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {497 let Some((idx, dist)) = self498 .distributions()499 .enumerate()500 .find(|(_, d)| d.owners.contains(&owner))501 else {502 return DistEntry::Vacant(VacantDistEntry {503 distributions: self,504 owners: BTreeSet::from([owner]),505 });506 };507 DistEntry::Occupied(OccupiedDistEntry {508 owners: dist.owners.clone(),509 distributions: self,510 idx,511 })512 }513 pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {514 for ele in self.distributions_mut() {515 ele.prune_owners(&dist.owners, reason.clone());516 }517 self.stored.push(dist);518 }519 pub fn contains(&self, owner: &SecretOwner) -> bool {520 self.distributions().any(|d| d.owners.contains(owner))521 }522}523524pub struct OccupiedDistEntry<'d> {525 distributions: &'d mut FleetSecretDistributions,526 idx: usize,527 owners: BTreeSet<SecretOwner>,528}529impl<'d> OccupiedDistEntry<'d> {530 fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {531 let dist = &mut self.distributions.stored[self.idx];532 if whole_dist {533 dist.prune(reason);534 } else {535 dist.prune_owners(&self.owners, reason);536 }537 VacantDistEntry {538 distributions: self.distributions,539 owners: self.owners,540 }541 }542 pub fn set(self, secret: FleetSecretData, reason: String) -> Self {543 self.remove(false, reason).set(secret)544 }545}546pub struct VacantDistEntry<'d> {547 distributions: &'d mut FleetSecretDistributions,548 owners: BTreeSet<SecretOwner>,549}550impl<'d> VacantDistEntry<'d> {551 pub fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {552 let Self {553 distributions,554 owners,555 } = self;556 let idx = distributions.stored.len();557 distributions.stored.push(FleetSecretDistribution {558 owners: owners.clone(),559 secret,560561 owners_pending_prune: BTreeMap::new(),562 pending_prune: None,563 _deprecated_managed: true,564 });565 OccupiedDistEntry {566 distributions,567 owners,568 idx,569 }570 }571}572573pub enum DistEntry<'d> {574 Vacant(VacantDistEntry<'d>),575 Occupied(OccupiedDistEntry<'d>),576}577impl DistEntry<'_> {578 pub fn remove(self, whole_dist: bool, reason: String) -> Self {579 match self {580 DistEntry::Vacant(_) => self,581 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),582 }583 }584 pub fn set(self, secret: FleetSecretData, reason: String) -> Self {585 Self::Occupied(match self {586 DistEntry::Vacant(e) => e.set(secret),587 DistEntry::Occupied(e) => e.set(secret, reason),588 })589 }590}591592impl Serialize for FleetSecretDistributions {593 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>594 where595 S: serde::Serializer,596 {597 let mut v = self.clone();598 v.prune_dead();599 let mut found_hosts = BTreeSet::new();600 for ele in v.distributions() {601 if ele.pending_prune.is_some() {602 continue;603 }604 if ele.owners.is_empty() {605 panic!("consistency: secret distribution has no defined owners");606 }607 for ele in ele.owners.iter() {608 if !found_hosts.insert(ele) {609 panic!(610 "consistency: secret distribution contains duplicate entry for the same host",611 );612 }613 }614 }615 match v.stored.len() {616 0 => panic!("consistency: empty distributions"),617 1 => v.stored[0].serialize(serializer),618 _ => {619 let mut sorted = v.stored.clone();620 621 sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);622 sorted.serialize(serializer)623 }624 }625 }626}627impl<'de> Deserialize<'de> for FleetSecretDistributions {628 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>629 where630 D: serde::Deserializer<'de>,631 {632 #[derive(Deserialize)]633 #[serde(untagged)]634 enum Distributions {635 One(FleetSecretDistribution),636 Many(Vec<FleetSecretDistribution>),637 }638 let d = Distributions::deserialize(deserializer)?;639 let stored = match d {640 Distributions::One(d) => vec![d],641 Distributions::Many(ds) => ds,642 };643 if stored.is_empty() {644 return Err(de::Error::custom("consistency: empty distributions"));645 }646 let mut found_hosts = BTreeSet::new();647 for ele in stored.iter() {648 if ele.pending_prune.is_some() {649 continue;650 }651 if ele.owners.is_empty() {652 return Err(de::Error::custom(653 "consistency: secret distribution has no defined owners",654 ));655 }656 for ele in ele.owners.iter() {657 if !found_hosts.insert(ele) {658 return Err(de::Error::custom(659 "consistency: secret distribution contains duplicate entry for the same host",660 ));661 }662 }663 }664 Ok(Self { stored })665 }666}667668#[derive(Deserialize, Default)]669pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);670671impl Serialize for FleetSecrets {672 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>673 where674 S: serde::Serializer,675 {676 let data: BTreeMap<String, FleetSecretDistributions> = self677 .0678 .iter()679 .filter(|(_, v)| !v.stored.is_empty())680 .map(|(k, v)| (k.clone(), v.clone()))681 .collect();682683 data.serialize(serializer)684 }685}686687impl FleetSecrets {688 pub fn keys(&self) -> btree_map::Keys<'_, String, FleetSecretDistributions> {689 self.0.keys()690 }691692 pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {693 self.0694 .iter()695 .filter(|(_, d)| d.contains(owner))696 .map(|(n, _)| n)697 }698699 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {700 match self.0.entry(secret) {701 Entry::Vacant(e) => {702 e.insert(FleetSecretDistributions { stored: vec![data] });703 }704 Entry::Occupied(mut e) => {705 let dists = e.get_mut();706 dists.extend(data, "secret data was replaced".to_owned())707 }708 }709 }710 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {711 self.0.get(secret)712 }713 pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {714 self.0.get_mut(secret)715 }716717 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {718 self.0.entry(secret.to_owned()).or_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}