{ 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; 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"; }; uidMap = mkOption { type = types.attrsOf types.int; default = {}; description = "optional mapping of users to user IDs; required for SYSROOT when user isn't available on host"; }; gidMap = mkOption { type = types.attrsOf types.int; default = {}; description = "optional mapping of groups to group IDs; required for SYSROOT when group isn't available on host"; }; packages = mkOption { type = types.listOf types.package; default = []; description = "packages for script"; }; 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") (lib.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}" ]; }) ]); }