Remove monitoring01 host configuration and unused service modules (prometheus, grafana, loki, tempo, pyroscope). Migrate blackbox, exportarr, and pve exporters to monitoring02 with scrape configs moved to VictoriaMetrics. Update alert rules, terraform vault policies/secrets, http-proxy entries, and documentation to reflect the monitoring02 migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
261 lines
7.9 KiB
Nix
261 lines
7.9 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.vault;
|
|
|
|
# Import vault-fetch package
|
|
vault-fetch = pkgs.callPackage ../scripts/vault-fetch { };
|
|
|
|
# Helper to create fetch scripts using writeShellApplication
|
|
mkFetchScript = name: secretCfg: pkgs.writeShellApplication {
|
|
name = "fetch-${name}";
|
|
runtimeInputs = [ vault-fetch ];
|
|
text = ''
|
|
# Set Vault environment variables
|
|
export VAULT_ADDR="${cfg.vaultAddress}"
|
|
export VAULT_SKIP_VERIFY="${if cfg.skipTlsVerify then "1" else "0"}"
|
|
'' + (if secretCfg.extractKey != null then ''
|
|
# Fetch to temporary directory, then extract single key
|
|
TMPDIR=$(mktemp -d)
|
|
trap 'rm -rf $TMPDIR' EXIT
|
|
|
|
vault-fetch \
|
|
"${secretCfg.secretPath}" \
|
|
"$TMPDIR" \
|
|
"${secretCfg.cacheDir}"
|
|
|
|
# Extract the specified key and write as a single file
|
|
if [ ! -f "$TMPDIR/${secretCfg.extractKey}" ]; then
|
|
echo "ERROR: Key '${secretCfg.extractKey}' not found in secret" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Ensure parent directory exists
|
|
mkdir -p "$(dirname "${secretCfg.outputDir}")"
|
|
cp "$TMPDIR/${secretCfg.extractKey}" "${secretCfg.outputDir}"
|
|
chown ${secretCfg.owner}:${secretCfg.group} "${secretCfg.outputDir}"
|
|
chmod ${secretCfg.mode} "${secretCfg.outputDir}"
|
|
'' else ''
|
|
# Fetch secret as directory of files
|
|
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}"/*
|
|
'');
|
|
};
|
|
|
|
# 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/ha1/mqtt-password"
|
|
'';
|
|
};
|
|
|
|
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.
|
|
'';
|
|
};
|
|
|
|
extractKey = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
description = ''
|
|
Extract a single key from the vault secret JSON and write it as a
|
|
plain file instead of a directory of files. When set, outputDir
|
|
becomes a file path rather than a directory path.
|
|
'';
|
|
};
|
|
|
|
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 ''
|
|
{
|
|
mqtt-password = {
|
|
secretPath = "hosts/ha1/mqtt-password";
|
|
owner = "mosquitto";
|
|
group = "mosquitto";
|
|
services = [ "mosquitto" ];
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
|
|
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 = lib.getExe (mkFetchScript name secretCfg);
|
|
|
|
# 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: [
|
|
# When extractKey is set, outputDir is a file path - create parent dir instead
|
|
(if secretCfg.extractKey != null
|
|
then "d ${dirOf secretCfg.outputDir} 0755 root root -"
|
|
else "d ${secretCfg.outputDir} 0755 root root -")
|
|
"d ${secretCfg.cacheDir} 0700 root root -"
|
|
]) cfg.secrets);
|
|
};
|
|
}
|