git.delta.rocks / jrsonnet / refs/commits / 5b343db89280

difftreelog

feat infer secret parts from generator

uqnzwwslYaroslav Bolyukin2025-10-27parent: #3bff084.patch.diff
in: trunk

3 files changed

modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -58,7 +58,6 @@
 	owner: String,
 	root_path: Option<PathBuf>,
 
-	#[serde(flatten)]
 	parts: BTreeMap<String, Part>,
 }
 
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -54,170 +54,214 @@
 
   inherit (modules) mkFleetDefault mkFleetGeneratorDefault;
 
-  secrets = {
-    /**
-      Generate a random secret password, 32 ascii characters by default
+  secrets =
+    let
+      describedGenerator =
+        generator: {parts ? {}}:
+        {parts = {};}
+        // {
+          __functor = generator;
+        };
+    in
+    {
+      inherit describedGenerator;
 
-      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].
+      /**
+        Generate a random secret password, 32 ascii characters by default
 
-      Output:
-        Resulting secret has only part: secret, which contains encrypted password.
-    */
-    mkPassword =
-      {
-        size ? 32,
-      }:
-      {
-        coreutils,
-        mkSecretGenerator,
-      }:
-      mkSecretGenerator {
-        script = ''
-          mkdir $out
-          gh generate password -o $out/secret --size ${toString size}
-        '';
-      };
+        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].
 
-    /**
-      Generate a random ed25519 keypair
+        Output:
+          Resulting secret has only part: secret, which contains encrypted password.
+      */
+      mkPassword =
+        {
+          size ? 32,
+        }:
+        describedGenerator
+          (
+            {
+              coreutils,
+              mkSecretGenerator,
+            }:
+            mkSecretGenerator {
+              script = ''
+                mkdir $out
+                gh generate password -o $out/secret --size ${toString size}
+              '';
+            }
+          )
+          {
+            parts.secret.encrypted = true;
+          };
+
+      /**
+        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}"}
-        '';
-      };
+        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;
+          };
 
-    /**
-      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 =
-      {
-        size ? 4096,
-      }:
-      {
-        openssl,
-        mkSecretGenerator,
-      }:
-      mkSecretGenerator {
-        script = ''
-          mkdir $out
+        Output:
+          Resulting secret has two parts: public and secret, where the secret part is encrypted.
+          Both parts are PEM encoded.
+      */
+      mkRsa =
+        {
+          size ? 4096,
+        }:
+        describedGenerator
+          (
+            {
+              openssl,
+              mkSecretGenerator,
+            }:
+            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;
+          };
 
