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

difftreelog

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

Yaroslav Bolyukin2025-01-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
--- a/modules/secrets-data.nix
+++ b/modules/secrets-data.nix
@@ -15,7 +15,7 @@
     options = {
       raw = mkOption {
         type = nullOr str;
-        description = "Encrypted + encoded secret data";
+        description = "Raw secret data in unspecified encoded and optionally encrypted format.";
         default = null;
       };
     };
@@ -26,29 +26,28 @@
     options = {
       createdAt = mkOption {
         type = str;
-        description = "When this secret was (re)generated";
+        description = "Timestamp of secret generation/last rotation.";
         default = null;
       };
       expiresAt = mkOption {
         type = nullOr str;
-        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+        description = "Expiration timestamp triggering mandatory secret rotation.";
         default = null;
       };
 
       owners = mkOption {
         type = listOf str;
         description = ''
-          For which owners this secret is currently encrypted,
-          if not matches expectedOwners - then this secret is considered outdated, and
-          should be regenerated/reencrypted.
+          List of hosts currently authorized to decrypt this shared secret.
 
-          Imported from fleet.nix
+          If owners differ from expected owners, the secret is considered outdated
+          and requires regeneration or re-encryption.
         '';
         default = [];
       };
       generationData = mkOption {
         type = unspecified;
-        description = "Data that is embedded into secret part";
+        description = "Contextual metadata associated with secret part.";
         default = null;
       };
     };
@@ -60,22 +59,22 @@
     options = {
       createdAt = mkOption {
         type = str;
-        description = "When this secret was (re)generated";
+        description = "Timestamp of secret generation/last rotation.";
         default = null;
       };
       expiresAt = mkOption {
         type = nullOr str;
-        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+        description = "Expiration timestamp triggering mandatory secret rotation.";
         default = null;
       };
       shared = mkOption {
         type = bool;
-        description = "On which date this secret will expire, someone should regenerate this secret before it expires.";
+        description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";
         default = false;
       };
       generationData = mkOption {
         type = unspecified;
-        description = "Data that is embedded into secret part";
+        description = "Contextual metadata associated with secret part.";
         default = null;
       };
     };
@@ -87,12 +86,12 @@
       sharedSecrets = mkOption {
         type = attrsOf (submodule sharedSecretData);
         default = {};
-        description = "Stored shared secret data.";
+        description = "Shared secret data.";
       };
       hostSecrets = mkOption {
         type = attrsOf (attrsOf (submodule hostSecretData));
         default = {};
-        description = "Host secrets.";
+        description = "Host-specific secrets.";
         internal = true;
       };
     };
