{ config, depot, lib, pkgs, ... }: let kvRoot = "secrets/locksmith"; activeProvders = lib.filterAttrs (_: cfg: lib.any (secret: secret.nodes != []) (lib.attrValues cfg.secrets)) config.services.locksmith.providers; in { options.services.locksmith = with lib; { providers = mkOption { type = types.attrsOf (types.submodule ({ ... }: { options = { wantedBy = mkOption { type = types.listOf types.str; default = []; }; after = mkOption { type = types.listOf types.str; default = []; }; secrets = mkOption { type = types.attrsOf (types.submodule ({ ... }: { options = { nodes = mkOption { type = types.listOf types.str; default = []; }; command = mkOption { type = types.coercedTo types.package (package: "${package}") types.str; }; checkUpdate = mkOption { type = types.coercedTo types.package (package: "${package}") types.str; default = "true"; }; owner = mkOption { type = types.str; default = "root"; }; group = mkOption { type = types.str; default = "root"; }; mode = mkOption { type = types.str; default = "0400"; }; }; })); default = {}; }; }; })); default = {}; }; }; config.systemd.services = lib.mapAttrs' (providerName: providerConfig: { name = "locksmith-provider-${providerName}"; value = let providerRoot = "${kvRoot}/${providerName}"; in { description = "Locksmith Provider | ${providerName}"; distributed.enable = true; inherit (providerConfig) wantedBy after; serviceConfig = { Type = "oneshot"; PrivateTmp = true; LoadCredential = lib.mkForce []; }; path = [ config.services.consul.package pkgs.age ]; script = let activeSecrets = lib.filterAttrs (_: secret: secret.nodes != []) providerConfig.secrets; activeNodes = lib.unique (lib.flatten (lib.mapAttrsToList (_: secret: secret.nodes) activeSecrets)); secretNames = map (name: "${providerRoot}-${name}/") (lib.attrNames activeSecrets); createSecret = { path, nodes, owner, mode, group, command, checkUpdate }: '' if (${checkUpdate}); then consul kv put ${lib.escapeShellArg path}/mode ${lib.escapeShellArg mode} consul kv put ${lib.escapeShellArg path}/owner ${lib.escapeShellArg owner} consul kv put ${lib.escapeShellArg path}/group ${lib.escapeShellArg group} secret="$(mktemp -ut)" (${command}) > "$secret" ${lib.concatStringsSep "\n" (map (node: '' consul kv put ${lib.escapeShellArg path}/recipient/${node} "$(age < "$secret" --encrypt --armor -r ${lib.escapeShellArg depot.hours.${node}.ssh.id.publicKey})" '') nodes)} else echo Skipping update for ${lib.escapeShellArg path} fi ''; in '' # create/update secrets umask 77 ${lib.pipe activeSecrets [ (lib.mapAttrsToList (secretName: secretConfig: createSecret { path = "${providerRoot}-${secretName}"; inherit (secretConfig) nodes mode owner group command checkUpdate; })) (lib.concatStringsSep "\n") ]} # delete leftover secrets of this provider consul kv get --keys '${providerRoot}-' | grep -v ${lib.concatStringsSep " \\\n " (map (secret: "-e ${lib.escapeShellArg secret}") secretNames)} | xargs --no-run-if-empty -n1 consul kv delete --recurse # notify ${lib.pipe activeNodes [ (map (node: "consul event --name=chant:locksmith --node=${node}")) (lib.concatStringsSep "\n") ]} ''; }; }) activeProvders; }