1{2 lib,3 fleetLib,4 config,5 pkgs,6 ...7}:8let9 inherit (builtins) hashString elemAt length toJSON filter;10 inherit (lib.stringsWithDeps) stringAfter;11 inherit (lib.options) mkOption literalExpression;12 inherit (lib.lists) optional;13 inherit (lib.attrsets) mapAttrs mapAttrsToList;14 inherit (lib.modules) mkIf;15 inherit (lib.types)16 submodule17 str18 attrsOf19 nullOr20 unspecified21 lazyAttrsOf22 uniq23 functionTo24 package25 listOf26 ;27 inherit (fleetLib.strings) decodeRawSecret;2829 sysConfig = config;30 secretPartDataType = submodule {31 options = {32 raw = mkOption {33 type = str;34 internal = true;35 description = "Encoded & Encrypted secret part data, passed from fleet.nix";36 };37 };38 };39 secretDataType = submodule {40 freeformType = lazyAttrsOf secretPartDataType;41 options = {42 shared = mkOption {43 description = "Is this secret owned by this machine, or propagated from shared secrets";44 default = false;45 };46 };47 };48 secretPartType =49 secretName:50 submodule (51 { config, ... }:52 let53 partName = config._module.args.name;54 in55 {56 options = {57 hash = mkOption {58 type = str;59 description = "Hash of secret in encoded format";60 };61 path = mkOption {62 type = str;63 description = "Path to secret part, incorporating data hash (thus it will be updated on secret change)";64 };65 stablePath = mkOption {66 type = str;67 description = "Path to secret part, stable path (users are expected to watch for file changes/re-read secret on demand)";68 };69 data = mkOption {70 type = str;71 description = "Secret public data (only available for plaintext)";72 };73 };74 config =75 let76 raw = sysConfig.data.secrets.${secretName}.${partName}.raw;77 in78 {79 hash = hashString "sha1" raw;80 data = decodeRawSecret raw;81 path = "/run/secrets/${secretName}/${config.hash}-${partName}";82 stablePath = "/run/secrets/${secretName}/${partName}";83 };84 }85 );86 secretType = submodule (87 {88 config,89 loc,90 options,91 ...92 }:93 let94 secretName =95 96 97 let98 saLoc = options._module.specialArgs.loc;99 comp = elemAt saLoc;100 in101 assert102 (length saLoc == 2 ||103 length saLoc == 4 &&104 comp 0 == "secrets" && comp 2 == "_module" && comp 3 == "specialArgs") ||105 throw "Unexpected module structure ${toJSON saLoc}";106 if length saLoc == 2 then "documentation generator stub" else comp 1;107 in108 {109 freeformType = lazyAttrsOf (secretPartType secretName);110 options = {111 generator = mkOption {112 type = uniq (nullOr (functionTo package));113 description = "Derivation to evaluate for secret generation";114 default = null;115 };116 mode = mkOption {117 type = str;118 description = "Secret mode";119 default = "0440";120 };121 owner = mkOption {122 type = str;123 description = "Owner of the secret";124 default = "root";125 };126 group = mkOption {127 type = str;128 description = "Group of the secret";129 default = sysConfig.users.users.${config.owner}.group;130 defaultText = literalExpression "config.users.users.$${owner}.group";131 };132 expectedGenerationData = mkOption {133 type = unspecified;134 description = "Data that gets embedded into secret part";135 default = null;136 };137 expectedPrivateParts = mkOption {138 type = listOf str;139 default = [ ];140 description = "List of parts that are expected to be encrypted";141 };142 expectedPublicParts = mkOption {143 type = listOf str;144 default = [ ];145 description = "List of parts that are expected to be public";146 };147 };148 config = mapAttrs (_: _: { }) (removeAttrs (sysConfig.data.secrets.${secretName} or {}) [ "shared" ]);149 }150 );151 processPart = secretName: partName: part: {152 inherit (part) path stablePath;153 raw = config.data.secrets.${secretName}.${partName}.raw;154 };155 processSecret =156 secretName: secret:157 {158 inherit (secret) group mode owner;159 }160 // (mapAttrs (processPart secretName) (161 removeAttrs secret [162 "shared"163 "generator"164 "mode"165 "group"166 "owner"167 "expectedGenerationData"168 "expectedPrivateParts"169 "expectedPublicParts"170 ]171 ));172 secretsData = (mapAttrs (processSecret) config.secrets);173 secretsFile = pkgs.writeTextFile {174 name = "secrets.json";175 text = toJSON secretsData;176 };177 useSysusers =178 (config.systemd ? sysusers && config.systemd.sysusers.enable)179 || (config ? userborn && config.userborn.enable);180in181{182 options = {183 data.secrets = mkOption {184 type = attrsOf secretDataType;185 default = { };186 description = "Host-local secret data";187 };188 secrets = mkOption {189 type = attrsOf secretType;190 default = { };191 description = "Host-local secrets";192 };193 system.secretsData = mkOption {194 type = unspecified;195 default = {};196 description = "secrets.json contents";197 };198 };199 config = {200 system = {inherit secretsData;};201 environment.systemPackages = [ pkgs.fleet-install-secrets ];202203 warnings = filter (v: v!=null) (mapAttrsToList (204 name: secret:205 if206 secret.expectedPrivateParts == [ ]207 && secret.expectedPublicParts == [ ]208 && !(config.data.secrets.${name} or { shared = false; }).shared209 then210 "Secret ${name} has no expected parts defined, this is deprecated for better visibility"211 else212 null213 ) config.secrets);214215 systemd.services.fleet-install-secrets = mkIf useSysusers {216 wantedBy = [ "sysinit.target" ];217 after = [ "systemd-sysusers.service" ];218 restartTriggers = [219 secretsFile220 ];221 aliases = [222 "sops-install-secrets"223 "agenix-install-secrets"224 ];225226 unitConfig.DefaultDependencies = false;227228 serviceConfig = {229 Type = "oneshot";230 RemainAfterExit = true;231 ExecStart = "${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}";232 };233 };234 system.activationScripts.decryptSecrets = mkIf (!useSysusers) (235 stringAfter236 (237 [238 239 "users"240 "groups"241 "specialfs"242 ]243 244 245 246 ++ (optional (config.system.activationScripts ? "persist-files") "persist-files")247 )248 ''249 1>&2 echo "setting up secrets"250 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}251 ''252 );253 };254}