difftreelog
feat secret regeneration
in: trunk
6 files changed
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth130 },130 },131}131}132132133fn secret_needs_regeneration(134 secret: &FleetSecret,135 expected_generation_data: &serde_json::Value,136) -> bool {137 let data_is_expected = secret.generation_data == *expected_generation_data;138 // TODO: Leeway?139 let expired = secret140 .expires_at141 .map(|expiration| expiration < Utc::now())142 .unwrap_or(false);143 expired || !data_is_expected144}145146#[allow(clippy::too_many_arguments)]133#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]147#[tracing::instrument(skip(config, secret, field, prefer_identities, batch))]134async fn update_owner_set(148async fn maybe_regenerate_shared_secret(135 secret_name: &str,149 secret_name: &str,136 config: &Config,150 config: &Config,137 mut secret: FleetSharedSecret,151 mut secret: FleetSharedSecret,138 field: Value,152 field: Value,139 expected_owners: &[String],153 expected_owners: &[String],154 expected_generation_data: serde_json::Value,140 prefer_identities: &[String],155 prefer_identities: &[String],141 batch: Option<NixBuildBatch>,156 batch: Option<NixBuildBatch>,142) -> Result<FleetSharedSecret> {157) -> Result<FleetSharedSecret> {145 let set = original_set.iter().collect::<BTreeSet<_>>();160 let set = original_set.iter().collect::<BTreeSet<_>>();146 let expected_set = expected_owners.iter().collect::<BTreeSet<_>>();161 let expected_set = expected_owners.iter().collect::<BTreeSet<_>>();162163 let regeneration_required =164 secret_needs_regeneration(&secret.secret, &expected_generation_data);147165148 if set == expected_set {166 if set == expected_set && !regeneration_required {149 info!("no need to update owner list, it is already correct");167 info!("no need to update owner list, it is already correct");150 return Ok(secret);168 return Ok(secret);151 }169 }152170153 let should_regenerate = if set.difference(&expected_set).next().is_some() {171 let should_regenerate = if regeneration_required {172 info!("secret has its generation data changed, regeneration is required");173 true174 } else if set.difference(&expected_set).next().is_some() {154 // TODO: Remove this warning for revokable secrets.175 // TODO: Remove this warning for revokable secrets.155 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");176 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");156 nix_go_json!(field.regenerateOnOwnerRemoved)177 nix_go_json!(field.regenerateOnOwnerRemoved)161 };182 };162183163 if should_regenerate {184 if should_regenerate {164 info!("secret is owner-dependent, will regenerate");185 info!("secret needs to be regenerated");165 let generated =186 let generated = generate_shared(166 generate_shared(config, secret_name, field, expected_owners.to_vec(), batch).await?;187 config,188 secret_name,189 field,190 expected_owners.to_vec(),191 expected_generation_data,192 batch,193 )194 .await?;167 Ok(generated)195 Ok(generated)216 _display_name: &str,244 _display_name: &str,217 secret: Value,245 secret: Value,218 default_generator: Value,246 default_generator: Value,219 owners: &[String],247 expected_owners: &[String],248 expected_generation_data: serde_json::Value,220 batch: Option<NixBuildBatch>,249 batch: Option<NixBuildBatch>,221) -> Result<FleetSecret> {250) -> Result<FleetSecret> {222 let generator = nix_go!(secret.generator);251 let generator = nix_go!(secret.generator);232 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);261 let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);233262234 let mut recipients = Vec::new();263 let mut recipients = Vec::new();235 for owner in owners {264 for owner in expected_owners {236 let key = config.key(owner).await?;265 let key = config.key(owner).await?;237 recipients.push(key);266 recipients.push(key);238 }267 }288 created_at,317 created_at,289 expires_at,318 expires_at,290 parts,319 parts,291 // TODO: Fill with expected292 generation_data: serde_json::Value::Null,320 generation_data: expected_generation_data,293 })321 })294}322}295async fn generate(323async fn generate(296 config: &Config,324 config: &Config,297 display_name: &str,325 display_name: &str,298 secret: Value,326 secret: Value,299 owners: &[String],327 expected_owners: &[String],328 expected_generation_data: serde_json::Value,300 batch: Option<NixBuildBatch>,329 batch: Option<NixBuildBatch>,301) -> Result<FleetSecret> {330) -> Result<FleetSecret> {302 let generator = nix_go!(secret.generator);331 let generator = nix_go!(secret.generator);335 display_name,364 display_name,336 secret,365 secret,337 default_generator,366 default_generator,338 owners,367 expected_owners,368 expected_generation_data,339 batch,369 batch,340 )370 )341 .await371 .await342 }372 }343 GeneratorKind::Pure => {373 GeneratorKind::Pure => {344 generate_pure(config, display_name, secret, default_generator, owners).await374 generate_pure(375 config,376 display_name,377 secret,378 default_generator,379 expected_owners,380 )381 .await345 }382 }350 display_name: &str,387 display_name: &str,351 secret: Value,388 secret: Value,352 expected_owners: Vec<String>,389 expected_owners: Vec<String>,390 expected_generation_data: serde_json::Value,353 batch: Option<NixBuildBatch>,391 batch: Option<NixBuildBatch>,354) -> Result<FleetSharedSecret> {392) -> Result<FleetSharedSecret> {355 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);393 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);356 Ok(FleetSharedSecret {394 Ok(FleetSharedSecret {357 secret: generate(config, display_name, secret, &expected_owners, batch).await?,395 secret: generate(396 config,397 display_name,398 secret,399 &expected_owners,400 expected_generation_data,401 batch,402 )403 .await?,358 owners: expected_owners,404 owners: expected_owners,615661616 let config_field = &config.config_field;662 let config_field = &config.config_field;617 let field = nix_go!(config_field.sharedSecrets[{ name }]);663 let field = nix_go!(config_field.sharedSecrets[{ name }]);664 let expected_generation_data = nix_go_json!(field.expectedGenerationData);618665619 let updated = update_owner_set(666 let updated = maybe_regenerate_shared_secret(620 &name,667 &name,621 config,668 config,622 secret,669 secret,623 field,670 field,624 &target_machines,671 &target_machines,672 expected_generation_data,625 &prefer_identities,673 &prefer_identities,626 None,674 None,627 )675 )630 }678 }631 Secret::Regenerate { prefer_identities } => {679 Secret::Regenerate { prefer_identities } => {632 info!("checking for secrets to regenerate");680 info!("checking for secrets to regenerate");681 let stored_shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();633 {682 {683 // Generate missing shared634 let shared_batch = None;684 let shared_batch = None;635 let _span = info_span!("shared").entered();685 let _span = info_span!("shared").entered();636 let expected_shared_set = config686 let expected_shared_set = config637 .list_configured_shared()687 .list_configured_shared()638 .await?688 .await?639 .into_iter()689 .into_iter()640 .collect::<HashSet<_>>();690 .collect::<HashSet<_>>();641 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();642 for missing in expected_shared_set.difference(&shared_set) {691 for missing in expected_shared_set.difference(&stored_shared_set) {643 let config_field = &config.config_field;692 let config_field = &config.config_field;644 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);693 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);694 let expected_generation_data: serde_json::Value =695 nix_go_json!(secret.expectedGenerationData);645 let expected_owners: Option<Vec<String>> =696 let expected_owners: Option<Vec<String>> =646 nix_go_json!(secret.expectedOwners);697 nix_go_json!(secret.expectedOwners);647 let Some(expected_owners) = expected_owners else {698 let Some(expected_owners) = expected_owners else {648 // TODO: Might still need to regenerate699 // Can't generate this missing secret, as it has no defined owners.649 continue;700 continue;650 };701 };651 info!("generating secret: {missing}");702 info!("generating secret: {missing}");654 missing,705 missing,655 secret,706 secret,656 expected_owners,707 expected_owners,708 expected_generation_data,657 shared_batch.clone(),709 shared_batch.clone(),658 )710 )659 .in_current_span()711 .in_current_span()681 for missing in expected_set.difference(&stored_set) {733 for missing in expected_set.difference(&stored_set) {682 info!("generating secret: {missing}");734 info!("generating secret: {missing}");683 let secret = host.secret_field(missing).in_current_span().await?;735 let secret = host.secret_field(missing).in_current_span().await?;736 let expected_generation_data = nix_go_json!(secret.expectedGenerationData);684 let generated = match generate(737 let generated = match generate(685 config,738 config,686 missing,739 missing,687 secret,740 secret,688 &[host.name.clone()],741 &[host.name.clone()],742 expected_generation_data,689 hosts_batch.clone(),743 hosts_batch.clone(),690 )744 )691 .in_current_span()745 .in_current_span()699 };753 };700 config.insert_secret(&host.name, missing.to_string(), generated)754 config.insert_secret(&host.name, missing.to_string(), generated)701 }755 }756 for name in stored_set {757 info!("updating secret: {name}");758 let data = config.host_secret(&host.name, &name)?;759 let secret = host.secret_field(&name).in_current_span().await?;760 let expected_generation_data = nix_go_json!(secret.expectedGenerationData);761 if secret_needs_regeneration(&data, &expected_generation_data) {762 let generated = match generate(763 config,764 &name,765 secret,766 &[host.name.clone()],767 expected_generation_data,768 hosts_batch.clone(),769 )770 .in_current_span()771 .await772 {773 Ok(v) => v,774 Err(e) => {775 error!("{e:?}");776 continue;777 }778 };779 config.insert_secret(&host.name, name.to_string(), generated)780 }781 }702 }782 }703 let mut to_remove = Vec::new();783 let mut to_remove = Vec::new();704 for name in &config.list_shared() {784 for name in &stored_shared_set {705 info!("updating secret: {name}");785 info!("updating secret: {name}");706 let data = config.shared_secret(name)?;786 let data = config.shared_secret(name)?;707 let config_field = &config.config_field;787 let config_field = &config.config_field;714 }794 }715795716 let secret = nix_go!(config_field.sharedSecrets[{ name }]);796 let secret = nix_go!(config_field.sharedSecrets[{ name }]);797 let expected_generation_data = nix_go_json!(secret.expectedGenerationData);717 config.replace_shared(798 config.replace_shared(718 name.to_owned(),799 name.to_owned(),719 update_owner_set(800 maybe_regenerate_shared_secret(720 name,801 name,721 config,802 config,722 data,803 data,723 secret,804 secret,724 &expected_owners,805 &expected_owners,806 expected_generation_data,725 &prefer_identities,807 &prefer_identities,726 None,808 None,727 )809 )crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{1use std::{2 cell::OnceCell,2 cell::OnceCell,3 collections::BTreeSet,3 ffi::{OsStr, OsString},4 ffi::{OsStr, OsString},4 fmt::Display,5 fmt::Display,5 io::Write,6 io::Write,312}313}313314314impl Config {315impl Config {316 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {317 let config = &self.config_field;318 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);319 Ok(tagged)320 }321 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {322 let mut out = BTreeSet::new();323 for owner in owners {324 if let Some(tag) = owner.strip_prefix('@') {325 let hosts = self.tagged_hostnames(tag).await?;326 out.extend(hosts);327 } else {328 out.insert(owner);329 }330 }331 Ok(out)332 }315 pub fn local_host(&self) -> ConfigHost {333 pub fn local_host(&self) -> ConfigHost {316 ConfigHost {334 ConfigHost {317 config: self.clone(),335 config: self.clone(),crates/fleet-base/src/keys.rsdiffbeforeafterboth45 }45 }464647 pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {47 pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {48 let hosts = self.expand_owner_set(hosts).await?;48 futures::stream::iter(hosts.iter())49 futures::stream::iter(hosts.iter())49 .then(|m| self.recipient(m.as_ref()))50 .then(|m| self.recipient(m.as_ref()))50 .try_collect::<Vec<_>>()51 .try_collect::<Vec<_>>()modules/nixos/secrets.nixdiffbeforeafterboth42 description = "Secret public data (only available for plaintext)";42 description = "Secret public data (only available for plaintext)";43 };43 };4445 expectedGenerationData = mkOption {46 type = unspecified;47 description = "Data that gets embedded into secret part";48 default = null;49 };50 generationData = mkOption {51 type = unspecified;52 description = "Data that is embedded into secret part";53 default = null;54 };55 };44 };56 config = {45 config = {57 hash = hashString "sha1" config.raw;46 hash = hashString "sha1" config.raw;91 default = sysConfig.users.users.${config.owner}.group;80 default = sysConfig.users.users.${config.owner}.group;92 defaultText = literalExpression "config.users.users.$${owner}.group";81 defaultText = literalExpression "config.users.users.$${owner}.group";93 };82 };83 expectedGenerationData = mkOption {84 type = unspecified;85 description = "Data that gets embedded into secret part";86 default = null;87 };94 };88 };95 });89 });96 processPart = part: {90 processPart = part: {modules/secrets-data.nixdiffbeforeafterboth6}: let6}: let7 inherit (fleetLib.options) mkDataOption;7 inherit (fleetLib.options) mkDataOption;8 inherit (lib.options) mkOption;8 inherit (lib.options) mkOption;9 inherit (lib.types) nullOr listOf str attrsOf submodule bool;9 inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;10 inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;10 inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;11 inherit (lib.lists) sort unique concatLists;11 inherit (lib.lists) sort unique concatLists;12 inherit (lib.strings) toJSON;12 inherit (lib.strings) toJSON;46 '';46 '';47 default = [];47 default = [];48 };48 };49 generationData = mkOption {50 type = unspecified;51 description = "Data that is embedded into secret part";52 default = null;53 };49 };54 };50 };55 };515667 description = "On which date this secret will expire, someone should regenerate this secret before it expires.";72 description = "On which date this secret will expire, someone should regenerate this secret before it expires.";68 default = false;73 default = false;69 };74 };75 generationData = mkOption {76 type = unspecified;77 description = "Data that is embedded into secret part";78 default = null;79 };70 };80 };71 };81 };72in {82in {93 });103 });94 config = {104 config = {95 assertions =105 assertions =96 mapAttrsToList106 (mapAttrsToList97 (name: secret: {107 (name: secret: {98 assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;108 assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;99 message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${toJSON config.data.sharedSecrets.${name}.owners}. Run fleet secrets regenerate to fix";109 message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${toJSON config.data.sharedSecrets.${name}.owners}. Run fleet secrets regenerate to fix";100 })110 })101 config.sharedSecrets;111 config.sharedSecrets)112 ++ (mapAttrsToList113 (name: secret: {114 # TODO: Same aassertion should be in host secrets115 assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;116 message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${toJSON config.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";117 })118 config.sharedSecrets);102 sharedSecrets =119 sharedSecrets =103 mapAttrs (_: _: {}) config.data.sharedSecrets;120 mapAttrs (_: _: {}) config.data.sharedSecrets;104 };121 };modules/secrets.nixdiffbeforeafterboth45 description = "Derivation to evaluate for secret generation";45 description = "Derivation to evaluate for secret generation";46 default = null;46 default = null;47 };47 };48 expectedGenerationData = mkOption {49 type = unspecified;50 description = "Data that gets embedded into secret part";51 default = null;52 };48 };53 };49 };54 };50in {55in {