diff --git a/lib/init/nomad/lib-systemd.sh b/lib/init/nomad/lib-systemd.sh index 4da0fee..a67e0b3 100644 --- a/lib/init/nomad/lib-systemd.sh +++ b/lib/init/nomad/lib-systemd.sh @@ -46,11 +46,18 @@ systemd_install_unit() { if [ ! -f "$unit_path" ] \ || ! printf '%s\n' "$unit_content" | cmp -s - "$unit_path"; then log "writing unit → ${unit_path}" - local tmp - tmp="$(mktemp)" - printf '%s\n' "$unit_content" > "$tmp" - install -m 0644 -o root -g root "$tmp" "$unit_path" - rm -f "$tmp" + # Subshell-scoped EXIT trap guarantees the temp file is removed on + # both success AND set-e-induced failure of `install`. A function- + # scoped RETURN trap does NOT fire on errexit-abort (bash only runs + # RETURN on normal function exit), so the subshell is the reliable + # cleanup boundary. It's also isolated from the caller's EXIT trap. + ( + local tmp + tmp="$(mktemp)" + trap 'rm -f "$tmp"' EXIT + printf '%s\n' "$unit_content" > "$tmp" + install -m 0644 -o root -g root "$tmp" "$unit_path" + ) needs_reload=1 else log "unit file already up to date" diff --git a/lib/init/nomad/systemd-vault.sh b/lib/init/nomad/systemd-vault.sh index 8a241d4..109eba1 100755 --- a/lib/init/nomad/systemd-vault.sh +++ b/lib/init/nomad/systemd-vault.sh @@ -22,7 +22,8 @@ # # Seal model: # The single unseal key lives at /etc/vault.d/unseal.key (0400 root). -# Seal-key theft == vault theft. Dev-box acceptable; see docs/VAULT.md. +# Seal-key theft == vault theft. Factory-dev-box-acceptable tradeoff — +# we avoid running a second Vault to auto-unseal the first. # # Idempotency contract: # - Unit file NOT rewritten when on-disk content already matches desired. diff --git a/lib/init/nomad/vault-init.sh b/lib/init/nomad/vault-init.sh index e2929a6..6353208 100755 --- a/lib/init/nomad/vault-init.sh +++ b/lib/init/nomad/vault-init.sh @@ -31,7 +31,8 @@ # # Seal model: # Single unseal key persisted on disk at /etc/vault.d/unseal.key. Seal-key -# theft == vault theft. Factory-dev-box-acceptable; see docs/VAULT.md. +# theft == vault theft. Factory-dev-box-acceptable tradeoff — we avoid +# running a second Vault to auto-unseal the first. # # Environment: # VAULT_ADDR — Vault API address (default: http://127.0.0.1:8200). @@ -101,9 +102,21 @@ vault_reachable() { [ "$status" -eq 0 ] || [ "$status" -eq 2 ] } -# vault_initialized — echoes "true" / "false" / "" (empty on parse failure). +# vault_initialized — echoes "true" / "false" / "" (empty on parse failure +# or unreachable vault). Always returns 0 so that `x="$(vault_initialized)"` +# is safe under `set -euo pipefail`. +# +# Key subtlety: `vault status` exits 2 when Vault is sealed OR uninitialized +# — the exact state we need to *observe* on first run. Without the +# `|| true` guard, pipefail + set -e inside a standalone assignment would +# propagate that exit 2 to the outer script and abort before we ever call +# `vault operator init`. We capture `vault status`'s output to a variable +# first (pipefail-safe), then feed it to jq separately. vault_initialized() { - vault status -format=json 2>/dev/null | jq -r '.initialized' 2>/dev/null + local out="" + out="$(vault status -format=json 2>/dev/null || true)" + [ -n "$out" ] || { printf ''; return 0; } + printf '%s' "$out" | jq -r '.initialized' 2>/dev/null || printf '' } # write_secret_file PATH CONTENT