vault: implement bootstrap integration
Some checks failed
Run nix flake check / flake-check (push) Successful in 2m31s
Run nix flake check / flake-check (pull_request) Failing after 14m16s

This commit is contained in:
2026-02-02 22:27:28 +01:00
parent b5364d2ccc
commit 01d4812280
28 changed files with 2305 additions and 84 deletions

223
system/vault-secrets.nix Normal file
View File

@@ -0,0 +1,223 @@
{ 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);
};
}