git.delta.rocks / jrsonnet / refs/commits / d8f3e18873ec

difftreelog

refactor move parts to secret generator derivation

rquyvuskYaroslav Bolyukin2025-11-05parent: #488d19a.patch.diff
in: trunk

5 files changed

modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -158,7 +158,7 @@
 	expectations: &Expectations,
 ) -> Result<FleetSharedSecret> {
 	let reason = secret_needs_regeneration(&secret.secret, &secret.owners, expectations);
-	let value = definition.inner();
+	let value = definition.definition_value();
 
 	let (should_reencrypt, reason) = match reason {
 		Some(RegenerationReason::OwnersAdded(_)) => {
@@ -401,7 +401,13 @@
 	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);
 	Ok(FleetSharedSecret {
 		managed: Some(true),
-		secret: generate(config, display_name, secret.inner(), expectations).await?,
+		secret: generate(
+			config,
+			display_name,
+			secret.definition_value(),
+			expectations,
+		)
+		.await?,
 		owners: expectations.owners.clone(),
 	})
 }
@@ -711,7 +717,9 @@
 				}
 
 				let definition = config.shared_secret_definition(&name)?;
-				let expectations = definition.expectations()?;
+				let expectations = definition
+					.expectations()
+					.with_context(|| format!("expectations for shared {name:?}"))?;
 
 				let updated = maybe_regenerate_shared_secret(
 					&name,
@@ -744,7 +752,9 @@
 							info!("skipping unmanaged secret: {missing}");
 							continue;
 						}
-						let expectations = definition.expectations()?;
+						let expectations = definition
+							.expectations()
+							.with_context(|| format!("expectations for shared {missing:?}"))?;
 						info!("generating secret: {missing}");
 						let shared = generate_shared(config, missing, definition, &expectations)
 							.in_current_span()
@@ -768,13 +778,18 @@
 							.into_iter()
 							.collect::<HashSet<_>>();
 						for missing_secret in expected_set.difference(&stored_set) {
+							let secret = host.secret_definition(missing_secret)?;
+							if secret.is_shared()? {
+								continue;
+							}
 							info!("generating missing secret: {missing_secret}");
-							let definition = host.secret_definition(missing_secret)?;
-							let expectations = definition.expectations()?;
+							let expectations = secret.expectations().with_context(|| {
+								format!("expectations for {missing_secret:?} of {:?}", host.name)
+							})?;
 							let generated = match generate(
 								config,
 								missing_secret,
-								definition.inner(),
+								secret.definition_value()?,
 								&expectations,
 							)
 							.in_current_span()
@@ -796,16 +811,19 @@
 							)
 						}
 						for known_secret in stored_set.intersection(&expected_set) {
+							let secret = host.secret_definition(known_secret)?;
+							if secret.is_shared()? {
+								continue;
+							}
 							info!("updating secret: {known_secret}");
 							let data = config.host_secret(&host.name, known_secret)?;
-							let definition = host.secret_definition(known_secret)?;
-							let expectations = definition.expectations()?;
+							let expectations = secret.expectations()?;
 							if let Some(regen_reason) = data.needs_regeneration(&expectations) {
 								info!("needs regeneration: {regen_reason}");
 								let generated = match generate(
 									config,
 									known_secret,
-									definition.inner(),
+									secret.definition_value()?,
 									&expectations,
 								)
 								.in_current_span()
@@ -828,6 +846,10 @@
 							}
 						}
 						for removed_secret in stored_set.difference(&expected_set) {
+							let definition = host.secret_definition(removed_secret)?;
+							if definition.is_shared()? {
+								continue;
+							}
 							info!("removing secret: {removed_secret}");
 							config.remove_secret(&host.name, removed_secret);
 						}
