git.delta.rocks / jrsonnet / refs/commits / 3e7b063c34a7

difftreelog

feat secret regeneration

Yaroslav Bolyukin2024-11-30parent: #2a6bd68.patch.diff
in: trunk

6 files changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
130 },130 },
131}131}
132132
133fn 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 = secret
140 .expires_at
141 .map(|expiration| expiration < Utc::now())
142 .unwrap_or(false);
143 expired || !data_is_expected
144}
145
146#[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<_>>();
162
163 let regeneration_required =
164 secret_needs_regeneration(&secret.secret, &expected_generation_data);
147165
148 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 }
152170
153 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 true
174 } 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 };
162183
163 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);
233262
234 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 expected
292 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 .await
342 }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 .await
345 }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,
615661
616 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);
618665
619 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 shared
634 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 = config
637 .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 .await
772 {
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 }
715795
716 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 )
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -1,5 +1,6 @@
 use std::{
 	cell::OnceCell,
+	collections::BTreeSet,
 	ffi::{OsStr, OsString},
 	fmt::Display,
 	io::Write,
@@ -312,6 +313,23 @@
 }
 
 impl Config {
+	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {
+		let config = &self.config_field;
+		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);
+		Ok(tagged)
+	}
+	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {
+		let mut out = BTreeSet::new();
+		for owner in owners {
+			if let Some(tag) = owner.strip_prefix('@') {
+				let hosts = self.tagged_hostnames(tag).await?;
+				out.extend(hosts);
+			} else {
+				out.insert(owner);
+			}
+		}
+		Ok(out)
+	}
 	pub fn local_host(&self) -> ConfigHost {
 		ConfigHost {
 			config: self.clone(),
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -45,6 +45,7 @@
 	}
 
 	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {
+		let hosts = self.expand_owner_set(hosts).await?;
 		futures::stream::iter(hosts.iter())
 			.then(|m| self.recipient(m.as_ref()))
 			.try_collect::<Vec<_>>()
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -41,17 +41,6 @@
           type = str;
           description = "Secret public data (only available for plaintext)";
         };
-
-        expectedGenerationData = mkOption {
-          type = unspecified;
-          description = "Data that gets embedded into secret part";
-          default = null;
-        };
-        generationData = mkOption {
-          type = unspecified;
-          description = "Data that is embedded into secret part";
-          default = null;
-        };
       };
       config = {
         hash = hashString "sha1" config.raw;
@@ -91,6 +80,11 @@
         default = sysConfig.users.users.${config.owner}.group;
         defaultText = literalExpression "config.users.users.$${owner}.group";
       };
+      expectedGenerationData = mkOption {
+        type = unspecified;
+        description = "Data that gets embedded into secret part";
+        default = null;
+      };
     };
   });
   processPart = part: {
modifiedmodules/secrets-data.nixdiffbeforeafterboth
--- a/modules/secrets-data.nix
+++ b/modules/secrets-data.nix
@@ -6,7 +6,7 @@
 }: let
   inherit (fleetLib.options) mkDataOption;
   inherit (lib.options) mkOption;
-  inherit (lib.types) nullOr listOf str attrsOf submodule bool;
+  inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;
   inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;
   inherit (lib.lists) sort unique concatLists;
   inherit (lib.strings) toJSON;
@@ -46,6 +46,11 @@
         '';
         default = [];
       };
+      generationData = mkOption {
+        type = unspecified;
+        description = "Data that is embedded into secret part";
+        default = null;
+      };
     };
   };
 
@@ -67,6 +72,11 @@
         description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
         default = false;
       };
+      generationData = mkOption {
+        type = unspecified;
+        description = "Data that is embedded into secret part";
+        default = null;
+      };
     };
   };
 in {
@@ -93,12 +103,19 @@
   });
   config = {
     assertions =
-      mapAttrsToList
-      (name: secret: {
-        assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;
-        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";
-      })
-      config.sharedSecrets;
+      (mapAttrsToList
+        (name: secret: {
+          assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;
+          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";
+        })
+        config.sharedSecrets)
+      ++ (mapAttrsToList
+        (name: secret: {
+          # TODO: Same aassertion should be in host secrets
+          assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;
+          message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${toJSON config.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";
+        })
+        config.sharedSecrets);
     sharedSecrets =
       mapAttrs (_: _: {}) config.data.sharedSecrets;
   };
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -45,6 +45,11 @@
         description = "Derivation to evaluate for secret generation";
         default = null;
       };
+      expectedGenerationData = mkOption {
+        type = unspecified;
+        description = "Data that gets embedded into secret part";
+        default = null;
+      };
     };
   };
 in {