Renew root-owned /etc infra tokens too (backups were dead 24 days)
renew-app-tokens.sh only walked ~/.config/openbao/*.token, so the three root-owned tokens were never renewed and lapsed on 2026-07-29: /etc/openbao-backup.token -> nightly raft snapshots failed (403) /etc/openbao-unsealer-backup.token -> same, unsealer instance /etc/openbao-cert-renew.token -> would have failed silently at <21d Nightly backups had been failing for 24 days before this was noticed; the last good snapshot was 2026-07-28. All three tokens have been re-issued as periodic (30d) and the script now covers both sets. The unsealer's token belongs to a separate instance with no published port, so it renews via `docker compose exec` rather than curl -- renewing it against main returns 403. Note `bao token renew` takes no -self flag; the bare form is the renew-self call. Service now runs as root to read /etc, and chowns the log back to lutz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NFtVLA7VVqXL5G2S18c4Jk
This commit is contained in:
@@ -3,27 +3,70 @@
|
||||
# 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.
|
||||
#
|
||||
# Best-effort: reads each ~/.config/openbao/*.token and calls renew-self. A dead
|
||||
# token logs a failure but never aborts the rest. No secrets are printed.
|
||||
# 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.
|
||||
#
|
||||
# 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}"
|
||||
DIR="$HOME/.config/openbao"
|
||||
LOG="$DIR/token-renew.log"
|
||||
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"
|
||||
LOG="$USER_DIR/token-renew.log"
|
||||
ts="$(date '+%F %T %Z')"
|
||||
shopt -s nullglob
|
||||
|
||||
renewed=0; failed=0
|
||||
for tf in "$DIR"/*.token; do # *.token only — backup files (*.token.bak.*) don't match
|
||||
tok="$(cat "$tf" 2>/dev/null)"
|
||||
|
||||
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")"
|
||||
[ -n "$tok" ] || { echo "$ts $name EMPTY" >> "$LOG"; failed=$((failed+1)); continue; }
|
||||
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
|
||||
echo "$ts $name renewed ttl=${ttl}s" >> "$LOG"; renewed=$((renewed+1))
|
||||
else
|
||||
echo "$ts $name RENEW FAILED: $(printf '%s' "$resp" | jq -c '.errors // .' 2>/dev/null)" >> "$LOG"; failed=$((failed+1))
|
||||
fi
|
||||
if [ -n "$ttl" ]; then ok "$name" "$ttl"
|
||||
else bad "$name" "$(printf '%s' "$resp" | jq -c '.errors // .' 2>/dev/null)"; fi
|
||||
}
|
||||
|
||||
# renew_via_exec <token-file> — for the unsealer instance (no published port)
|
||||
renew_via_exec() {
|
||||
local tf="$1" name tok resp ttl
|
||||
name="$(basename "$tf")"
|
||||
tok="$(cat "$tf" 2>/dev/null)"
|
||||
[ -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 "$tf"; else renew_via_api "$tf"; fi
|
||||
done
|
||||
|
||||
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 ]
|
||||
|
||||
@@ -6,7 +6,7 @@ Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=lutz
|
||||
Group=lutz
|
||||
# Runs as root: must read the root-owned /etc/openbao-*.token infra tokens
|
||||
# (backup x2, cert-renew) alongside the user-owned ~lutz/.config/openbao ones.
|
||||
ExecStart=/home/lutz/Projects/OpenBAO/scripts/renew-app-tokens.sh
|
||||
# Renews ~/.config/openbao/*.token via auth/token/renew-self. Best-effort.
|
||||
# Renews both sets via auth/token/renew-self. Best-effort.
|
||||
|
||||
Reference in New Issue
Block a user