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 };97in98{99 options.data = mkDataOption (100 { config, ... }:101 {102 options = {103 sharedSecrets = mkOption {104 type = attrsOf (submodule sharedSecretData);105 default = { };106 description = "Shared secret data.";107 };108 hostSecrets = mkOption {109 type = attrsOf (attrsOf (submodule hostSecretData));110 default = { };111 description = "Host-specific secrets.";112 internal = true;113 };114 };115 config.hostSecrets =116 let117 hostsWithSharedSecrets = unique (118 concatLists (mapAttrsToList (_: s: s.owners) config.sharedSecrets)119 );120 secretsHavingHost = host: filterAttrs (_: secret: lib.elem host secret.owners) config.sharedSecrets;121 toHostSecret = _: secret: (removeAttrs secret [ "owners" ]) // { shared = true; };122 in123 genAttrs hostsWithSharedSecrets (host: mapAttrs toHostSecret (secretsHavingHost host));124 }125 );126 config = {127 assertions =128 (mapAttrsToList (name: secret: {129 assertion =130 secret.expectedOwners == null131 ||132 sort (a: b: a < b) (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners133 == sort (a: b: a < b) secret.expectedOwners;134 message = "Shared secret ${name} is expected to be encrypted for ${toJSON secret.expectedOwners}, but it is encrypted for ${135 toJSON (config.data.sharedSecrets.${name} or { owners = [ ]; }).owners136 }. Run fleet secrets regenerate to fix";137 }) config.sharedSecrets)138 ++ (mapAttrsToList (name: secret: {139 140 assertion =141 (config.data.sharedSecrets.${name} or { generationData = null; }).generationData142 == secret.expectedGenerationData;143 message = "Shared secret ${name} has unexpected generation data ${toJSON secret.expectedGenerationData} != ${144 toJSON (config.data.sharedSecrets.${name} or { generationData = null; }).generationData145 }. Run fleet secrets regenerate to fix";146 }) config.sharedSecrets);147 sharedSecrets = mapAttrs (_: _: { }) config.data.sharedSecrets;148 };149}