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 116 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}