modifiedcrates/fleet-base/src/secret.rsdiffbeforeafterboth
before · crates/fleet-base/src/secret.rs
1use std::collections::BTreeSet;23use anyhow::Result;4use chrono::{DateTime, Utc};5use nix_eval::{Value, nix_go, nix_go_json};67use crate::fleetdata::FleetSecretData;89#[derive(Debug)]10pub struct Expectations {11	pub owners: BTreeSet<String>,12	pub generation_data: serde_json::Value,13	pub public_parts: BTreeSet<String>,14	pub private_parts: BTreeSet<String>,15}1617pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);18impl HostSecretDefinition {19	pub fn is_managed(&self) -> Result<bool> {20		let value = &self.1;21		Ok(!nix_go!(value.generator).is_null())22	}23	pub fn expectations(&self) -> Result<Expectations> {24		let value = &self.1;25		Ok(Expectations {26			owners: BTreeSet::from([self.0.clone()]),27			generation_data: nix_go_json!(value.expectedGenerationData),28			public_parts: nix_go_json!(value.expectedPublicParts),29			private_parts: nix_go_json!(value.expectedPrivateParts),30		})31	}32	pub fn inner(&self) -> Value {33		self.1.clone()34	}35}3637pub struct SharedSecretDefinition(pub(crate) Value);38impl SharedSecretDefinition {39	pub fn is_managed(&self) -> Result<bool> {40		let value = &self.0;41		Ok(!nix_go!(value.generator).is_null())42	}43	pub fn expectations(&self) -> Result<Expectations> {44		let value = &self.0;45		Ok(Expectations {46			owners: nix_go_json!(value.expectedOwners),47			generation_data: nix_go_json!(value.expectedGenerationData),48			public_parts: nix_go_json!(value.expectedPublicParts),49			private_parts: nix_go_json!(value.expectedPrivateParts),50		})51	}52	pub fn inner(&self) -> Value {53		self.0.clone()54	}55}5657#[derive(thiserror::Error, Debug)]58pub enum RegenerationReason {59	#[error("owners added: {0:?}")]60	OwnersAdded(BTreeSet<String>),61	#[error("owners added: {0:?}")]62	OwnersRemoved(BTreeSet<String>),63	#[error("unexpected generation data, expected: {expected:?}, found: {found:?}")]64	GenerationData {65		expected: serde_json::Value,66		found: serde_json::Value,67	},68	#[error("unexpected part list, expected: {expected:?}, found: {found:?}")]69	PartList {70		expected: BTreeSet<String>,71		found: BTreeSet<String>,72	},73	#[error("part {0} is expected to be encrypted")]74	ExpectedPrivate(String),75	#[error("part {0} is not expected to be encrypted")]76	ExpectedPublic(String),77	#[error("secret is expired at {0}")]78	Expired(DateTime<Utc>),79}8081pub fn secret_needs_regeneration(82	secret: &FleetSecretData,83	owners: &BTreeSet<String>,84	expectations: &Expectations,85) -> Option<RegenerationReason> {86	if !owners.is_empty() {87		let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();88		if !added.is_empty() {89			return Some(RegenerationReason::OwnersAdded(added));90		}9192		let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();93		if !removed.is_empty() {94			return Some(RegenerationReason::OwnersRemoved(removed));95		}96	}9798	if secret.generation_data != expectations.generation_data {99		return Some(RegenerationReason::GenerationData {100			expected: expectations.generation_data.clone(),101			found: secret.generation_data.clone(),102		});103	}104105	if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {106		let expected: BTreeSet<String> = expectations107			.public_parts108			.union(&expectations.private_parts)109			.cloned()110			.collect();111		let found: BTreeSet<String> = secret.parts.keys().cloned().collect();112113		if found != expected {114			return Some(RegenerationReason::PartList { expected, found });115		}116117		for (name, value) in secret.parts.iter() {118			if value.raw.encrypted {119				if !expectations.private_parts.contains(name) {120					return Some(RegenerationReason::ExpectedPrivate(name.clone()));121				}122			} else if !expectations.public_parts.contains(name) {123				return Some(RegenerationReason::ExpectedPublic(name.clone()));124			}125		}126	}127128	if let Some(expiration) = secret.expires_at {129		// TODO: Leeway?130		if expiration < Utc::now() {131			return Some(RegenerationReason::Expired(expiration));132		}133	}134135	None136}
after · crates/fleet-base/src/secret.rs
1use std::collections::BTreeSet;23use anyhow::Result;4use chrono::{DateTime, Utc};5use nix_eval::{Value, nix_go, nix_go_json};67use crate::fleetdata::FleetSecretData;89#[derive(Debug)]10pub struct Expectations {11	pub owners: BTreeSet<String>,12	pub generation_data: serde_json::Value,13	pub public_parts: BTreeSet<String>,14	pub private_parts: BTreeSet<String>,15}1617pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);18impl HostSecretDefinition {19	pub fn is_managed(&self) -> Result<bool> {20		let def = self.definition_value()?;21		Ok(!nix_go!(def.generator).is_null())22	}23	pub fn is_shared(&self) -> Result<bool> {24		let def = self.definition_value()?;25		Ok(nix_go_json!(def.shared))26	}27	pub fn expectations(&self) -> Result<Expectations> {28		let def = self.definition_value()?;29		let parts = nix_go!(def.parts);3031		let mut public_parts = BTreeSet::new();32		let mut private_parts = BTreeSet::new();33		for part in parts.list_fields()? {34			if nix_go_json!(parts[&part].encrypted) {35				private_parts.insert(part.clone());36			} else {37				public_parts.insert(part.clone());38			}39		}4041		Ok(Expectations {42			owners: BTreeSet::from([self.0.clone()]),43			generation_data: nix_go_json!(def.expectedGenerationData),44			public_parts,45			private_parts,46		})47	}48	pub fn definition_value(&self) -> Result<Value> {49		let value = &self.1;50		Ok(nix_go!(value.definition))51	}52}5354pub struct SharedSecretDefinition(pub(crate) Value);55impl SharedSecretDefinition {56	pub fn is_managed(&self) -> Result<bool> {57		let value = &self.0;58		Ok(!nix_go!(value.generator).is_null())59	}60	pub fn expectations(&self) -> Result<Expectations> {61		let value = &self.0;62		Ok(Expectations {63			owners: nix_go_json!(value.expectedOwners),64			generation_data: nix_go_json!(value.expectedGenerationData),65			public_parts: nix_go_json!(value.expectedPublicParts),66			private_parts: nix_go_json!(value.expectedPrivateParts),67		})68	}69	pub fn definition_value(&self) -> Value {70		self.0.clone()71	}72}7374#[derive(thiserror::Error, Debug)]75pub enum RegenerationReason {76	#[error("owners added: {0:?}")]77	OwnersAdded(BTreeSet<String>),78	#[error("owners added: {0:?}")]79	OwnersRemoved(BTreeSet<String>),80	#[error("unexpected generation data, expected: {expected:?}, found: {found:?}")]81	GenerationData {82		expected: serde_json::Value,83		found: serde_json::Value,84	},85	#[error("unexpected part list, expected: {expected:?}, found: {found:?}")]86	PartList {87		expected: BTreeSet<String>,88		found: BTreeSet<String>,89	},90	#[error("part {0} is expected to be encrypted")]91	ExpectedPrivate(String),92	#[error("part {0} is not expected to be encrypted")]93	ExpectedPublic(String),94	#[error("secret is expired at {0}")]95	Expired(DateTime<Utc>),96}9798pub fn secret_needs_regeneration(99	secret: &FleetSecretData,100	owners: &BTreeSet<String>,101	expectations: &Expectations,102) -> Option<RegenerationReason> {103	if !owners.is_empty() {104		let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();105		if !added.is_empty() {106			return Some(RegenerationReason::OwnersAdded(added));107		}108109		let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();110		if !removed.is_empty() {111			return Some(RegenerationReason::OwnersRemoved(removed));112		}113	}114115	if secret.generation_data != expectations.generation_data {116		return Some(RegenerationReason::GenerationData {117			expected: expectations.generation_data.clone(),118			found: secret.generation_data.clone(),119		});120	}121122	if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {123		let expected: BTreeSet<String> = expectations124			.public_parts125			.union(&expectations.private_parts)126			.cloned()127			.collect();128		let found: BTreeSet<String> = secret.parts.keys().cloned().collect();129130		if found != expected {131			return Some(RegenerationReason::PartList { expected, found });132		}133134		for (name, value) in secret.parts.iter() {135			if value.raw.encrypted {136				if !expectations.private_parts.contains(name) {137					return Some(RegenerationReason::ExpectedPrivate(name.clone()));138				}139			} else if !expectations.public_parts.contains(name) {140				return Some(RegenerationReason::ExpectedPublic(name.clone()));141			}142		}143	}144145	if let Some(expiration) = secret.expires_at {146		// TODO: Leeway?147		if expiration < Utc::now() {148			return Some(RegenerationReason::Expired(expiration));149		}150	}151152	None153}
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -57,215 +57,191 @@
 
   inherit (modules) mkFleetDefault mkFleetGeneratorDefault;
 
