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
before · lib/default.nix
1# Shared functions for fleet configuration, available as `fleet` module argument2{ lib }:3let4  inherit (lib.trivial) isFunction;5  inherit (lib.options) mkOption mergeOneOption;6  inherit (lib.modules) mkOverride;7  inherit (lib.types)8    listOf9    submodule10    attrsOf11    mkOptionType12    ;13  inherit (lib.strings) optionalString hasPrefix removePrefix;14in15rec {16  types = {17    overlay = mkOptionType {18      name = "nixpkgs-overlay";19      description = "nixpkgs overlay";20      check = isFunction;21      merge = mergeOneOption;22    };23    listOfOverlay = listOf types.overlay;2425    mkHostsType = module: attrsOf (submodule module);26    mkDataType = module: submodule module;27  };2829  options = {30    mkHostsOption =31      module:32      mkOption {33        type = types.mkHostsType module;34      };35    mkDataOption =36      module:37      mkOption {38        type = types.mkDataType module;39      };40  };4142  inherit (options) mkHostsOption;4344  modules = {45    /**46      Use in places, where fleet might know better than nixpkgs defaults to47    */48    mkFleetDefault = mkOverride 999;49    /**50      Some generators use mkDefault, but optionDefault is set by nixpkgs.51    */52    mkFleetGeneratorDefault = mkOverride 1001;53  };5455  inherit (modules) mkFleetDefault mkFleetGeneratorDefault;5657  secrets = {58    /**59      Generate a random secret password, 32 ascii characters by default6061      Options:62        size: generated password length in ascii characters (bytes).63        noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might64                   not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].6566      Output:67        Resulting secret has only part: secret, which contains encrypted password.68    */69    mkPassword =70      {71        size ? 32,72      }:73      {74        coreutils,75        mkSecretGenerator,76      }:77      mkSecretGenerator {78        script = ''79          mkdir $out80          gh generate password -o $out/secret --size ${toString size}81        '';82      };8384    /**85      Generate a random ed25519 keypair8687      Options:88        noEmbedPublic: By default, secret key also embeds public key in itself ("extended" format, 64 bytes)89                       When noEmbedPublis is enabled - only the private scalar is included.90        encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".9192      Output:93        Resulting secret has two parts: public and secret, where the secret part is encrypted.9495      This secret format is used by e.g Garage S3 server96    */97    mkEd25519 =98      {99        noEmbedPublic ? false,100        encoding ? null,101      }:102      { mkSecretGenerator }:103      mkSecretGenerator {104        script = ''105          mkdir $out106          gh generate ed25519 -p $out/public -s $out/secret \107            ${optionalString noEmbedPublic "--no-embed-public"} \108            ${optionalString (encoding != null) "--encoding=${encoding}"}109        '';110      };111112    /**113      Generate a random x25519 keypair114115      Options:116        encoding: Encoring of public and secret parts, can be "raw" (default), "base64" or "hex".117118      Output:119        Resulting secret has two parts: public and secret, where the secret part is encrypted.120121      This secret format is used by e.g Wireguard VPN for peers (base64-encoded)122    */123    mkX25519 =124      {125        encoding ? null,126      }:127      { mkSecretGenerator }:128      mkSecretGenerator {129        script = ''130          mkdir $out131          gh generate x25519 -p $out/public -s $out/secret \132            ${optionalString (encoding != null) "--encoding=${encoding}"}133        '';134      };135136    /**137      Generate a random RSA keypair138139      Options:140        size: RSA key size, 4096 by default141142      Output:143        Resulting secret has two parts: public and secret, where the secret part is encrypted.144        Both parts are PEM encoded.145    */146    mkRsa =147      {148        size ? 4096,149      }:150      {151        openssl,152        mkSecretGenerator,153      }:154      mkSecretGenerator {155        script = ''156          mkdir $out157158          ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}159          ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key160161          cat rsa_private.key | gh private -o $out/secret162          cat rsa_public.key | gh public -o $out/public163        '';164      };165166    /**167      Generate a random byte sequence168169      Options:170        size: generated password length in bytes, 32 by default.171        encoding: how the generated bytes should be encoded, "raw" (default), "hex" or "base64"172        noNuls: prevent output byte sequence from containing internal \0, useful for some C applications173                that can't handle their strings properly.174175      Output:176        Resulting secret has only part: secret, which contains encrypted bytes.177178      Might be used for e.g. Wireguard VPN PSK keys (base64-encoded)179    */180    mkBytes =181      {182        count ? 32,183        encoding,184        noNuls ? false,185      }:186      { mkSecretGenerator }:187      mkSecretGenerator {188        script = ''189          mkdir $out190          gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \191            ${optionalString noNuls "--no-nuls"}192        '';193      };194    /**195      Shorthand for `mkBytes`, which defaults to "hex" encoding196    */197    mkHexBytes =198      {199        count ? 32,200      }:201      mkBytes {202        inherit count;203        encoding = "hex";204      };205    /**206      Shorthand for `mkBytes`, which defaults to "base64" encoding207    */208    mkBase64Bytes =209      {210        count ? 32,211      }:212      mkBytes {213        inherit count;214        encoding = "base64";215      };216217    # Wireguard218    # mkWireguard = {}: mkX25519 {encoding = "base64";};219    # mkWireguardPsk = {}: mkBase64Bytes {count = 32;};220  };221222  inherit (secrets)223    mkPassword224    mkEd25519225    mkX25519226    mkRsa227    mkBytes228    mkHexBytes229    mkBase64Bytes230    ;231232  strings =233    let234      plaintextPrefix = "<PLAINTEXT>";235      plaintextNewlinePrefix = "<PLAINTEXT-NL>";236    in237    {238      /**239        Decode public secret part into string240      */241      decodeRawSecret =242        raw:243        if hasPrefix plaintextPrefix raw then244          removePrefix plaintextPrefix raw245        else if hasPrefix plaintextNewlinePrefix raw then246          removePrefix plaintextNewlinePrefix raw247        else248          throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";249    };250251  inherit (strings) decodeRawSecret;252}
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -6,12 +6,18 @@
   ...
 }:
 let
