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
1use 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}
313314
314impl 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(),
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
45 }45 }
4646
47 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<_>>()
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
42 description = "Secret public data (only available for plaintext)";42 description = "Secret public data (only available for plaintext)";
43 };43 };
44
45 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: {
modifiedmodules/secrets-data.nixdiffbeforeafterboth
6}: let6}: let
7 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 };
5156
67 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 (mapAttrsToList
97 (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 ++ (mapAttrsToList
113 (name: secret: {
114 # TODO: Same aassertion should be in host secrets
115 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 };
modifiedmodules/secrets.nixdiffbeforeafterboth
45 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 {