modifiedmodules/secrets.nixdiffbeforeafterboth
before · modules/secrets.nix
1{2  lib,3  config,4  ...5}: let6  inherit (lib.options) mkOption literalExpression;7  inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule;8  inherit (lib.strings) concatStringsSep;9  inherit (lib.attrsets) mapAttrs;1011  sharedSecret = {config, ...}: {12    options = {13      expectedOwners = mkOption {14        type = nullOr (listOf str);15        description = ''16          List of hosts to encrypt secret for. null if managed by user (= via owners field from fleet.nix)1718          Secrets would be decrypted and stored to /run/secrets/$\{name} on owners19        '';20        default = null;21      };22      # TODO: Aren't those options may be just desugared to data/expectedData?23      regenerateOnOwnerAdded = mkOption {24        type = bool;25        description = ''26          Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted.2728          You want to have this option set to true, when this secret contains some reference to its owners, i.e x509 SANs.29        '';30      };31      regenerateOnOwnerRemoved = mkOption {32        default = config.regenerateOnOwnerAdded;33        defaultText = literalExpression "regenerateOnOwnerAdded";34        type = bool;35        description = ''36          Should this secret be removed on owner removal, or it may be just reencrypted3738          Most probably its value should be equal to regenerateOnOwnerAdded, override only if you know what are you doing.39          Contrary to regenerateOnOwnerAdded, you may want to set this option to false, when host permissions are revoked40          in some other way than by this secret ownership, I.e by firewall/etc.41        '';42      };43      generator = mkOption {44        type = nullOr unspecified;45        description = "Derivation to evaluate for secret generation";46        default = null;47      };48      expectedGenerationData = mkOption {49        type = unspecified;50        description = "Data that gets embedded into secret part";51        default = null;52      };53    };54  };55in {56  options = {57    sharedSecrets = mkOption {58      type = attrsOf (submodule sharedSecret);59      default = {};60      description = "Shared secrets";61    };62  };63  config = {64    hosts =65      mapAttrs (_: secretMap: {66        nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt" "generationData"]) secretMap;67      })68      config.data.hostSecrets;69    nixpkgs.overlays = [70      (final: prev: {71        mkSecretGenerators = {recipients}: rec {72          # TODO: Merge both generators to one with consistent options syntax?73          # Impure generator is built on local machine, then built closure is copied to remote machine,74          # and then it is ran in inpure context, so that this generator may access HSMs and other things.75          mkImpureSecretGenerator = {76            script,77            # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD78            # (Some secrets-encryption-in-git/managed PKI solution is expected)79            impureOn ? null,80          }:81            (prev.writeShellScript "impureGenerator.sh" ''82              #!/bin/sh83              set -eu8485              export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}";86              export PATH=${final.fleet-generator-helper}/bin:$PATH8788              # TODO: Provide tempdir from outside, to make it securely erasurable as needed?89              tmp=$(mktemp -d)90              cd $tmp91              # cd /var/empty9293              created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")9495              ${script}9697              if ! test -d $out; then98                echo "impure generator script did not produce expected \$out output"99                exit 1100              fi101102              echo -n $created_at > $out/created_at103              echo -n SUCCESS > $out/marker104            '')105            .overrideAttrs (old: {106              passthru = {107                inherit impureOn;108                generatorKind = "impure";109              };110            });111          # Pure generators are disabled for now112          mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};113114          # TODO: Implement consistent naming115          # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...116          # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.117          # mkSecretGenerator = {script}:118          #   (prev.writeShellScript "generator.sh" ''119          #     #!/bin/sh120          #     set -eu121          #     # TODO: make nix daemon build secret, not just the script.122          #     cd /var/empty123          #124          #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")125          #126          #     ${script}127          #     if ! test -d $out; then128          #       echo "impure generator script did not produce expected \$out output"129          #       exit 1130          #     fi131          #132          #     echo -n $created_at > $out/created_at133          #     echo -n SUCCESS > $out/marker134          #   '')135          #   .overrideAttrs (old: {136          #     passthru = {137          #       generatorKind = "pure";138          #     };139          #     # TODO: make nix daemon build secret, not just the script.140          #     # __impure = true;141          #   });142        };143      })144    ];145  };146}
after · modules/secrets.nix
1{2  lib,3  config,4  ...5}: let6  inherit (lib.options) mkOption literalExpression;7  inherit (lib.types) unspecified nullOr listOf str bool attrsOf submodule functionTo package;8  inherit (lib.strings) concatStringsSep;9  inherit (lib.attrsets) mapAttrs;1011  sharedSecret = {config, ...}: {12    options = {13      expectedOwners = mkOption {14        type = nullOr (listOf str);15        description = ''16          Specifies the list of hosts authorized to decrypt and access this shared secret.1718          When null, secret ownership is managed manually via fleet.nix and CLI.19          Decrypted secrets will be stored at /run/secrets/$\{name} on authorized hosts.20        '';21        default = null;22      };23      regenerateOnOwnerAdded = mkOption {24        type = bool;25        description = ''26          Controls whether the secret must be regenerated when new owners are added.2728          Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).29          When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.30        '';31      };32      regenerateOnOwnerRemoved = mkOption {33        default = config.regenerateOnOwnerAdded;34        defaultText = literalExpression "regenerateOnOwnerAdded";35        type = bool;36        description = ''37          Determines secret behavior when owners are removed from the configuration.3839          Typically mirrors regenerateOnOwnerAdded. Override cautiously.40          Set to false if host permissions are revoked through alternative mechanisms like firewall rules.41        '';42      };43      generator = mkOption {44        type = nullOr (functionTo package);45        description = ''46          Function evaluating to nix derivation responsible for (re)generating the secret's content.4748          An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,49          use `mkSecretGenerator` helpers to implement own generators.50        '';51        default = null;52      };53      expectedGenerationData = mkOption {54        type = unspecified;55        description = "Contextual metadata embedded within the secret part value";56        default = null;57      };58    };59  };60in {61  options = {62    sharedSecrets = mkOption {63      type = attrsOf (submodule sharedSecret);64      default = {};65      description = "Collection of secrets shared across multiple hosts with configurable ownership";66    };67  };68  config = {69    hosts =70      mapAttrs (_: secretMap: {71        nixos.secrets = mapAttrs (_: s: removeAttrs s ["createdAt" "expiresAt" "generationData"]) secretMap;72      })73      config.data.hostSecrets;74    nixpkgs.overlays = [75      (final: prev: {76        mkSecretGenerators = {recipients}: rec {77          # TODO: Merge both generators to one with consistent options syntax?78          # Impure generator is built on local machine, then built closure is copied to remote machine,79          # and then it is ran in inpure context, so that this generator may access HSMs and other things.80          mkImpureSecretGenerator = {81            script,82            # If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD83            # (Some secrets-encryption-in-git/managed PKI solution is expected)84            impureOn ? null,85          }:86            (prev.writeShellScript "impureGenerator.sh" ''87              #!/bin/sh88              set -eu8990              export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}";91              export PATH=${final.fleet-generator-helper}/bin:$PATH9293              # TODO: Provide tempdir from outside, to make it securely erasurable as needed?94              tmp=$(mktemp -d)95              cd $tmp96              # cd /var/empty9798              created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")99100              ${script}101102              if ! test -d $out; then103                echo "impure generator script did not produce expected \$out output"104                exit 1105              fi106107              echo -n $created_at > $out/created_at108              echo -n SUCCESS > $out/marker109            '')110            .overrideAttrs (old: {111              passthru = {112                inherit impureOn;113                generatorKind = "impure";114              };115            });116          # Pure generators are disabled for now117          mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};118119          # TODO: Implement consistent naming120          # Pure secret generator is supposed to be run entirely by nix, using `__impure` derivation type...121          # But for now, it is ran the same way as `impureSecretGenerator`, but on the local machine.122          # mkSecretGenerator = {script}:123          #   (prev.writeShellScript "generator.sh" ''124          #     #!/bin/sh125          #     set -eu126          #     # TODO: make nix daemon build secret, not just the script.127          #     cd /var/empty128          #129          #     created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")130          #131          #     ${script}132          #     if ! test -d $out; then133          #       echo "impure generator script did not produce expected \$out output"134          #       exit 1135          #     fi136          #137          #     echo -n $created_at > $out/created_at138          #     echo -n SUCCESS > $out/marker139          #   '')140          #   .overrideAttrs (old: {141          #     passthru = {142          #       generatorKind = "pure";143          #     };144          #     # TODO: make nix daemon build secret, not just the script.145          #     # __impure = true;146          #   });147        };148      })149    ];150  };151}