-  secrets =
-    let
-      describedGenerator =
-        generator: {parts ? {}}:
-        {parts = {};}
-        // {
-          __functionArgs = functionArgs generator;
-          __functor = _: generator;
-        };
-    in
-    {
-      inherit describedGenerator;
+  secrets = {
 
-      /**
-        Generate a random secret password, 32 ascii characters by default
+    /**
+      Generate a random secret password, 32 ascii characters by default
 
-        Options:
-          size: generated password length in ascii characters (bytes).
-          noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might
-                     not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].
+      Options:
+        size: generated password length in ascii characters (bytes).
+        noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might
+                   not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].
 
-        Output:
-          Resulting secret has only part: secret, which contains encrypted password.
-      */
-      mkPassword =
+      Output:
+        Resulting secret has only part: secret, which contains encrypted password.
+    */
+    mkPassword =
+      {
+        size ? 32,
+      }:
+      (
         {
-          size ? 32,
+          coreutils,
+          mkSecretGenerator,
         }:
-        describedGenerator
-          (
-            {
-              coreutils,
-              mkSecretGenerator,
-            }:
-            mkSecretGenerator {
-              script = ''
-                mkdir $out
-                gh generate password -o $out/secret --size ${toString size}
-              '';
-            }
-          )
-          {
-            parts.secret.encrypted = true;
-          };
+        mkSecretGenerator {
+          script = ''
+            mkdir $out
+            gh generate password -o $out/secret --size ${toString size}
+          '';
+          parts.secret.encrypted = true;
+        }
+      );
 
-      /**
-        Generate a random ed25519 keypair
+    /**
+      Generate a random ed25519 keypair
 
-        Options:
-          noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)
-                         When noEmbedPublis is enabled - only the private scalar is included.
-          encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
+      Options:
+        noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)
+                       When noEmbedPublis is enabled - only the private scalar is included.
+        encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
 
-        Output:
-          Resulting secret has two parts: public and secret, where the secret part is encrypted.
+      Output:
+        Resulting secret has two parts: public and secret, where the secret part is encrypted.
+
+      This secret format is used by e.g Garage S3 server
+    */
+    mkEd25519 =
+      {
+        noEmbedPublic ? false,
+        encoding ? null,
+      }:
+      (
+        { mkSecretGenerator }:
+        mkSecretGenerator {
+          script = ''
+            mkdir $out
+            gh generate ed25519 -p $out/public -s $out/secret \
+              ${optionalString noEmbedPublic "--no-embed-public"} \
+              ${optionalString (encoding != null) "--encoding=${encoding}"}
+          '';
+          parts.secret.encrypted = true;
+          parts.public.encrypted = false;
+        }
+      );
 
-        This secret format is used by e.g Garage S3 server
-      */
-      mkEd25519 =
-        {
-          noEmbedPublic ? false,
-          encoding ? null,
-        }:
-        describedGenerator
-          (
-            { mkSecretGenerator }:
-            mkSecretGenerator {
-              script = ''
-                mkdir $out
-                gh generate ed25519 -p $out/public -s $out/secret \
-                  ${optionalString noEmbedPublic "--no-embed-public"} \
-                  ${optionalString (encoding != null) "--encoding=${encoding}"}
-              '';
-            }
-          )
-          {
-            parts.secret.encrypted = true;
-            parts.public.encrypted = false;
-          };
+    /**
+      Generate a random x25519 keypair
 
-      /**
-        Generate a random x25519 keypair
+      Options:
+        encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
 
-        Options:
-          encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".
+      Output:
+        Resulting secret has two parts: public and secret, where the secret part is encrypted.
 
-        Output:
-          Resulting secret has two parts: public and secret, where the secret part is encrypted.
+      This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
+    */
+    mkX25519 =
+      {
+        encoding ? null,
+      }:
+      (
+        { mkSecretGenerator }:
+        mkSecretGenerator {
+          script = ''
+            mkdir $out
+            gh generate x25519 -p $out/public -s $out/secret \
+              ${optionalString (encoding != null) "--encoding=${encoding}"}
+          '';
 
-        This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
-      */
-      mkX25519 =
-        {
-          encoding ? null,
-        }:
-        describedGenerator
-          (
-            { mkSecretGenerator }:
-            mkSecretGenerator {
-              script = ''
-                mkdir $out
-                gh generate x25519 -p $out/public -s $out/secret \
-                  ${optionalString (encoding != null) "--encoding=${encoding}"}
-              '';
-            }
-          )
-          {
-            parts.secret.encrypted = true;
-            parts.public.encrypted = false;
-          };
+          parts.secret.encrypted = true;
+          parts.public.encrypted = false;
+        }
+      );
 
-      /**
-        Generate a random RSA keypair
+    /**
+      Generate a random RSA keypair
 
-        Options:
-          size: RSA key size, 4096 by default
+      Options:
+        size: RSA key size, 4096 by default
 
-        Output:
-          Resulting secret has two parts: public and secret, where the secret part is encrypted.
-          Both parts are PEM encoded.
-      */
-      mkRsa =
+      Output:
+        Resulting secret has two parts: public and secret, where the secret part is encrypted.
+        Both parts are PEM encoded.
+    */
+    mkRsa =
+      {
+        size ? 4096,
+      }:
+      (
         {
-          size ? 4096,
+          openssl,
+          mkSecretGenerator,
         }:
-        describedGenerator
-          (
-            {
-              openssl,
-              mkSecretGenerator,
-            }:
-            mkSecretGenerator {
-              script = ''
-                mkdir $out
+        mkSecretGenerator {
+          script = ''
+            mkdir $out
+
+            ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
+            ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
 
-                ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
-                ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
+            cat rsa_private.key | gh private -o $out/secret
+            cat rsa_public.key | gh public -o $out/public
+          '';
 
-                cat rsa_private.key | gh private -o $out/secret
-                cat rsa_public.key | gh public -o $out/public
-              '';
-            }
-          )
-          {
-            parts.secret.encrypted = true;
-            parts.public.encrypted = false;
-          };
+          parts.secret.encrypted = true;
+          parts.public.encrypted = false;
+        }
+      );
 
-      /**
-        Generate a random byte sequence
+    /**
+      Generate a random byte sequence
 
-        Options:
-          size: generated password length in bytes, 32 by default.
-          encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"
-          noNuls: prevent output byte sequence from containing internal \0, useful for some C applications
-                  that can't handle their strings properly.
+      Options:
+        size: generated password length in bytes, 32 by default.
+        encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"
+        noNuls: prevent output byte sequence from containing internal \0, useful for some C applications
+                that can't handle their strings properly.
 
-        Output:
-          Resulting secret has only part: secret, which contains encrypted bytes.
+      Output:
+        Resulting secret has only part: secret, which contains encrypted bytes.
 
-        Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)
-      */
-      mkBytes =
-        {
-          count ? 32,
-          encoding,
-          noNuls ? false,
-        }:
-        describedGenerator
-          (
-            { mkSecretGenerator }:
-            mkSecretGenerator {
-              script = ''
-                mkdir $out
-                gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \
-                  ${optionalString noNuls "--no-nuls"}
-              '';
-            }
-          )
-          {
-            parts.secret.encrypted = true;
-          };
-      /**
-        Shorthand for `mkBytes`, which defaults to "hex" encoding
-      */
-      mkHexBytes =
-        {
-          count ? 32,
-        }:
-        mkBytes {
-          inherit count;
-          encoding = "hex";
-        };
-      /**
-        Shorthand for `mkBytes`, which defaults to "base64" encoding
-      */
-      mkBase64Bytes =
-        {
-          count ? 32,
-        }:
-        mkBytes {
-          inherit count;
-          encoding = "base64";
-        };
+      Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)
+    */
+    mkBytes =
+      {
+        count ? 32,
+        encoding,
+        noNuls ? false,
+      }:
+      (
+        { mkSecretGenerator }:
+        mkSecretGenerator {
+          script = ''
+            mkdir $out
+            gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \
+              ${optionalString noNuls "--no-nuls"}
+          '';
+          parts.secret.encrypted = true;
+        }
+      );
+    /**
+      Shorthand for `mkBytes`, which defaults to "hex" encoding
+    */
+    mkHexBytes =
+      {
+        count ? 32,
+      }:
+      mkBytes {
+        inherit count;
+        encoding = "hex";
+      };
+    /**
+      Shorthand for `mkBytes`, which defaults to "base64" encoding
+    */
+    mkBase64Bytes =
+      {
+        count ? 32,
+      }:
+      mkBytes {
+        inherit count;
+        encoding = "base64";
+      };
 
-      # Wireguard
-      # mkWireguard = {}: mkX25519 {encoding = "base64";};
-      # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};
-    };
+    # Wireguard
+    # mkWireguard = {}: mkX25519 {encoding = "base64";};
+    # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};
+  };
 
   inherit (secrets)
     mkPassword
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -105,10 +105,14 @@
     in
     {
       options = {
+        shared = mkOption {
+          type = bool;
+          description = "Was this secret propagated from a shared secret?";
+        };
         parts = mkOption {
           type = lazyAttrsOf (secretPartType secretName);
           description = "Definition of secret parts";
-          default = {};
+          default = { };
         };
         generator = mkOption {
           type = uniq (nullOr (functionTo package));
@@ -137,24 +141,39 @@
           default = null;
         };
       };
-      config.parts = mkMerge [
-        (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)
-        (mapAttrs (_: _: {}) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) ["shared" "managed"]))
-      ];
+      config = {
+        shared = (sysConfig.data.secrets.${secretName} or { shared = false; }).shared;
+        parts = mkMerge [
+          (mkIf (config.generator != null)
+            (
+              # Get fake derivation body, in future it should be implemented the same way as in Rust.
+              lib.callPackageWith (
+                pkgs
+                // {
+                  mkSecretGenerator = pkgs.stdenv.mkDerivation;
+                  mkImpureSecretGenerator = pkgs.stdenv.mkDerivation;
+                }
+              ) config.generator { }
+            ).parts
+          )
+          (mapAttrs (_: _: { }) (
+            removeAttrs (sysConfig.data.secrets.${secretName} or { }) [
+              "shared"
+              "managed"
+            ]
+          ))
+        ];
+      };
     }
   );
   processPart = secretName: partName: part: {
     inherit (part) path stablePath;
     raw = config.data.secrets.${secretName}.${partName}.raw;
   };
