nixfiles/modules/nixos/secrets.nix

324 lines
9.6 KiB
Nix

{
lib,
pkgs,
config,
...
}: let
inherit (lib.modules) mkIf mkMerge;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.lists) filter;
inherit (lib) types;
inherit (builtins) isString listToAttrs attrNames;
cfg = config.services.secrets;
secretsLib = import ./secretsLib/lib.nix {
inherit lib pkgs;
};
in {
options = {
services.secrets = {
enable = mkEnableOption "secrets";
debug = mkOption {
type = types.bool;
default = false;
};
createSecretsDir = mkOption {
type = types.bool;
default = true;
};
secretsDirUser = mkOption {
type = types.either types.str types.int;
default = "root";
};
secretsDirGroup = mkOption {
type = types.either types.str types.int;
default = "root";
};
secretsDir = mkOption {
type = types.str;
default = "/secrets";
};
vaultLogin = {
enable = mkOption {
type = types.bool;
default = false;
};
vaultURL = mkOption {
type = types.str;
default = cfg.vaultURL;
};
loginUsername = mkOption {
type = types.str;
};
loginPasswordFile = mkOption {
type = types.nullOr types.path;
default =
if cfg.secrets ? "vault_password"
then cfg.secrets."vault_password".path
else null;
};
};
autoSecrets = {
enable = mkEnableOption "autoSecrets";
affectedSystemdServices = mkOption {
type = types.listOf (types.either (types.str) (types.submodule {
options = {
name = mkOption {
type = types.str;
};
withPartOf = mkOption {
type = types.bool;
default = true;
description = "add service to auto-secrets's PartOf; this may not be wanted for stuff on a timer such as backups";
};
};
default = {};
}));
default = [];
description = "systemd target names to be restarted with and start after auto-secrets has run, without the .service suffix";
};
};
requiredVaultPaths = mkOption {
type = types.listOf (types.oneOf [
types.str
(types.submodule {
options = {
path = mkOption {
type = types.str;
};
capabilities = mkOption {
type = types.listOf types.str;
default = ["read" "list"];
};
};
})
]);
default = [];
description = "default vault paths as API path, not kv2 path";
};
vaultURL = mkOption {
type = types.str;
default = "https://vault.owo.monster";
description = "default Vault URL, can be overrided with env variables";
};
extraFunctions = mkOption {
type = types.lines;
default = "";
description = "extra bash functions to add to top of script for init";
};
extraCheckFunctions = mkOption {
type = types.lines;
default = "";
description = "extra bash functions to add to top of script for check";
};
uidMap = mkOption {
type = types.attrsOf types.int;
default = listToAttrs (map (
secretName: let
secretUserName = cfg.secrets.${secretName}.user;
in {
name = "${secretUserName}";
value = config.users.users.${secretUserName}.uid;
}
) (filter
(secretName: let
secret = cfg.secrets.${secretName};
in
isString secret.user
&& (
if config.users.users ? "${secret.user}"
then true
else builtins.trace "warning: secrets module could not find a uid mapping for user ${secret.user}" false
))
(attrNames cfg.secrets)));
description = ''
optional mapping of users to user IDs
required for SYSROOT when user isn't available on host
defaults to getting values from users.users.<name>.uid for all secrets with user set as string
'';
};
gidMap = mkOption {
type = types.attrsOf types.int;
default = listToAttrs (map (
secretName: let
secretGroupName = cfg.secrets.${secretName}.group;
in {
name = "${secretGroupName}";
value = config.users.groups.${secretGroupName}.gid;
}
) (filter
(secretName: let
secret = cfg.secrets.${secretName};
in
isString secret.group
&& (
if config.users.groups ? "${secret.group}"
then true
else builtins.trace "warning: secrets module could not find a gid mapping for group ${secret.group}" false
))
(attrNames cfg.secrets)));
description = ''
optional mapping of groups to group IDs
required for SYSROOT when group isn't available on host
defaults to getting values from users.groups.<group>.gid for all secrets with group set as string
'';
};
packages = mkOption {
type = types.listOf types.package;
default = [];
description = "packages for init script";
};
checkPackages = mkOption {
type = types.listOf types.package;
default = [];
description = "packages for checkscript";
};
secrets = mkOption {
type = types.attrsOf (types.submodule ({name, ...}: {
options = {
user = mkOption {
type = types.either types.str types.int;
default = "root";
};
group = mkOption {
type = types.either types.str types.int;
default = "root";
};
permissions = mkOption {
type = types.str;
default = "660";
};
path = mkOption {
type = types.str;
default = "${cfg.secretsDir}/${name}";
};
fetchScript = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
script used to fetch secrets, $secretFile is secret.path
'';
};
checkScript = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
script used to check contents of secret file, set LOCAL_FAIL to true on failure
'';
};
manual = mkOption {
type = types.bool;
default = false;
description = "should the secret be manually deployed";
};
};
}));
};
};
};
config =
{
assertions = [
{
assertion = !(cfg.autoSecrets.enable && !cfg.vaultLogin.enable);
message = "vaultLogin needs to be enabled and configured for autoSecrets to work";
}
{
assertion = !(cfg.vaultLogin.enable && !cfg.vaultLogin.loginPasswordFile == null);
message = "loginPasswordFile needs to be set or a secret called vault_password needs to be configured for vaultLogin to work";
}
];
}
// (mkMerge [
(mkIf (cfg.enable) {
environment.systemPackages = [
(secretsLib.mkSecretsInitScript cfg)
(secretsLib.mkSecretsCheckScript cfg)
];
})
(mkIf (cfg.enable && cfg.vaultLogin.enable) {
environment.systemPackages = [
(secretsLib.mkVaultLoginScript cfg)
];
})
(mkIf (cfg.enable && cfg.autoSecrets.enable) {
systemd = let
# normalise this to make next part easier
affectedSystemdServices =
map (unit: let
name =
if isString unit
then unit
else unit.name;
withPartOf =
if isString unit
then true
else unit.withPartOf;
in {
inherit name withPartOf;
})
cfg.autoSecrets.affectedSystemdServices;
in {
services =
(listToAttrs (map (unitConfig: {
name = unitConfig.name;
value = {
after = ["auto-secrets.service"];
wants = ["auto-secrets.service"];
partOf = mkIf unitConfig.withPartOf ["auto-secrets.service"];
};
})
affectedSystemdServices))
// {
auto-secrets.partOf =
map (unitConfig: unitConfig.name + ".service")
(filter
(unitConfig: unitConfig.withPartOf)
affectedSystemdServices);
};
};
})
(mkIf (cfg.enable && cfg.autoSecrets.enable) {
systemd.services.auto-secrets = {
wantedBy = ["multi-user.target"];
after = ["network.target"];
path = with pkgs; [bash vault-bin getent];
script = ''
${secretsLib.mkVaultLoginScript cfg}/bin/vault-login
${secretsLib.mkSecretsInitScript cfg}/bin/secrets-init
'';
};
})
(mkIf (cfg.enable && cfg.createSecretsDir) {
systemd.tmpfiles.rules = [
"d ${cfg.secretsDir} - ${toString cfg.secretsDirUser} ${toString cfg.secretsDirGroup}"
];
})
]);
}