git.delta.rocks / jrsonnet / refs/commits / 75ab1d080b42

difftreelog

feat explicit managed marker

kyyyxoxpYaroslav Bolyukin2025-10-27parent: #4eb6f73.patch.diff
in: trunk

2 files changed

modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -139,7 +139,7 @@
       };
       config.parts = mkMerge [
         (mkIf (config.generator != null && config.generator ? parts) config.generator.parts)
-        (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared"]))
+        (mapAttrs (_: _: {}) (removeAttrs sysConfig.data.secrets.${secretName} ["shared" "managed"]))
       ];
     }
   );
modifiedmodules/secrets-data.nixdiffbeforeafterboth
before · modules/secrets-data.nix
1{2  lib,3  fleetLib,4  config,5  ...6}:7let8  inherit (fleetLib.options) mkDataOption;9  inherit (lib.options) mkOption;10  inherit (lib.types)11    nullOr12    listOf13    str14    attrsOf15    submodule16    bool17    unspecified18    ;19  inherit (lib.attrsets)20    mapAttrsToList21    mapAttrs22    filterAttrs23    genAttrs24    ;25  inherit (lib.lists) sort unique concatLists;26  inherit (lib.strings) toJSON;2728  secretDataValue = {29    options = {30      raw = mkOption {31        type = nullOr str;32        description = "Raw secret data in unspecified encoded and optionally encrypted format.";33        default = null;34      };35    };36  };3738  sharedSecretData = {39    freeformType = attrsOf (submodule secretDataValue);40    options = {41      createdAt = mkOption {42        type = str;43        description = "Timestamp of secret generation/last rotation.";44        default = null;45      };46      expiresAt = mkOption {47        type = nullOr str;48        description = "Expiration timestamp triggering mandatory secret rotation.";49        default = null;50      };5152      owners = mkOption {53        type = listOf str;54        description = ''55          List of hosts currently authorized to decrypt this shared secret.5657          If owners differ from expected owners, the secret is considered outdated58          and requires regeneration or re-encryption.59        '';60        default = [ ];61      };62      generationData = mkOption {63        type = unspecified;64        description = "Contextual metadata associated with secret part.";65        default = null;66      };67    };68    config = { };69  };7071  hostSecretData = {72    freeformType = attrsOf (submodule secretDataValue);73    options = {74      createdAt = mkOption {75        type = str;76        description = "Timestamp of secret generation/last rotation.";77        default = null;78      };79      expiresAt = mkOption {80        type = nullOr str;81        description = "Expiration timestamp triggering mandatory secret rotation.";82        default = null;83      };84      shared = mkOption {85        type = bool;86        description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";87        default = false;88      };89      generationData = mkOption {90        type = unspecified;91        description = "Contextual metadata associated with secret part.";92        default = null;93      };94    };95    config = { };96  };97  managerKey = {98    options = {99      name = mkOption {100        type = str;101        description = "Who does this manager key belongs to.";102      };103      key = mkOption {104        type = str;105        description = "Age-compatible key";106      };107    };108    config = { };109  };110in111{112  options.data = mkDataOption (113    { config, ... }:114    {115      options = {116        managerKeys = mkOption {117          type = listOf (submodule managerKey);118        };119        sharedSecrets = mkOption {120          type = attrsOf (submodule sharedSecretData);121          default = { };122          description = "Shared secret data.";123        };124        hostSecrets = mkOption {125          type = attrsOf (attrsOf (submodule hostSecretData));126          default = { };127          description = "Host-specific secrets.";128          internal = true;129        };130      };131      config.hostSecrets =132        let133          hostsWithSharedSecrets = unique (134            concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets)135          );136          secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;137          toHostSecret = _: secret: (removeAttrs secret [ "owners" ]) // { shared = true; };138        in139        genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));140    }141  );142  config = {143    assertions =144      (mapAttrsToList (name: secret: {145        assertion =146          secret.expectedOwners == null147          ||148            sort (a: b: a < b) (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners149            == sort (a: b: a < b) secret.expectedOwners;150        message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${151          toJSON (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners152        }. Run fleet secrets regenerate to fix";153      }) config.sharedSecrets)154155      ++ (mapAttrsToList (name: secret: {156        # TODO: Same assertion should be in host secrets157        assertion =158          (config.data.sharedSecrets.${name} or { generationData = null; }).generationData159          == secret.expectedGenerationData;160        message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${161          toJSON (config.data.sharedSecrets.${name} or { generationData = null; }).generationData162        }. Run fleet secrets regenerate to fix";163      }) config.sharedSecrets);164  };165}
after · modules/secrets-data.nix
1{2  lib,3  fleetLib,4  config,5  ...6}:7let8  inherit (fleetLib.options) mkDataOption;9  inherit (lib.options) mkOption;10  inherit (lib.types)11    nullOr12    listOf13    str14    attrsOf15    submodule16    bool17    unspecified18    ;19  inherit (lib.attrsets)20    mapAttrsToList21    mapAttrs22    filterAttrs23    genAttrs24    ;25  inherit (lib.lists) sort unique concatLists;26  inherit (lib.strings) toJSON;2728  secretDataValue = {29    options = {30      raw = mkOption {31        type = nullOr str;32        description = "Raw secret data in unspecified encoded and optionally encrypted format.";33        default = null;34      };35    };36  };3738  sharedSecretData = {39    freeformType = attrsOf (submodule secretDataValue);40    options = {41      managed = mkOption {42        type = nullOr bool;43        description = "Is current fleet data value is generated by generator";44        default = null;45      };4647      createdAt = mkOption {48        type = str;49        description = "Timestamp of secret generation/last rotation.";50        default = null;51      };52      expiresAt = mkOption {53        type = nullOr str;54        description = "Expiration timestamp triggering mandatory secret rotation.";55        default = null;56      };5758      owners = mkOption {59        type = listOf str;60        description = ''61          List of hosts currently authorized to decrypt this shared secret.6263          If owners differ from expected owners, the secret is considered outdated64          and requires regeneration or re-encryption.65        '';66        default = [ ];67      };68      generationData = mkOption {69        type = unspecified;70        description = "Contextual metadata associated with secret part.";71        default = null;72      };73    };74    config = { };75  };7677  hostSecretData = {78    freeformType = attrsOf (submodule secretDataValue);79    options = {80      createdAt = mkOption {81        type = str;82        description = "Timestamp of secret generation/last rotation.";83        default = null;84      };85      expiresAt = mkOption {86        type = nullOr str;87        description = "Expiration timestamp triggering mandatory secret rotation.";88        default = null;89      };90      shared = mkOption {91        type = bool;92        description = "Indicates if secret is a shared secret, so other hosts might have the same piece of secret data.";93        default = false;94      };95      generationData = mkOption {96        type = unspecified;97        description = "Contextual metadata associated with secret part.";98        default = null;99      };100    };101    config = { };102  };103  managerKey = {104    options = {105      name = mkOption {106        type = str;107        description = "Who does this manager key belongs to.";108      };109      key = mkOption {110        type = str;111        description = "Age-compatible key";112      };113    };114    config = { };115  };116in117{118  options.data = mkDataOption (119    { config, ... }:120    {121      options = {122        managerKeys = mkOption {123          type = listOf (submodule managerKey);124        };125        sharedSecrets = mkOption {126          type = attrsOf (submodule sharedSecretData);127          default = { };128          description = "Shared secret data.";129        };130        hostSecrets = mkOption {131          type = attrsOf (attrsOf (submodule hostSecretData));132          default = { };133          description = "Host-specific secrets.";134          internal = true;135        };136      };137      config.hostSecrets =138        let139          hostsWithSharedSecrets = unique (140            concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets)141          );142          secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;143          toHostSecret = _: secret: (removeAttrs secret [ "owners" ]) // { shared = true; };144        in145        genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));146    }147  );148  config = {149    assertions =150      (mapAttrsToList (name: secret: {151        assertion =152          secret.expectedOwners == null153          ||154            sort (a: b: a < b) (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners155            == sort (a: b: a < b) secret.expectedOwners;156        message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${157          toJSON (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners158        }. Run fleet secrets regenerate to fix";159      }) config.sharedSecrets)160161      ++ (mapAttrsToList (name: secret: {162        # TODO: Same assertion should be in host secrets163        assertion =164          (config.data.sharedSecrets.${name} or { generationData = null; }).generationData165          == secret.expectedGenerationData;166        message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${167          toJSON (config.data.sharedSecrets.${name} or { generationData = null; }).generationData168        }. Run fleet secrets regenerate to fix";169      }) config.sharedSecrets);170  };171}