Files
OpenBAO/scripts/renew-app-tokens.sh
Lutz Finsterle fef9e7f1ae Renew the transit seal token so auto-unseal survives a reboot
The main node's transit seal token (config/seal.hcl) lives on the unsealer
and was never renewed. It lapsed, and after the 2026-09-17 reboot main
crash-looped with 403 on transit/encrypt/autounseal even once the unsealer
was unsealed. renew-app-tokens.sh now parses it out of seal.hcl and renews
it via the same docker-exec path as the unsealer backup token.

Also widen .gitignore to config/seal.hcl* so timestamped backups of the
file (which contain the token) cannot be committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 10:10:18 +02:00

80 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Renew every scoped OpenBAO app token so periodic tokens never lapse. Periodic
# tokens live only as long as they're renewed within their period; nothing was
# renewing these, so they expired (2026-08-01). Runs daily via a systemd timer.
#
# Covers TWO sets of tokens:
# 1. $USER_DIR/*.token — user-owned app tokens, renewed against main.
# 2. /etc/openbao-*.token — root-owned infra tokens (backup, cert-renew).
# The /etc set lapsed unnoticed for 24 days (2026-07-29..08-22) because this
# script only walked the user dir, which broke nightly backups and would have
# broken cert renewal — hence it now runs as root to read both.
#
# The unsealer's backup token belongs to the SEPARATE openbao-unsealer instance,
# which publishes no host port, so it is renewed via `docker compose exec`
# rather than curl. Renewing it against main would 403.
#
# The main node's transit SEAL token (config/seal.hcl) also lives on the
# unsealer and was NOT covered until 2026-09-18 — it lapsed, and after the next
# reboot main could not auto-unseal (403 on transit/encrypt/autounseal). It is
# renewed via the same exec path, parsed out of seal.hcl.
#
# Best-effort: a dead token logs a failure but never aborts the rest. No secrets
# are printed.
set -uo pipefail
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
PROJECT_DIR="/home/lutz/Projects/OpenBAO"
USER_DIR="${TOKEN_DIR:-/home/lutz/.config/openbao}" # explicit: $HOME is /root under the timer
UNSEALER_TOKEN="/etc/openbao-unsealer-backup.token"
SEAL_HCL="$PROJECT_DIR/config/seal.hcl"
LOG="$USER_DIR/token-renew.log"
ts="$(date '+%F %T %Z')"
shopt -s nullglob
renewed=0; failed=0
ok() { echo "$ts $1 renewed ttl=${2}s" >> "$LOG"; renewed=$((renewed+1)); }
bad() { echo "$ts $1 RENEW FAILED: $2" >> "$LOG"; failed=$((failed+1)); }
# renew_via_api <token-file>
renew_via_api() {
local tf="$1" name tok resp ttl
name="$(basename "$tf")"
tok="$(cat "$tf" 2>/dev/null)"
[ -n "$tok" ] || { bad "$name" "EMPTY"; return; }
resp="$(curl -sS --max-time 10 -H "X-Vault-Token: $tok" -X POST "$ADDR/v1/auth/token/renew-self" 2>/dev/null)"
ttl="$(printf '%s' "$resp" | jq -r '.auth.lease_duration // empty' 2>/dev/null)"
if [ -n "$ttl" ]; then ok "$name" "$ttl"
else bad "$name" "$(printf '%s' "$resp" | jq -c '.errors // .' 2>/dev/null)"; fi
}
# renew_via_exec <name> <token> — for the unsealer instance (no published port)
renew_via_exec() {
local name="$1" tok="$2" resp ttl
[ -n "$tok" ] || { bad "$name" "EMPTY"; return; }
# Bare `bao token renew` (no TOKEN arg) is the renew-self form; there is no
# -self flag in OpenBao's CLI.
resp="$(cd "$PROJECT_DIR" && docker compose exec -T -e BAO_TOKEN="$tok" openbao-unsealer \
bao token renew -format=json 2>&1)"
ttl="$(printf '%s' "$resp" | jq -r '.auth.lease_duration // empty' 2>/dev/null)"
if [ -n "$ttl" ]; then ok "$name" "$ttl"
else bad "$name" "$(printf '%s' "$resp" | jq -c '.errors // .' 2>/dev/null || printf '%s' "$resp" | tr '\n' ' ')"; fi
}
# 1. user-owned app tokens (*.token only — backups like *.token.bak.* don't match)
for tf in "$USER_DIR"/*.token; do renew_via_api "$tf"; done
# 2. root-owned infra tokens; the unsealer one needs the exec path
for tf in /etc/openbao-*.token; do
[ -r "$tf" ] || { bad "$(basename "$tf")" "not readable (run as root)"; continue; }
if [ "$tf" = "$UNSEALER_TOKEN" ]; then renew_via_exec "$(basename "$tf")" "$(cat "$tf" 2>/dev/null)"; else renew_via_api "$tf"; fi
done
# 3. main's transit seal token (on the unsealer)
renew_via_exec "seal.hcl" "$(sed -nE 's/^[[:space:]]*token[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$SEAL_HCL" 2>/dev/null)"
echo "$ts summary: renewed=$renewed failed=$failed" >> "$LOG"
# Running as root must not leave the log root-owned for the next user-context read.
chown lutz:lutz "$LOG" 2>/dev/null || true
[ "$failed" -eq 0 ]