{ config, lib, inputs, den, ... }: let inherit (lib) mkOption types; cfg = config.secrets; in { options.secrets = { masterKeys = mkOption { type = types.listOf types.str; default = [ ]; description = "A list of master keys for encrypting secrets."; }; secretsDir = mkOption { type = types.str; default = null; description = "Path to the directory containing secrets relative to flake root."; }; formatter = mkOption { type = lib.types.functionTo lib.types.unspecified; default = pkgs: pkgs.prettier; description = "The formatter function to use for formatting sops file"; }; nixosModule = { default = mkOption { type = types.functionTo types.attrs; default = host: { }; description = "A function that takes a hostname and returns a NixOS module to apply to that host."; }; network = mkOption { type = types.functionTo types.attrs; default = { config, ... }: { }; description = "A function that takes the NixOS configuration and returns a NixOS module to apply to the host based on its network configuration."; }; }; homeManagerModule = { default = mkOption { type = types.functionTo types.attrs; default = identity: { }; }; }; }; config = let secrets = builtins.fromJSON (builtins.readFile "${inputs.self}/${cfg.secretsDir}/secrets.json"); toKeyList = v: if builtins.isString v then [ v ] else if builtins.isList v then v else throw "Unexpected type ${builtins.typeOf v} for sopsPublic"; # Every host across every system: hostName -> [host pubkeys]. host_keys = lib.mergeAttrsList ( lib.flatten ( map ( perSystem: lib.mapAttrsToList (_: host: { ${host.hostName} = toKeyList (host.sopsPublic or [ ]); }) perSystem ) (builtins.attrValues den.hosts) ) ); # Every user on every host is a home identity "@". home_keys = lib.mergeAttrsList ( lib.flatten ( map ( perSystem: lib.mapAttrsToList ( _: host: lib.mapAttrsToList ( _: user: { "${user.userName}@${host.hostName}" = toKeyList (user.sopsPublic or [ ]); } ) (host.users or { }) ) perSystem ) (builtins.attrValues den.hosts) ) ); # Replaces the old hand-maintained `homeIdentities` option. homeIdentities = builtins.attrNames home_keys; # A secret's `hosts` may target either a host or a home identity. identity_keys = host_keys // home_keys; all_host_keys = lib.flatten (lib.attrValues host_keys); all_home_keys = lib.flatten (lib.attrValues home_keys); secret_map = lib.mapAttrs ( name: value: let sopskeys = lib.unique ( lib.flatten (map (k: identity_keys.${k}) value.hosts) ++ (lib.optionals value.globalHosts all_host_keys) ++ (lib.optionals value.globalHomes all_home_keys) ++ cfg.masterKeys ); # Descriptive flag: is this secret consumed by a home-manager user? # True when it is global to homes or targets any home identity. home = value.globalHomes || lib.any (h: lib.elem h homeIdentities) value.hosts; in { inherit (value) format neededForUsers hosts globalHosts globalHomes ; keys = value.keys or [ ]; inherit sopskeys home; } ) secrets; rules = lib.mapAttrsToList (name: value: { path_regex = "${cfg.secretsDir}/${name}$"; key_groups = [ { age = value.sopskeys; } ]; }) secret_map; sops_secrets_map = host: lib.mkMerge ( lib.concatLists ( lib.mapAttrsToList ( name: value: let hasHost = (lib.elem host value.hosts) || value.globalHosts; isYamlOrJson = value.format == "yaml" || value.format == "json"; in ( ( [ ( if hasHost && !(isYamlOrJson && value.keys != [ ]) then { ${name} = { inherit (value) format neededForUsers; sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; }; } else { } ) ] ++ lib.optionals hasHost ( lib.map (v: { "${name}-${v}" = { inherit (value) format neededForUsers; sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; key = v; }; }) value.keys ) ) ) ) secret_map ) ); # Home-manager analog of sops_secrets_map. Takes a key identity (e.g. # `laptop-home`) and deliberately omits `neededForUsers`, which the # sops-nix home-manager module does not support (it only matters for # decrypting before system users exist, a NixOS-only concern). home_secrets_map = identity: lib.mkMerge ( lib.concatLists ( lib.mapAttrsToList ( name: value: let hasHost = (lib.elem identity value.hosts) || value.globalHomes; isYamlOrJson = value.format == "yaml" || value.format == "json"; in ( [ ( if hasHost && !(isYamlOrJson && value.keys != [ ]) then { ${name} = { inherit (value) format; sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; }; } else { } ) ] ++ lib.optionals hasHost ( lib.map (v: { "${name}-${v}" = { inherit (value) format; sopsFile = inputs.self + "/${cfg.secretsDir}/${name}"; key = v; }; }) value.keys ) ) ) secret_map ) ); in { flake.secretsManifest = { secretsDir = cfg.secretsDir; masterKeys = cfg.masterKeys; hosts = lib.mapAttrs (_: keys: { inherit keys; }) host_keys; inherit homeIdentities; secrets = secret_map; }; secrets.nixosModule = { default = host: { config = { sops.secrets = sops_secrets_map host; }; }; network = { config, ... }: { config.sops.secrets = sops_secrets_map config.networking.hostName; }; }; secrets.homeManagerModule = { default = identity: { config.sops.secrets = home_secrets_map identity; }; }; perSystem = { pkgs, self', ... }: let formatted = let unformatted = (pkgs.formats.yaml { }).generate ".sops.yaml" { creation_rules = rules; }; in pkgs.stdenvNoCC.mkDerivation { name = ".sops-yaml-formatted"; src = unformatted; phases = [ "format" ]; format = '' cp $src .sops.yaml chmod +w .sops.yaml ${lib.getExe (cfg.formatter pkgs)} .sops.yaml cp .sops.yaml $out ''; }; in { packages.write-sops-config = pkgs.writeShellApplication { name = "write-sops-config"; text = '' cp ${formatted} .sops.yaml find ${cfg.secretsDir} -type f ! -name "secrets.json" -exec ${pkgs.lib.getExe pkgs.sops} updatekeys -y {} \; ''; }; checks.check-sops-config = pkgs.runCommand "check-sops-config" { nativeBuildInputs = [ pkgs.diffutils ]; } '' set -e diff -u ${inputs.self}/.sops.yaml ${formatted} touch $out ''; }; }; }