From bbb22e588e89ef764cec531531e6c4c19f7e3d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Fri, 6 Feb 2026 00:17:24 +0100 Subject: [PATCH] system: replace writeShellScript with writeShellApplication Convert remaining writeShellScript usages to writeShellApplication for shellcheck validation and strict bash options. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- hosts/vaulttest01/configuration.nix | 48 +++++++++-------- services/monitoring/prometheus.nix | 62 +++++++++++----------- system/vault-secrets.nix | 81 +++++++++++++++-------------- 4 files changed, 103 insertions(+), 90 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8396ca0..7eb92c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -402,7 +402,7 @@ This means: **Firewall**: Disabled on most hosts (trusted network). Enable selectively in host configuration if needed. -**Shell scripts**: Use `pkgs.writeShellApplication` instead of `pkgs.writeShellScriptBin` for creating shell scripts. `writeShellApplication` provides automatic shellcheck validation, sets strict bash options (`set -euo pipefail`), and allows declaring `runtimeInputs` for dependencies. +**Shell scripts**: Use `pkgs.writeShellApplication` instead of `pkgs.writeShellScript` or `pkgs.writeShellScriptBin` for creating shell scripts. `writeShellApplication` provides automatic shellcheck validation, sets strict bash options (`set -euo pipefail`), and allows declaring `runtimeInputs` for dependencies. ### Monitoring Stack diff --git a/hosts/vaulttest01/configuration.nix b/hosts/vaulttest01/configuration.nix index 96baf1e..b315e09 100644 --- a/hosts/vaulttest01/configuration.nix +++ b/hosts/vaulttest01/configuration.nix @@ -5,6 +5,32 @@ ... }: +let + vault-test-script = pkgs.writeShellApplication { + name = "vault-test"; + text = '' + echo "=== Vault Secret Test ===" + echo "Secret path: hosts/vaulttest01/test-service" + + if [ -f /run/secrets/test-service/password ]; then + echo "✓ Password file exists" + echo "Password length: $(wc -c < /run/secrets/test-service/password)" + else + echo "✗ Password file missing!" + exit 1 + fi + + if [ -d /var/lib/vault/cache/test-service ]; then + echo "✓ Cache directory exists" + else + echo "✗ Cache directory missing!" + exit 1 + fi + + echo "Test successful!" + ''; + }; +in { imports = [ ../template2/hardware-configuration.nix @@ -79,27 +105,7 @@ Type = "oneshot"; RemainAfterExit = true; - ExecStart = pkgs.writeShellScript "vault-test" '' - echo "=== Vault Secret Test ===" - echo "Secret path: hosts/vaulttest01/test-service" - - if [ -f /run/secrets/test-service/password ]; then - echo "✓ Password file exists" - echo "Password length: $(wc -c < /run/secrets/test-service/password)" - else - echo "✗ Password file missing!" - exit 1 - fi - - if [ -d /var/lib/vault/cache/test-service ]; then - echo "✓ Cache directory exists" - else - echo "✗ Cache directory missing!" - exit 1 - fi - - echo "Test successful!" - ''; + ExecStart = lib.getExe vault-test-script; StandardOutput = "journal+console"; }; diff --git a/services/monitoring/prometheus.nix b/services/monitoring/prometheus.nix index e0fe2c2..b95033c 100644 --- a/services/monitoring/prometheus.nix +++ b/services/monitoring/prometheus.nix @@ -7,42 +7,44 @@ let autoScrapeConfigs = monLib.generateScrapeConfigs self externalTargets; # Script to fetch AppRole token for Prometheus to use when scraping OpenBao metrics - fetchOpenbaoToken = pkgs.writeShellScript "fetch-openbao-token" '' - set -euo pipefail + fetchOpenbaoToken = pkgs.writeShellApplication { + name = "fetch-openbao-token"; + runtimeInputs = [ pkgs.curl pkgs.jq ]; + text = '' + VAULT_ADDR="https://vault01.home.2rjus.net:8200" + APPROLE_DIR="/var/lib/vault/approle" + OUTPUT_FILE="/run/secrets/prometheus/openbao-token" - VAULT_ADDR="https://vault01.home.2rjus.net:8200" - APPROLE_DIR="/var/lib/vault/approle" - OUTPUT_FILE="/run/secrets/prometheus/openbao-token" + # Read AppRole credentials + if [ ! -f "$APPROLE_DIR/role-id" ] || [ ! -f "$APPROLE_DIR/secret-id" ]; then + echo "AppRole credentials not found at $APPROLE_DIR" >&2 + exit 1 + fi - # Read AppRole credentials - if [ ! -f "$APPROLE_DIR/role-id" ] || [ ! -f "$APPROLE_DIR/secret-id" ]; then - echo "AppRole credentials not found at $APPROLE_DIR" >&2 - exit 1 - fi + ROLE_ID=$(cat "$APPROLE_DIR/role-id") + SECRET_ID=$(cat "$APPROLE_DIR/secret-id") - ROLE_ID=$(cat "$APPROLE_DIR/role-id") - SECRET_ID=$(cat "$APPROLE_DIR/secret-id") + # Authenticate to Vault + AUTH_RESPONSE=$(curl -sf -k -X POST \ + -d "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \ + "$VAULT_ADDR/v1/auth/approle/login") - # Authenticate to Vault - AUTH_RESPONSE=$(${pkgs.curl}/bin/curl -sf -k -X POST \ - -d "{\"role_id\":\"$ROLE_ID\",\"secret_id\":\"$SECRET_ID\"}" \ - "$VAULT_ADDR/v1/auth/approle/login") + # Extract token + VAULT_TOKEN=$(echo "$AUTH_RESPONSE" | jq -r '.auth.client_token') + if [ -z "$VAULT_TOKEN" ] || [ "$VAULT_TOKEN" = "null" ]; then + echo "Failed to extract Vault token from response" >&2 + exit 1 + fi - # Extract token - VAULT_TOKEN=$(echo "$AUTH_RESPONSE" | ${pkgs.jq}/bin/jq -r '.auth.client_token') - if [ -z "$VAULT_TOKEN" ] || [ "$VAULT_TOKEN" = "null" ]; then - echo "Failed to extract Vault token from response" >&2 - exit 1 - fi + # Write token to file + mkdir -p "$(dirname "$OUTPUT_FILE")" + echo -n "$VAULT_TOKEN" > "$OUTPUT_FILE" + chown prometheus:prometheus "$OUTPUT_FILE" + chmod 0400 "$OUTPUT_FILE" - # Write token to file - mkdir -p "$(dirname "$OUTPUT_FILE")" - echo -n "$VAULT_TOKEN" > "$OUTPUT_FILE" - chown prometheus:prometheus "$OUTPUT_FILE" - chmod 0400 "$OUTPUT_FILE" - - echo "Successfully fetched OpenBao token" - ''; + echo "Successfully fetched OpenBao token" + ''; + }; in { # Systemd service to fetch AppRole token for Prometheus OpenBao scraping diff --git a/system/vault-secrets.nix b/system/vault-secrets.nix index 4b8c3c2..60466dd 100644 --- a/system/vault-secrets.nix +++ b/system/vault-secrets.nix @@ -8,6 +8,48 @@ let # 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 = { @@ -162,44 +204,7 @@ in 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"}" - '' + (if secretCfg.extractKey != null then '' - # Fetch to temporary directory, then extract single key - TMPDIR=$(mktemp -d) - trap "rm -rf $TMPDIR" EXIT - - ${vault-fetch}/bin/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}/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}"/* - '')); + ExecStart = lib.getExe (mkFetchScript name secretCfg); # Logging StandardOutput = "journal";