-    /**
-      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,
-      }:
-      { mkSecretGenerator }:
-      mkSecretGenerator {
-        script = ''
-          mkdir $out
-          gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \
-            ${optionalString noNuls "--no-nuls"}
-        '';
-      };
-    /**
-      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,
+        }:
+        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";
+        };
 
-    # 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
11 inherit (lib.options) mkOption literalExpression;17 inherit (lib.options) mkOption literalExpression;
12 inherit (lib.lists) optional;18 inherit (lib.lists) optional;
13 inherit (lib.attrsets) mapAttrs mapAttrsToList;19 inherit (lib.attrsets) mapAttrs mapAttrsToList;
14 inherit (lib.modules) mkIf;20 inherit (lib.modules) mkIf mkMerge;
15 inherit (lib.types)21 inherit (lib.types)
16 submodule22 submodule
17 str23 str
23 functionTo29 functionTo
24 package30 package
25 listOf31 listOf
32 bool
26 ;33 ;
27 inherit (fleetLib.strings) decodeRawSecret;34 inherit (fleetLib.strings) decodeRawSecret;
2835
54 in61 in
55 {62 {
56 options = {63 options = {
64 encrypted = mkOption {
65 type = bool;
66 description = "Is this secret part supposed to be encrypted?";
67 };
68
57 hash = mkOption {69 hash = mkOption {
58 type = str;70 type = str;
86 secretType = submodule (98 secretType = submodule (
87 {99 {
88 config,100 config,
89 loc,
90 options,
91 ...101 ...
92 }:102 }:
93 let103 let
94 secretName =104 secretName = config._module.args.name;
95 # Due to config definition for freeformType, we can't just use _module.args due to infinite recursion, instead
96 # extract the secret name the ugly way...
97 let
98 saLoc = options._module.specialArgs.loc;
99 comp = elemAt saLoc;
100 in
101 assert
102 (length saLoc == 2 ||
103 length saLoc == 4 &&
104 comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") ||
105 throw "Unexpected module structure ${toJSON saLoc}";
106 if length saLoc == 2 then "documentation generator stub" else comp 1;
107 in105 in
108 {106 {
107 options = {
108 parts = mkOption {
109 freeformType = lazyAttrsOf (secretPartType secretName);109 type = lazyAttrsOf (secretPartType secretName);
110 options = {110 description = "Definition of secret parts";
111 default = {};
112 };
111 generator = mkOption {113 generator = mkOption {
112 type = uniq (nullOr (functionTo package));114 type = uniq (nullOr (functionTo package));
113 description = "Derivation to evaluate for secret generation";115 description = "Derivation to evaluate for secret generation";
134 description = "Data that gets embedded into secret part";136 description = "Data that gets embedded into secret part";
135 default = null;137 default = null;
136 };138 };
137 expectedPrivateParts = mkOption {
138 type = listOf str;
139 default = [ ];
140 description = "List of parts that are expected to be encrypted";
141 };
142 expectedPublicParts = mkOption {
143 type = listOf str;
144 default = [ ];
145 description = "List of parts that are expected to be public";
146 };
147 };139 };
148 config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]);140 config.parts = mkMerge [
141 (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)
142 (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared"]))
143 ];
149 }144 }
150 );145 );
151 processPart = secretName: partName: part: {146 processPart = secretName: partName: part: {
154 };149 };
155 processSecret =150 processSecret =
156 secretName: secret:151 secretName: secret:
157 {152 {
158 inherit (secret) group mode owner;153 inherit (secret.definition) group mode owner;
159 }
160 // (mapAttrs (processPart secretName) (154 parts = (mapAttrs (processPart secretName) (
161 removeAttrs secret [155 secret.definition.parts
162 "shared"
163 "generator"
164 "mode"
165 "group"
166 "owner"
167 "expectedGenerationData"
168 "expectedPrivateParts"
169 "expectedPublicParts"
170 ]
171 ));156 ));
157 };
172 secretsData = (mapAttrs (processSecret) config.secrets);158 secretsData = (mapAttrs (processSecret) config.secrets);
173 secretsFile = pkgs.writeTextFile {159 secretsFile = pkgs.writeTextFile {
174 name = "secrets.json";160 name = "secrets.json";
188 secrets = mkOption {174 secrets = mkOption {
189 type = attrsOf secretType;175 type = attrsOf secretType;
190 default = { };176 default = { };
177 apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v);
191 description = "Host-local secrets";178 description = "Host-local secrets";
192 };179 };
193 system.secretsData = mkOption {180 system.secretsData = mkOption {
200 system = {inherit secretsData;};187 system = { inherit secretsData; };
201 environment.systemPackages = [ pkgs.fleet-install-secrets ];188 environment.systemPackages = [ pkgs.fleet-install-secrets ];
202
203 warnings = filter (v: v!=null) (mapAttrsToList (
204 name: secret:
205 if
206 secret.expectedPrivateParts == [ ]
207 && secret.expectedPublicParts == [ ]
208 && !(config.data.secrets.${name} or { shared = false; }).shared
209 then
210 "Secret ${name} has no expected parts defined, this is deprecated for better visibility"
211 else
212 null
213 ) config.secrets);
214189
215 systemd.services.fleet-install-secrets = mkIf useSysusers {190 systemd.services.fleet-install-secrets = mkIf useSysusers {
216 wantedBy = [ "sysinit.target" ];191 wantedBy = [ "sysinit.target" ];