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

difftreelog

doc: tried to improve docs, realised I'm still too lazy for that

Yaroslav Bolyukin2025-03-23parent: #a70ed4a.patch.diff
in: trunk

5 files changed

addeddocs/secrets.adocdiffbeforeafterboth
--- /dev/null
+++ b/docs/secrets.adoc
@@ -0,0 +1,35 @@
+= Fleet Secrets Management System
+
+== Overview
+
+Secret management system is a built-in way to deploy secrets to remote systems, similar to agenix and other similar systems.
+
+Secrets are encrypted using system's host ssh key (/etc/ssh/ssh_host_ed25519_key), which is not required to build the
+remote system/add secret to fleet configuration, fleet users are encrypting secrets using received public key instead,
+they don't need the root access to receive the public encryption key.
+
+== Example
+
+[source,nix]
+----
+{
+  fleet.secrets = {
+    "my-secret" = {
+      expectedOwners = [ "host1" "host2" ];
+      regenerateOnOwnerAdded = true;
+      generator = {mkImpureSecretGenerator}:
+        mkImpureSecretGenerator {
+          script = ''
+            echo "secret content" | gh private -o $out/secret
+          '';
+        };
+    };
+  }
+}
+----
+
+== Limitations and Future Improvements
+
+- Pure secret generators are currently disabled
+- Support for other secret management systems (e.g systemd-creds has planned asymmetric encryption support)
+
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -33,16 +33,30 @@
   inherit (options) mkHostsOption;
 
   modules = {
-    # mkDefault = mkOverride 1000
-    # For places, where fleet knows better than nixpkgs defaults.
+    /**
+      Use in places, where fleet might know better than nixpkgs defaults to
+    */
     mkFleetDefault = mkOverride 999;
-    # Some generators use mkDefault, but optionDefault is set by nixpkgs.
+    /**
+      Some generators use mkDefault, but optionDefault is set by nixpkgs.
+    */
     mkFleetGeneratorDefault = mkOverride 1001;
   };
 
   inherit (modules) mkFleetDefault mkFleetGeneratorDefault;
 
   secrets = {
+    /**
+      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].
+
+      Output:
+        Resulting secret has only part: secret, which contains encrypted password.
+    */
     mkPassword = {size ? 32}: {
       coreutils,
       mkSecretGenerator,
@@ -54,6 +68,19 @@
         '';
       };
 
+    /**
+      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".
+
+      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,
@@ -67,6 +94,17 @@
         '';
       };
 
+    /**
+      Generate a random x25519 keypair
+
+      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.
+
+      This secret format is used by e.g Wireguard VPN for peers (base64-encoded)
+    */
     mkX25519 = {encoding ? null}: {mkSecretGenerator}:
       mkSecretGenerator {
         script = ''
@@ -76,6 +114,16 @@
         '';
       };
 
+    /**
+      Generate a random RSA keypair
+
+      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,
@@ -92,6 +140,20 @@
         '';
       };
 
+    /**
+      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.
+
+      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,
@@ -104,11 +166,17 @@
             ${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;
@@ -126,6 +194,9 @@
     plaintextPrefix = "<PLAINTEXT>";
     plaintextNewlinePrefix = "<PLAINTEXT-NL>";
   in {
+    /**
+      Decode public secret part into string
+    */
     decodeRawSecret = raw:
       if hasPrefix plaintextPrefix raw
       then removePrefix plaintextPrefix raw
modifiedmodules/hosts.nixdiffbeforeafterboth
--- a/modules/hosts.nix
+++ b/modules/hosts.nix
@@ -11,6 +11,8 @@
   inherit (lib.attrsets) mapAttrsToList mapAttrs;
   inherit (lib.lists) flatten groupBy;
 in {
+  # Fleet Meta Configuration Module
+
   options = {
     data = mkOption {
       type = mkDataType {
@@ -18,74 +20,94 @@
           version = mkOption {
             type = str;
             internal = true;
+            description = "Internal version identifier for saved fleet state";
           };
+
           gcRootPrefix = mkOption {
             type = str;
             internal = true;
+            description = "Prefix for fleet-generated gc garbage collection roots";
           };
+
           hosts = mkOption {
             type = attrsOf (submodule {
               options.encryptionKey = mkOption {
                 type = str;
-                description = "Rage SSH encryption key for secrets.";
+                description = "Rage SSH encryption key for host-bound secrets";
               };
             });
           };
         };
       };
       description = ''
-        Configuration provided from outside.
-        Usually used to persist fleet data between runs.
+        Persistent configuration data for fleet management.
+        Typically used to maintain state between fleet configuration runs.
       '';
     };
+
     taggedWith = mkOption {
       type = attrsOf (listOf str);
       internal = true;
+      description = "Mapping of hosts grouped by tags, used by fleet CLI";
     };
+
     hosts = mkOption {
       type = mkHostsType ({config, ...}: {
         options = {
           system = mkOption {
-            description = "Type of the system.";
+            description = "System architecture and platform identifier";
             type = str;
             example = "x86_64-linux";
           };
+
           tags = mkOption {
-            description = "Host tag. In CLI, you can refer to all hosts having this tag using @tag syntax.";
+            description = ''
+              Tags for host classification.
+              Used for host selection via @tag syntax in CLI tools.
+            '';
             type = listOf str;
           };
+
+          # Network configuration details
           network = mkOption {
             type = submodule {
               options = {
                 internalIps = mkOption {
-                  description = "Internal ips";
+                  description = "List of internal IP addresses for the host";
                   type = listOf str;
                   default = [];
                 };
+
                 externalIps = mkOption {
-                  description = "External ips";
+                  description = "List of external IP addresses for the host";
                   type = listOf str;
                   default = [];
                 };
               };
             };
-            description = "Network definition of host";
           };
         };
         config = {
+          # Default hostname generation
           nixos.networking.hostName = mkFleetGeneratorDefault config._module.args.name;
+          # Default 'all' tag for every host
           tags = ["all"];
         };
         _file = ./meta.nix;
       });
       default = {};
-      description = "Configurations of individual hosts";
     };
   };
+
+  # Generate a mapping of hosts indexed by their tags
   config.taggedWith = let
+    # Flatten host tags into a list of {hostname, tag} pairs
     hostTagList = flatten (mapAttrsToList (hostname: host: map (tag: {inherit hostname tag;}) host.tags) config.hosts);
+    # Group hostnames by their tags
     grouped = mapAttrs (_: hosts: lib.map (pair: pair.hostname) hosts) (groupBy (elem: elem.tag) hostTagList);
   in
     grouped;
+
+  # Source file reference
   _file = ./meta.nix;
 }