-  processSecret =
-    secretName: secret:
-    {
-      inherit (secret.definition) group mode owner;
-      parts = (mapAttrs (processPart secretName) (
-        secret.definition.parts
-      ));
-    };
+  processSecret = secretName: secret: {
+    inherit (secret.definition) group mode owner;
+    parts = (mapAttrs (processPart secretName) (secret.definition.parts));
+  };
   secretsData = (mapAttrs (processSecret) config.secrets);
   secretsFile = pkgs.writeTextFile {
     name = "secrets.json";
@@ -174,7 +193,7 @@
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
-      apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v);
+      apply = v: (mapAttrs (_: secret: secret.parts // { definition = secret; }) v);
       description = "Host-local secrets";
     };
     system.secretsData = mkOption {
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -124,6 +124,7 @@
                 # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
                 # (Some secrets-encryption-in-git/managed PKI solution is expected)
                 impureOn ? null,
+                parts,
               }:
               (prev.writeShellScript "impureGenerator.sh" ''
                 #!/bin/sh
@@ -151,12 +152,12 @@
               '').overrideAttrs
                 (old: {
                   passthru = {
-                    inherit impureOn;
+                    inherit impureOn parts;
                     generatorKind = "impure";
                   };
                 });
             # Pure generators are disabled for now
-            mkSecretGenerator = { script }: mkImpureSecretGenerator { inherit script; };
+            mkSecretGenerator = { script, parts }: mkImpureSecretGenerator { inherit script parts; };
 
             # TODO: Implement consistent naming
             # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...