vault: implement bootstrap integration
This commit is contained in:
@@ -10,5 +10,6 @@
|
||||
./root-ca.nix
|
||||
./sops.nix
|
||||
./sshd.nix
|
||||
./vault-secrets.nix
|
||||
];
|
||||
}
|
||||
|
||||
223
system/vault-secrets.nix
Normal file
223
system/vault-secrets.nix
Normal 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user