-  inherit (builtins) hashString elemAt length toJSON filter;
+  inherit (builtins)
+    hashString
+    elemAt
+    length
+    toJSON
+    filter
+    ;
   inherit (lib.stringsWithDeps) stringAfter;
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.lists) optional;
   inherit (lib.attrsets) mapAttrs mapAttrsToList;
-  inherit (lib.modules) mkIf;
+  inherit (lib.modules) mkIf mkMerge;
   inherit (lib.types)
     submodule
     str
@@ -23,6 +29,7 @@
     functionTo
     package
     listOf
+    bool
     ;
   inherit (fleetLib.strings) decodeRawSecret;
 
@@ -54,6 +61,11 @@
       in
       {
         options = {
+          encrypted = mkOption {
+            type = bool;
+            description = "Is this secret part supposed to be encrypted?";
+          };
+
           hash = mkOption {
             type = str;
             description = "Hash of secret in encoded format";
@@ -86,28 +98,18 @@
   secretType = submodule (
     {
       config,
-      loc,
-      options,
       ...
     }:
     let
-      secretName =
-        # Due to config definition for freeformType, we can't just use _module.args due to infinite recursion, instead
-        # extract the secret name the ugly way...
-        let
-          saLoc = options._module.specialArgs.loc;
-          comp = elemAt saLoc;
-        in
-        assert
-          (length saLoc == 2 ||
-          length saLoc == 4 &&
-          comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") ||
-          throw "Unexpected module structure ${toJSON saLoc}";
-        if length saLoc == 2 then "documentation generator stub" else comp 1;
+      secretName = config._module.args.name;
     in
     {
-      freeformType = lazyAttrsOf (secretPartType secretName);
       options = {
+        parts = mkOption {
+          type = lazyAttrsOf (secretPartType secretName);
+          description = "Definition of secret parts";
+          default = {};
+        };
         generator = mkOption {
           type = uniq (nullOr (functionTo package));
           description = "Derivation to evaluate for secret generation";
@@ -134,18 +136,11 @@
           description = "Data that gets embedded into secret part";
           default = null;
         };
-        expectedPrivateParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be encrypted";
-        };
-        expectedPublicParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be public";
-        };
       };
-      config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]);
+      config.parts = mkMerge [
+        (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)
+        (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared"]))
+      ];
     }
   );
   processPart = secretName: partName: part: {
@@ -155,20 +150,11 @@
   processSecret =
     secretName: secret:
     {
-      inherit (secret) group mode owner;
-    }
-    // (mapAttrs (processPart secretName) (
-      removeAttrs secret [
-        "shared"
-        "generator"
-        "mode"
-        "group"
-        "owner"
-        "expectedGenerationData"
-        "expectedPrivateParts"
-        "expectedPublicParts"
-      ]
-    ));
+      inherit (secret.definition) group mode owner;
+      parts = (mapAttrs (processPart secretName) (
+        secret.definition.parts
+      ));
+    };
   secretsData = (mapAttrs (processSecret) config.secrets);
   secretsFile = pkgs.writeTextFile {
     name = "secrets.json";
@@ -188,29 +174,18 @@
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
+      apply = v: (mapAttrs (_: secret: secret.parts // {definition = secret;}) v);
       description = "Host-local secrets";
     };
     system.secretsData = mkOption {
       type = unspecified;
-      default = {};
+      default = { };
       description = "secrets.json contents";
     };
   };
   config = {
-    system = {inherit secretsData;};
+    system = { inherit secretsData; };
     environment.systemPackages = [ pkgs.fleet-install-secrets ];
-
-    warnings = filter (v: v!=null) (mapAttrsToList (
-      name: secret:
-      if
-        secret.expectedPrivateParts == [ ]
-        && secret.expectedPublicParts == [ ]
-        && !(config.data.secrets.${name} or { shared = false; }).shared
-      then
-        "Secret ${name} has no expected parts defined, this is deprecated for better visibility"
-      else
-        null
-    ) config.secrets);
 
     systemd.services.fleet-install-secrets = mkIf useSysusers {
       wantedBy = [ "sysinit.target" ];