{ 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..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..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: { inherit (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 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}" ]; }) ]); }