difftreelog
feat explicit managed marker
in: trunk
2 files changed
modules/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"]))
];
}
);
modules/secrets-data.nixdiffbeforeafterboth1{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 ${toJSONsecret.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 ${toJSONsecret.expectedGenerationData} != ${161 toJSON(config.data.sharedSecrets.${name}or{generationData=null;}).generationData162 }. Run fleet secrets regenerate to fix";163 }) config.sharedSecrets);164 };165}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 ${toJSONsecret.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 ${toJSONsecret.expectedGenerationData} != ${167 toJSON(config.data.sharedSecrets.${name}or{generationData=null;}).generationData168 }. Run fleet secrets regenerate to fix";169 }) config.sharedSecrets);170 };171}