modifiedmodules/secrets-data.nixdiffbeforeafterboth
before · modules/secrets-data.nix
1{2  lib,3  fleetLib,4  config,5  ...6}: let7  inherit (fleetLib.options) mkDataOption;8  inherit (lib.options) mkOption;9  inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;10  inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;11  inherit (lib.lists) sort unique concatLists;12  inherit (lib.strings) toJSON;1314  secretDataValue = {15    options = {16      raw = mkOption {17        type = nullOr str;18        description = "Encrypted + encoded secret data";19        default = null;20      };21    };22  };2324  sharedSecretData = {25    freeformType = attrsOf (submodule secretDataValue);26    options = {27      createdAt = mkOption {28        type = str;29        description = "When this secret was (re)generated";30        default = null;31      };32      expiresAt = mkOption {33        type = nullOr str;34        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";35        default = null;36      };3738      owners = mkOption {39        type = listOf str;40        description = ''41          For which owners this secret is currently encrypted,42          if not matches expectedOwners - then this secret is considered outdated, and43          should be regenerated/reencrypted.4445          Imported from fleet.nix46        '';47        default = [];48      };49      generationData = mkOption {50        type = unspecified;51        description = "Data that is embedded into secret part";52        default = null;53      };54    };55    config = {};56  };5758  hostSecretData = {59    freeformType = attrsOf (submodule secretDataValue);60    options = {61      createdAt = mkOption {62        type = str;63        description = "When this secret was (re)generated";64        default = null;65      };66      expiresAt = mkOption {67        type = nullOr str;68        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";69        default = null;70      };71      shared = mkOption {72        type = bool;73        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";74        default = false;75      };76      generationData = mkOption {77        type = unspecified;78        description = "Data that is embedded into secret part";79        default = null;80      };81    };82    config = {};83  };84in {85  options.data = mkDataOption ({config, ...}: {86    options = {87      sharedSecrets = mkOption {88        type = attrsOf (submodule sharedSecretData);89        default = {};90        description = "Stored shared secret data.";91      };92      hostSecrets = mkOption {93        type = attrsOf (attrsOf (submodule hostSecretData));94        default = {};95        description = "Host secrets.";96        internal = true;97      };98    };99    config.hostSecrets = let100      hostsWithSharedSecrets = unique (concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets));101      secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;102      toHostSecret = _: secret: (removeAttrs secret ["owners"]) // {shared = true;};103    in104      genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));105  });106  config = {107    assertions =108      (mapAttrsToList109        (name: secret: {110          assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;111          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";112        })113        config.sharedSecrets)114      ++ (mapAttrsToList115        (name: secret: {116          # TODO: Same aassertion should be in host secrets117          assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;118          message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${toJSON config.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";119        })120        config.sharedSecrets);121    sharedSecrets =122      mapAttrs (_: _: {}) config.data.sharedSecrets;123  };124}
after · modules/secrets-data.nix
1{2  lib,3  fleetLib,4  config,5  ...6}: let7  inherit (fleetLib.options) mkDataOption;8  inherit (lib.options) mkOption;9  inherit (lib.types) nullOr listOf str attrsOf submodule bool unspecified;10  inherit (lib.attrsets) mapAttrsToList mapAttrs filterAttrs genAttrs;11  inherit (lib.lists) sort unique concatLists;12  inherit (lib.strings) toJSON;1314  secretDataValue = {15    options = {16      raw = mkOption {17        type = nullOr str;18        description = "Raw secret data in unspecified encoded and optionally encrypted format.";19        default = null;20      };21    };22  };2324  sharedSecretData = {25    freeformType = attrsOf (submodule secretDataValue);26    options = {27      createdAt = mkOption {28        type = str;29        description = "Timestamp of secret generation/last rotation.";30        default = null;31      };32      expiresAt = mkOption {33        type = nullOr str;34        description = "Expiration timestamp triggering mandatory secret rotation.";35        default = null;36      };3738      owners = mkOption {39        type = listOf str;40        description = ''41          List of hosts currently authorized to decrypt this shared secret.4243          If owners differ from expected owners, the secret is considered outdated44          and requires regeneration or re-encryption.45        '';46        default = [];47      };48      generationData = mkOption {49        type = unspecified;50        description = "Contextual metadata associated with secret part.";51        default = null;52      };53    };54    config = {};55  };5657  hostSecretData = {58    freeformType = attrsOf (submodule secretDataValue);59    options = {60      createdAt = mkOption {61        type = str;62        description = "Timestamp of secret generation/last rotation.";63        default = null;64      };65      expiresAt = mkOption {66        type = nullOr str;67        description = "Expiration timestamp triggering mandatory secret rotation.";68        default = null;69      };70      shared = mkOption {71        type = bool;72        description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";73        default = false;74      };75      generationData = mkOption {76        type = unspecified;77        description = "Contextual metadata associated with secret part.";78        default = null;79      };80    };81    config = {};82  };83in {84  options.data = mkDataOption ({config, ...}: {85    options = {86      sharedSecrets = mkOption {87        type = attrsOf (submodule sharedSecretData);88        default = {};89        description = "Shared secret data.";90      };91      hostSecrets = mkOption {92        type = attrsOf (attrsOf (submodule hostSecretData));93        default = {};94        description = "Host-specific secrets.";95        internal = true;96      };97    };98    config.hostSecrets = let99      hostsWithSharedSecrets = unique (concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets));100      secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;101      toHostSecret = _: secret: (removeAttrs secret ["owners"]) // {shared = true;};102    in103      genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));104  });105  config = {106    assertions =107      (mapAttrsToList108        (name: secret: {109          assertion = secret.expectedOwners == null || sort (a: b: a < b) config.data.sharedSecrets.${name}.owners == sort (a: b: a < b) secret.expectedOwners;110          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";111        })112        config.sharedSecrets)113      ++ (mapAttrsToList114        (name: secret: {115          # TODO: Same aassertion should be in host secrets116          assertion = config.data.sharedSecrets.${name}.generationData == secret.expectedGenerationData;117          message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${toJSON config.data.sharedSecrets.${name}.expectedGenerationData}. Run fleet secrets regenerate to fix";118        })119        config.sharedSecrets);120    sharedSecrets =121      mapAttrs (_: _: {}) config.data.sharedSecrets;122  };123}
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -4,7 +4,7 @@
   ...
 }: let
   inherit (lib.options) mkOption literalExpression;
-  inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule;
+  inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule functionTo package;
   inherit (lib.strings) concatStringsSep;
   inherit (lib.attrsets) mapAttrs;
 
@@ -13,19 +13,20 @@
       expectedOwners = mkOption {
         type = nullOr (listOf str);
         description = ''
-          List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)
+          Specifies the list of hosts authorized to decrypt and access this shared secret.
 
-          Secrets would be decrypted and stored to /run/secrets/$\{name} on owners
+          When null, secret ownership is managed manually via fleet.nix and CLI.
+          Decrypted secrets will be stored at /run/secrets/$\{name} on authorized hosts.
         '';
         default = null;
       };
-      # TODO: Aren't those options may be just desugared to data/expectedData?
       regenerateOnOwnerAdded = mkOption {
         type = bool;
         description = ''
-          Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.
+          Controls whether the secret must be regenerated when new owners are added.
 
-          You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.
+          Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).
+          When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.
         '';
       };
       regenerateOnOwnerRemoved = mkOption {
@@ -33,21 +34,25 @@
         defaultText = literalExpression "regenerateOnOwnerAdded";
         type = bool;
         description = ''
-          Should this secret be removed on owner removal, or it may be just reencrypted
+          Determines secret behavior when owners are removed from the configuration.
 
-          Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.
-          Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked
-          in some other way than by this secret ownership, I.e by firewall/etc.
+          Typically mirrors regenerateOnOwnerAdded. Override cautiously.
+          Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
         '';
       };
       generator = mkOption {
-        type = nullOr unspecified;
-        description = "Derivation to evaluate for secret generation";
+        type = nullOr (functionTo package);
+        description = ''
+          Function evaluating to nix derivation responsible for (re)generating the secret's content.
+
+          An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,
+          use `mkSecretGenerator` helpers to implement own generators.
+        '';
         default = null;
       };
       expectedGenerationData = mkOption {
         type = unspecified;
-        description = "Data that gets embedded into secret part";
+        description = "Contextual metadata embedded within the secret part value";
         default = null;
       };
     };
@@ -57,7 +62,7 @@
     sharedSecrets = mkOption {
       type = attrsOf (submodule sharedSecret);
       default = {};
-      description = "Shared secrets";
+      description = "Collection of secrets shared across multiple hosts with configurable ownership";
     };
   };
   config = {