Files
sops-manager/nix/flakeModule.nix
T
2026-06-06 01:14:08 +04:00

272 lines
8.6 KiB
Nix

{
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 "<userName>@<hostName>".
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
'';
};
};
}