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 }221222 pub fn prune(&mut self, reason: String) {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);228 }229 pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {230 231 232 233 234 for owner in owners {235 if self.owners.remove(owner) {236 self.owners_pending_prune237 .insert(owner.to_owned(), reason.clone());238 }239 }240 241 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 }249}250251#[derive(Clone, Debug, Default)]252#[must_use]253pub struct FleetSecretDistributions {254 stored: Vec<FleetSecretDistribution>,255}256257fn 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 = a266 .owners_ex(include_pruned_owners)267 .any(|o| prefer_identities.contains(o));268 let b_has = b269 .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 282 return a.cmp(&b);283 }284 (None, None) => {}285 }286287 288 return a.owners.len().cmp(&b.owners.len());289}290291impl FleetSecretDistributions {292 293 fn prune_expired(&mut self, now: DateTime<Utc>) {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 }301 }302 303 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);318319 let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();320321 let mut to_add = expected_owners.difference(¤t_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 }330331 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 352 self.prune_missing_parts(expected_parts, Some(&owner));353 }354 355 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 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 }383384 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) = self387 .distributions_mut()388 .find(|v| v.owners_pending_prune.contains_key(&owner))389 {390 dist.unprune_owner(owner);391 Some(dist)392 } else {393 None394 }395 }396397 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 }404405 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 416 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 448 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 }457458 459 460 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 }468469 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 }478 #[allow(479 clippy::len_without_is_empty,480 reason = "should not be empty for a long time"481 )]482 pub fn len(&self) -> usize {483 self.distributions().count()484 }485486 pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {487 self.distributions().find(|d| d.owners.contains(owner))488 }489 fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {490 let Some((idx, dist)) = self491 .distributions()492 .enumerate()493 .find(|(_, d)| d.owners.contains(&owner))494 else {495 return DistEntry::Vacant(VacantDistEntry {496 distributions: self,497 owners: BTreeSet::from([owner]),498 });499 };500 DistEntry::Occupied(OccupiedDistEntry {501 owners: dist.owners.clone(),502 distributions: self,503 idx,504 })505 }506 pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {507 for ele in self.distributions_mut() {508 ele.prune_owners(&dist.owners, reason.clone());509 }510 self.stored.push(dist);511 }512 pub fn contains(&self, owner: &SecretOwner) -> bool {513 self.distributions().any(|d| d.owners.contains(owner))514 }515}516517struct OccupiedDistEntry<'d> {518 distributions: &'d mut FleetSecretDistributions,519 idx: usize,520 owners: BTreeSet<SecretOwner>,521}522impl<'d> OccupiedDistEntry<'d> {523 fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {524 let dist = &mut self.distributions.stored[self.idx];525 if whole_dist {526 dist.prune(reason);527 } else {528 dist.prune_owners(&self.owners, reason);529 }530 VacantDistEntry {531 distributions: self.distributions,532 owners: self.owners,533 }534 }535 fn set(self, secret: FleetSecretData, reason: String) -> Self {536 self.remove(false, reason).set(secret)537 }538}539struct VacantDistEntry<'d> {540 distributions: &'d mut FleetSecretDistributions,541 owners: BTreeSet<SecretOwner>,542}543impl<'d> VacantDistEntry<'d> {544 fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {545 let Self {546 distributions,547 owners,548 } = self;549 let idx = distributions.stored.len();550 distributions.stored.push(FleetSecretDistribution {551 owners: owners.clone(),552 secret,553554 owners_pending_prune: BTreeMap::new(),555 pending_prune: None,556 _deprecated_managed: true,557 });558 OccupiedDistEntry {559 distributions,560 owners,561 idx,562 }563 }564}565566enum DistEntry<'d> {567 Vacant(VacantDistEntry<'d>),568 Occupied(OccupiedDistEntry<'d>),569}570impl DistEntry<'_> {571 fn remove(self, whole_dist: bool, reason: String) -> Self {572 match self {573 DistEntry::Vacant(_) => self,574 DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),575 }576 }577 fn set(self, secret: FleetSecretData, reason: String) -> Self {578 Self::Occupied(match self {579 DistEntry::Vacant(e) => e.set(secret),580 DistEntry::Occupied(e) => e.set(secret, reason),581 })582 }583}584585impl Serialize for FleetSecretDistributions {586 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>587 where588 S: serde::Serializer,589 {590 let mut v = self.clone();591 v.prune_dead();592 let mut found_hosts = BTreeSet::new();593 for ele in v.distributions() {594 if ele.pending_prune.is_some() {595 continue;596 }597 if ele.owners.is_empty() {598 panic!("consistency: secret distribution has no defined owners");599 }600 for ele in ele.owners.iter() {601 if !found_hosts.insert(ele) {602 panic!(603 "consistency: secret distribution contains duplicate entry for the same host",604 );605 }606 }607 }608 match v.stored.len() {609 0 => panic!("consistency: empty distributions"),610 1 => v.stored[0].serialize(serializer),611 _ => {612 let mut sorted = v.stored.clone();613 614 sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);615 sorted.serialize(serializer)616 }617 }618 }619}620impl<'de> Deserialize<'de> for FleetSecretDistributions {621 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>622 where623 D: serde::Deserializer<'de>,624 {625 #[derive(Deserialize)]626 #[serde(untagged)]627 enum Distributions {628 One(FleetSecretDistribution),629 Many(Vec<FleetSecretDistribution>),630 }631 let d = Distributions::deserialize(deserializer)?;632 let stored = match d {633 Distributions::One(d) => vec![d],634 Distributions::Many(ds) => ds,635 };636 if stored.is_empty() {637 return Err(de::Error::custom("consistency: empty distributions"));638 }639 let mut found_hosts = BTreeSet::new();640 for ele in stored.iter() {641 if ele.pending_prune.is_some() {642 continue;643 }644 if ele.owners.is_empty() {645 return Err(de::Error::custom(646 "consistency: secret distribution has no defined owners",647 ));648 }649 for ele in ele.owners.iter() {650 if !found_hosts.insert(ele) {651 return Err(de::Error::custom(652 "consistency: secret distribution contains duplicate entry for the same host",653 ));654 }655 }656 }657 Ok(Self { stored })658 }659}660661#[derive(Deserialize, Default)]662pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);663664impl Serialize for FleetSecrets {665 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>666 where667 S: serde::Serializer,668 {669 let data: BTreeMap<String, FleetSecretDistributions> = self670 .0671 .iter()672 .filter(|(_, v)| !v.stored.is_empty())673 .map(|(k, v)| (k.clone(), v.clone()))674 .collect();675676 data.serialize(serializer)677 }678}679680impl FleetSecrets {681 pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {682 self.0.keys()683 }684685 pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {686 self.0687 .iter()688 .filter(|(_, d)| d.contains(owner))689 .map(|(n, _)| n)690 }691692 pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {693 match self.0.entry(secret) {694 Entry::Vacant(e) => {695 e.insert(FleetSecretDistributions { stored: vec![data] });696 }697 Entry::Occupied(mut e) => {698 let dists = e.get_mut();699 dists.extend(data, "secret data was replaced".to_owned())700 }701 }702 }703 pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {704 self.0.get(secret)705 }706 pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {707 self.0.get_mut(secret)708 }709710 pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {711 self.0712 .entry(secret.to_owned())713 .or_insert(FleetSecretDistributions::default())714 }715716 pub fn contains(&self, secret: &str) -> bool {717 self.0.contains_key(secret)718 }719 pub fn remove(&mut self, secret: &str) {720 self.0.remove(secret);721 }722723 fn merge_from_hosts(724 &mut self,725 host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,726 ) {727 for (host, host_secrets) in host_secrets {728 for (secret_name, mut secret_data) in host_secrets {729 secret_data.owners.insert(host.clone());730 self.set_data(secret_name, secret_data);731 }732 }733 }734735 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 }750}751752#[derive(Debug, Clone)]753pub struct Expectations {754 pub owners: BTreeSet<SecretOwner>,755 pub generation_data: serde_json::Value,756 pub parts: BTreeMap<String, GeneratorPart>,757}758#[derive(Deserialize, Debug, Clone)]759pub struct GeneratorPart {760 pub encrypted: bool,761}762763#[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 }784}