Compare commits
4 Commits
f2919cf50a
...
471692c283
| Author | SHA1 | Date | |
|---|---|---|---|
| 471692c283 | |||
| ee0b8c818f | |||
| c5ff1a3fca | |||
| 123e65ee46 |
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
openbao:
|
||||
image: openbao/openbao:2.5.5
|
||||
image: openbao/openbao:2.6.2
|
||||
container_name: openbao
|
||||
restart: unless-stopped
|
||||
# The image entrypoint already runs `bao server -config=/openbao/config`
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
# Minimal sidecar instance that provides transit auto-unseal for the main
|
||||
# node above. Shamir-sealed itself; only reachable on the internal network.
|
||||
openbao-unsealer:
|
||||
image: openbao/openbao:2.5.5
|
||||
image: openbao/openbao:2.6.2
|
||||
container_name: openbao-unsealer
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
|
||||
31
policies/approle-rotate.hcl
Normal file
31
policies/approle-rotate.hcl
Normal file
@@ -0,0 +1,31 @@
|
||||
# approle-rotate — lets the rotation timer mint and prune SecretIDs for the
|
||||
# demo-app AppRole, and nothing else. Consumed via a periodic token at
|
||||
# /etc/openbao-approle-rotate.token by scripts/rotate-approle-secret-id.sh.
|
||||
#
|
||||
# Deliberately scoped to the single role: no access to the RoleID, no ability
|
||||
# to read any secret, and no reach into other AppRoles. A leak of this token
|
||||
# lets an attacker mint credentials for demo-app only -- which is why the role
|
||||
# itself stays limited to secret/demo-app/*.
|
||||
|
||||
# Read the role config -- only for secret_id_num_uses, so the rotator knows
|
||||
# whether a verification login would burn a limited-use SecretID. This exposes
|
||||
# role settings (TTLs, bound policies) but NOT the RoleID, which lives at the
|
||||
# separate .../role-id path and stays denied.
|
||||
path "auth/approle/role/demo-app" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# Generate a new SecretID, and list existing accessors for pruning.
|
||||
path "auth/approle/role/demo-app/secret-id" {
|
||||
capabilities = ["update", "list"]
|
||||
}
|
||||
|
||||
# Inspect an accessor (creation_time drives which ones are safe to prune).
|
||||
path "auth/approle/role/demo-app/secret-id-accessor/lookup" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
|
||||
# Destroy a superseded SecretID by accessor.
|
||||
path "auth/approle/role/demo-app/secret-id-accessor/destroy" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
@@ -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 ]
|
||||
|
||||
114
scripts/rotate-approle-secret-id.sh
Executable file
114
scripts/rotate-approle-secret-id.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rotate the demo-app AppRole SecretID before its 30d TTL expires.
|
||||
#
|
||||
# WHY THIS EXISTS: unlike tokens, a SecretID cannot be renewed -- it must be
|
||||
# re-issued by a privileged caller. So renew-app-tokens.sh structurally cannot
|
||||
# cover it, and the original SecretID silently expired on ~2026-07-28, leaving
|
||||
# scripts/app-get-secret.sh broken until it was noticed on 2026-08-22.
|
||||
#
|
||||
# ORDER OF OPERATIONS IS THE SAFETY PROPERTY. We mint, then PROVE the new
|
||||
# SecretID actually authenticates, and only then overwrite the file and prune
|
||||
# old accessors. A failure at any step leaves the previous working credential
|
||||
# untouched and exits non-zero, so OnFailure= mails the alert.
|
||||
#
|
||||
# Old accessors are kept one cycle deep (KEEP=2: current + previous) so a
|
||||
# consumer that read the file microseconds before rotation can still log in.
|
||||
#
|
||||
# Installed as openbao-approle-rotate.timer (weekly -- 4x margin on a 30d TTL,
|
||||
# so three consecutive failures can occur before anything actually breaks).
|
||||
set -uo pipefail
|
||||
|
||||
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
ROLE="${ROLE:-demo-app}"
|
||||
TOKEN_FILE="/etc/openbao-approle-rotate.token"
|
||||
OUT="${OUT:-/home/lutz/.config/openbao/approle/secret_id}"
|
||||
OWNER="lutz:lutz"
|
||||
KEEP="${KEEP:-2}" # accessors to retain: current + previous
|
||||
|
||||
log() { printf '%s [approle-rotate] %s\n' "$(date '+%F %T')" "$*"; }
|
||||
die() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
[ -r "$TOKEN_FILE" ] || die "token file $TOKEN_FILE not readable (run as root?)"
|
||||
TOK="$(cat "$TOKEN_FILE")"
|
||||
[ -n "$TOK" ] || die "token file $TOKEN_FILE is empty"
|
||||
command -v jq >/dev/null || die "jq not found"
|
||||
|
||||
api() { # <method> <path> [data]
|
||||
local m="$1" p="$2" d="${3:-}"
|
||||
if [ -n "$d" ]; then
|
||||
curl -sS --max-time 15 -H "X-Vault-Token: $TOK" -X "$m" -d "$d" "$ADDR/v1/$p"
|
||||
else
|
||||
curl -sS --max-time 15 -H "X-Vault-Token: $TOK" -X "$m" "$ADDR/v1/$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 0. sanity: a test login must not burn a limited-use SecretID -----------
|
||||
NUM_USES="$(api GET "auth/approle/role/$ROLE" | jq -r '.data.secret_id_num_uses // empty')"
|
||||
[ -n "$NUM_USES" ] || die "cannot read role $ROLE (token scoped wrong, or role gone)"
|
||||
VERIFY=1
|
||||
if [ "$NUM_USES" != "0" ] && [ "$NUM_USES" -le 1 ] 2>/dev/null; then
|
||||
VERIFY=0
|
||||
log "NOTE secret_id_num_uses=$NUM_USES -- skipping verification login so it is not consumed"
|
||||
fi
|
||||
|
||||
# --- 1. mint ---------------------------------------------------------------
|
||||
RESP="$(api POST "auth/approle/role/$ROLE/secret-id" \
|
||||
"{\"metadata\":\"{\\\"rotated_at\\\":\\\"$(date -u '+%FT%TZ')\\\",\\\"by\\\":\\\"rotate-approle-secret-id.sh\\\"}\"}")"
|
||||
NEW_SID="$(printf '%s' "$RESP" | jq -r '.data.secret_id // empty')"
|
||||
NEW_ACC="$(printf '%s' "$RESP" | jq -r '.data.secret_id_accessor // empty')"
|
||||
[ -n "$NEW_SID" ] && [ -n "$NEW_ACC" ] \
|
||||
|| die "mint failed: $(printf '%s' "$RESP" | jq -c '.errors // .')"
|
||||
log "minted new SecretID (accessor ${NEW_ACC:0:8}...)"
|
||||
|
||||
# --- 2. PROVE it works before touching anything ----------------------------
|
||||
if [ "$VERIFY" = "1" ]; then
|
||||
RID="$(cat "$(dirname "$OUT")/role_id" 2>/dev/null)"
|
||||
[ -n "$RID" ] || die "role_id file missing next to $OUT; refusing to rotate blind"
|
||||
LOGIN="$(curl -sS --max-time 15 --data "$(RID="$RID" SID="$NEW_SID" python3 -c \
|
||||
'import json,os;print(json.dumps({"role_id":os.environ["RID"],"secret_id":os.environ["SID"]}))')" \
|
||||
"$ADDR/v1/auth/approle/login" 2>/dev/null)"
|
||||
TESTTOK="$(printf '%s' "$LOGIN" | jq -r '.auth.client_token // empty')"
|
||||
if [ -z "$TESTTOK" ]; then
|
||||
api POST "auth/approle/role/$ROLE/secret-id-accessor/destroy" \
|
||||
"{\"secret_id_accessor\":\"$NEW_ACC\"}" >/dev/null 2>&1
|
||||
die "new SecretID failed verification login; destroyed it, left existing credential in place: $(printf '%s' "$LOGIN" | jq -c '.errors // .')"
|
||||
fi
|
||||
# don't leave a live token lying around just because we tested
|
||||
curl -sS --max-time 10 -H "X-Vault-Token: $TESTTOK" -X POST \
|
||||
"$ADDR/v1/auth/token/revoke-self" >/dev/null 2>&1 || true
|
||||
log "verification login OK (test token revoked)"
|
||||
fi
|
||||
|
||||
# --- 3. install atomically -------------------------------------------------
|
||||
TMP="${OUT}.tmp.$$"
|
||||
( umask 077; printf '%s' "$NEW_SID" > "$TMP" ) || die "cannot write $TMP"
|
||||
chmod 0600 "$TMP" || die "chmod failed on $TMP"
|
||||
chown "$OWNER" "$TMP" 2>/dev/null || log "WARN could not chown $TMP to $OWNER"
|
||||
mv -f "$TMP" "$OUT" || die "atomic replace of $OUT failed"
|
||||
log "installed new SecretID at $OUT"
|
||||
|
||||
# --- 4. prune superseded accessors (keep current + previous) ---------------
|
||||
mapfile -t ACCS < <(api LIST "auth/approle/role/$ROLE/secret-id" | jq -r '.data.keys[]? // empty')
|
||||
if [ "${#ACCS[@]}" -gt "$KEEP" ]; then
|
||||
# order by creation_time, newest first
|
||||
ORDERED="$(for a in "${ACCS[@]}"; do
|
||||
ct="$(api POST "auth/approle/role/$ROLE/secret-id-accessor/lookup" \
|
||||
"{\"secret_id_accessor\":\"$a\"}" | jq -r '.data.creation_time // empty')"
|
||||
[ -n "$ct" ] && printf '%s\t%s\n' "$ct" "$a"
|
||||
done | sort -r)"
|
||||
n=0
|
||||
while IFS=$'\t' read -r ct a; do
|
||||
[ -n "$a" ] || continue
|
||||
n=$((n+1))
|
||||
[ "$n" -le "$KEEP" ] && continue
|
||||
if api POST "auth/approle/role/$ROLE/secret-id-accessor/destroy" \
|
||||
"{\"secret_id_accessor\":\"$a\"}" >/dev/null 2>&1; then
|
||||
log "pruned superseded accessor ${a:0:8}... (created $ct)"
|
||||
else
|
||||
log "WARN could not prune accessor ${a:0:8}..."
|
||||
fi
|
||||
done <<< "$ORDERED"
|
||||
fi
|
||||
|
||||
REMAIN="$(api LIST "auth/approle/role/$ROLE/secret-id" | jq -r '.data.keys | length // 0')"
|
||||
log "done; $REMAIN SecretID accessor(s) live for role $ROLE"
|
||||
114
scripts/send-failure-alert.sh
Executable file
114
scripts/send-failure-alert.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# OnFailure handler for the OpenBAO systemd timers. Invoked as:
|
||||
# send-failure-alert.sh <failed-unit-name>
|
||||
# by openbao-alert@.service, which the timer units reference via OnFailure=.
|
||||
#
|
||||
# Backups died silently for 24 days (2026-07-29..08-22) because a failed
|
||||
# oneshot unit makes no noise. This makes it make noise.
|
||||
#
|
||||
# CIRCULAR-DEPENDENCY NOTE: the SMTP password lives in OpenBAO, but the most
|
||||
# likely reason one of these units failed is that OpenBAO itself is down or
|
||||
# sealed -- in which case fetching the password would fail too, and the alert
|
||||
# would be lost exactly when it matters most. So:
|
||||
# 1. every alert is ALWAYS appended to $ALERT_LOG first, before any network
|
||||
# call, so a durable record exists even with no OpenBAO and no internet;
|
||||
# 2. creds are fetched from OpenBAO when it is reachable, and cached to a
|
||||
# root-only 0600 file that is used as the fallback when it is not.
|
||||
# The cache is a deliberate trade-off, consistent with the house rule that
|
||||
# secrets live in root-owned 0600 files or come from the store at runtime.
|
||||
set -uo pipefail
|
||||
|
||||
UNIT="${1:-unknown.unit}"
|
||||
HOST="$(hostname -s 2>/dev/null || echo pi)"
|
||||
TS="$(date '+%F %T %Z')"
|
||||
|
||||
BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
|
||||
TOKEN_FILE="/etc/openbao-alert-smtp.token"
|
||||
CACHE_FILE="/etc/openbao-smtp-cache"
|
||||
ALERT_LOG="/var/log/openbao-alerts.log"
|
||||
|
||||
SMTP_HOST="securesmtp.t-online.de"; SMTP_PORT="587"
|
||||
MAIL_FROM="lutz.finsterle@t-online.de"; MAIL_TO="lutz.finsterle@t-online.de"
|
||||
|
||||
# --- 1. gather context ------------------------------------------------------
|
||||
STATUS="$(systemctl status --no-pager --full "$UNIT" 2>&1 | head -40)"
|
||||
JOURNAL="$(journalctl -u "$UNIT" -n 40 --no-pager 2>&1 | tail -40)"
|
||||
RESULT="$(systemctl show -p Result --value "$UNIT" 2>/dev/null)"
|
||||
EXECMAIN="$(systemctl show -p ExecMainStatus --value "$UNIT" 2>/dev/null)"
|
||||
|
||||
BODY="OpenBAO maintenance unit FAILED on ${HOST}.
|
||||
|
||||
Unit: ${UNIT}
|
||||
When: ${TS}
|
||||
Result: ${RESULT:-unknown} (exit status ${EXECMAIN:-?})
|
||||
|
||||
This unit is part of the OpenBAO safety net (raft snapshots, TLS cert renewal,
|
||||
scoped-token renewal). A failure here is silent by default -- if you are reading
|
||||
this, the alerting is doing its job. Investigate promptly: a lapsed token or a
|
||||
missed snapshot degrades quietly and is easy to miss for weeks.
|
||||
|
||||
--- systemctl status ---
|
||||
${STATUS}
|
||||
|
||||
--- last 40 journal lines ---
|
||||
${JOURNAL}
|
||||
"
|
||||
|
||||
# --- 2. durable local record FIRST (never depends on OpenBAO or the network) --
|
||||
{
|
||||
echo "===== ${TS} ${UNIT} ====="
|
||||
printf '%s\n\n' "$BODY"
|
||||
} >> "$ALERT_LOG" 2>/dev/null
|
||||
chmod 0600 "$ALERT_LOG" 2>/dev/null || true
|
||||
|
||||
# --- 3. resolve SMTP creds: OpenBAO first, cached copy as fallback -----------
|
||||
user=""; pass=""; cred_src=""
|
||||
|
||||
if [ -r "$TOKEN_FILE" ]; then
|
||||
tok="$(cat "$TOKEN_FILE" 2>/dev/null)"
|
||||
if [ -n "$tok" ]; then
|
||||
resp="$(curl -sS --max-time 10 -H "X-Vault-Token: $tok" \
|
||||
"$BAO_ADDR/v1/secret/data/smtp/healthcheck" 2>/dev/null)"
|
||||
user="$(printf '%s' "$resp" | jq -r '.data.data.username // empty' 2>/dev/null)"
|
||||
pass="$(printf '%s' "$resp" | jq -r '.data.data.password // empty' 2>/dev/null)"
|
||||
if [ -n "$user" ] && [ -n "$pass" ]; then
|
||||
cred_src="openbao"
|
||||
# refresh the offline fallback copy
|
||||
umask 077
|
||||
printf '%s\n%s\n' "$user" "$pass" > "${CACHE_FILE}.tmp" 2>/dev/null \
|
||||
&& chmod 0600 "${CACHE_FILE}.tmp" 2>/dev/null \
|
||||
&& mv -f "${CACHE_FILE}.tmp" "$CACHE_FILE" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if { [ -z "$user" ] || [ -z "$pass" ]; } && [ -r "$CACHE_FILE" ]; then
|
||||
user="$(sed -n 1p "$CACHE_FILE" 2>/dev/null)"
|
||||
pass="$(sed -n 2p "$CACHE_FILE" 2>/dev/null)"
|
||||
cred_src="cache (OpenBAO unreachable -- it may itself be the problem)"
|
||||
fi
|
||||
|
||||
if [ -z "$user" ] || [ -z "$pass" ]; then
|
||||
echo "${TS} ${UNIT}: ALERT EMAIL NOT SENT -- no SMTP creds from OpenBAO or cache" >> "$ALERT_LOG"
|
||||
logger -t openbao-alert "FAILED unit ${UNIT}; could not send email (no SMTP creds)"
|
||||
exit 0 # never fail the handler: that would just add noise, not signal
|
||||
fi
|
||||
|
||||
# --- 4. send ----------------------------------------------------------------
|
||||
msg="$(printf 'From: %s\r\nTo: %s\r\nSubject: [ALERT] %s: %s failed\r\nDate: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s\r\n' \
|
||||
"$MAIL_FROM" "$MAIL_TO" "$HOST" "$UNIT" "$(date -R)" "$BODY (creds via ${cred_src})")"
|
||||
|
||||
if printf '%s' "$msg" | curl -sS --max-time 30 --ssl-reqd \
|
||||
--url "smtp://$SMTP_HOST:$SMTP_PORT" --user "$user:$pass" \
|
||||
--mail-from "$MAIL_FROM" --mail-rcpt "$MAIL_TO" --upload-file - 2>>"$ALERT_LOG"; then
|
||||
echo "${TS} ${UNIT}: alert email sent (creds via ${cred_src})" >> "$ALERT_LOG"
|
||||
logger -t openbao-alert "FAILED unit ${UNIT}; alert email sent"
|
||||
else
|
||||
echo "${TS} ${UNIT}: ALERT EMAIL FAILED TO SEND (creds via ${cred_src})" >> "$ALERT_LOG"
|
||||
logger -t openbao-alert "FAILED unit ${UNIT}; alert email could NOT be sent"
|
||||
fi
|
||||
|
||||
# Keep the log bounded.
|
||||
tail -n 2000 "$ALERT_LOG" > "${ALERT_LOG}.tmp" 2>/dev/null && mv -f "${ALERT_LOG}.tmp" "$ALERT_LOG" 2>/dev/null
|
||||
chmod 0600 "$ALERT_LOG" 2>/dev/null || true
|
||||
exit 0
|
||||
13
systemd/openbao-alert@.service
Normal file
13
systemd/openbao-alert@.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Email alert for failed OpenBAO maintenance unit %i
|
||||
# Deliberately no OnFailure= here: if the alerter itself fails it must not
|
||||
# recurse. It exits 0 on send failure and logs to /var/log/openbao-alerts.log.
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# %i (escaped instance), NOT %I: unit names contain '-', which systemd's
|
||||
# unescaping would turn into '/' (openbao-backup.service -> openbao/backup).
|
||||
ExecStart=/home/lutz/Projects/OpenBAO/scripts/send-failure-alert.sh %i
|
||||
# Runs as root: reads /etc/openbao-alert-smtp.token and journalctl -u.
|
||||
12
systemd/openbao-approle-rotate.service
Normal file
12
systemd/openbao-approle-rotate.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Rotate the demo-app AppRole SecretID before its 30d TTL expires
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
OnFailure=openbao-alert@%n.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Runs as root: reads /etc/openbao-approle-rotate.token and writes the
|
||||
# SecretID back as lutz:lutz 0600.
|
||||
ExecStart=/home/lutz/Projects/OpenBAO/scripts/rotate-approle-secret-id.sh
|
||||
13
systemd/openbao-approle-rotate.timer
Normal file
13
systemd/openbao-approle-rotate.timer
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Weekly rotation of the demo-app AppRole SecretID
|
||||
|
||||
[Timer]
|
||||
# Weekly against a 30d secret_id_ttl: 4x margin, so three consecutive failed
|
||||
# rotations can happen before the credential actually expires -- and the first
|
||||
# failure already mails an alert via OnFailure=.
|
||||
OnCalendar=Sun *-*-* 03:10:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=15m
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -3,6 +3,7 @@ Description=Raft snapshot backup of both OpenBAO instances (main + unsealer)
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
OnFailure=openbao-alert@%n.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@@ -3,6 +3,7 @@ Description=Renew openbao.famfi.home cert from OpenBAO PKI and reload Traefik
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
OnFailure=openbao-alert@%n.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@@ -3,10 +3,11 @@ Description=Renew scoped OpenBAO app tokens so periodic tokens never lapse
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
Requires=docker.service
|
||||
OnFailure=openbao-alert@%n.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