{ config, lib, pkgs, ... }: with lib; let cfg = config.vault; # Import vault-fetch package vault-fetch = pkgs.callPackage ../scripts/vault-fetch { }; # Secret configuration type secretType = types.submodule ({ name, config, ... }: { options = { secretPath = mkOption { type = types.str; description = '' Path to the secret in Vault (without /v1/secret/data/ prefix). Example: "hosts/monitoring01/grafana-admin" ''; }; outputDir = mkOption { type = types.str; default = "/run/secrets/${name}"; description = '' Directory where secret files will be written. Each key in the secret becomes a separate file. ''; }; cacheDir = mkOption { type = types.str; default = "/var/lib/vault/cache/${name}"; description = '' Directory for caching secrets when Vault is unreachable. ''; }; owner = mkOption { type = types.str; default = "root"; description = "Owner of the secret files"; }; group = mkOption { type = types.str; default = "root"; description = "Group of the secret files"; }; mode = mkOption { type = types.str; default = "0400"; description = "Permissions mode for secret files"; }; restartTrigger = mkOption { type = types.bool; default = false; description = '' Whether to create a systemd timer that periodically restarts services using this secret to rotate credentials. ''; }; restartInterval = mkOption { type = types.str; default = "weekly"; description = '' How often to restart services for secret rotation. Uses systemd.time format (e.g., "daily", "weekly", "monthly"). Only applies if restartTrigger is true. ''; }; services = mkOption { type = types.listOf types.str; default = []; description = '' List of systemd service names that depend on this secret. Used for periodic restart if restartTrigger is enabled. ''; }; }; }); in { options.vault = { enable = mkEnableOption "Vault secrets management" // { default = false; }; secrets = mkOption { type = types.attrsOf secretType; default = {}; description = '' Secrets to fetch from Vault. Each attribute name becomes a secret identifier. ''; example = literalExpression '' { grafana-admin = { secretPath = "hosts/monitoring01/grafana-admin"; owner = "grafana"; group = "grafana"; restartTrigger = true; restartInterval = "daily"; services = [ "grafana" ]; }; } ''; }; criticalServices = mkOption { type = types.listOf types.str; default = [ "bind" "openbao" "step-ca" ]; description = '' Services that should never get auto-restart timers for secret rotation. These are critical infrastructure services where automatic restarts could cause cascading failures. ''; }; vaultAddress = mkOption { type = types.str; default = "https://vault01.home.2rjus.net:8200"; description = "Vault server address"; }; skipTlsVerify = mkOption { type = types.bool; default = true; description = "Skip TLS certificate verification (useful for self-signed certs)"; }; }; config = mkIf (cfg.enable && cfg.secrets != {}) { # Create systemd services for fetching secrets and rotation systemd.services = # Fetch services (mapAttrs' (name: secretCfg: nameValuePair "vault-secret-${name}" { description = "Fetch Vault secret: ${name}"; before = map (svc: "${svc}.service") secretCfg.services; wantedBy = [ "multi-user.target" ]; # Ensure vault-fetch is available path = [ vault-fetch ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; # Fetch the secret ExecStart = pkgs.writeShellScript "fetch-${name}" '' set -euo pipefail # Set Vault environment variables export VAULT_ADDR="${cfg.vaultAddress}" export VAULT_SKIP_VERIFY="${if cfg.skipTlsVerify then "1" else "0"}" # Fetch secret using vault-fetch ${vault-fetch}/bin/vault-fetch \ "${secretCfg.secretPath}" \ "${secretCfg.outputDir}" \ "${secretCfg.cacheDir}" # Set ownership and permissions chown -R ${secretCfg.owner}:${secretCfg.group} "${secretCfg.outputDir}" chmod ${secretCfg.mode} "${secretCfg.outputDir}"/* ''; # Logging StandardOutput = "journal"; StandardError = "journal"; }; }) cfg.secrets) // # Rotation services (mapAttrs' (name: secretCfg: nameValuePair "vault-secret-rotate-${name}" (mkIf (secretCfg.restartTrigger && secretCfg.services != [] && !any (svc: elem svc cfg.criticalServices) secretCfg.services) { description = "Rotate Vault secret and restart services: ${name}"; serviceConfig = { Type = "oneshot"; }; script = '' # Restart the secret fetch service systemctl restart vault-secret-${name}.service # Restart all dependent services ${concatMapStringsSep "\n" (svc: "systemctl restart ${svc}.service") secretCfg.services} ''; }) ) cfg.secrets); # Create systemd timers for periodic secret rotation (if enabled) systemd.timers = mapAttrs' (name: secretCfg: nameValuePair "vault-secret-rotate-${name}" (mkIf (secretCfg.restartTrigger && secretCfg.services != [] && !any (svc: elem svc cfg.criticalServices) secretCfg.services) { description = "Rotate Vault secret and restart services: ${name}"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = secretCfg.restartInterval; Persistent = true; RandomizedDelaySec = "1h"; }; }) ) cfg.secrets; # Ensure runtime and cache directories exist systemd.tmpfiles.rules = [ "d /run/secrets 0755 root root -" ] ++ [ "d /var/lib/vault/cache 0700 root root -" ] ++ flatten (mapAttrsToList (name: secretCfg: [ "d ${secretCfg.outputDir} 0755 root root -" "d ${secretCfg.cacheDir} 0700 root root -" ]) cfg.secrets); }; }