Files
OpenBAO/scripts/renew-openbao-cert.sh
Lutz Finsterle 14f46e1082 Add unsealer, internal CA/TLS, auto-unseal, and automated cert+backup
Build out the home-lab OpenBAO deployment beyond the basic node:

- docker-compose: add openbao-unsealer sidecar; main node now transit
  auto-unseals against it (seal config kept in gitignored config/seal.hcl)
- policies/admin.hcl: non-root admin policy; per-engine rules for
  ssh/pki/pki_int/totp/transit
- Internal two-tier CA (pki/ root + pki_int/ intermediate) issues the
  openbao.famfi.home leaf Traefik serves; root CA published under ca/
- scripts/ + systemd/: daily cert renewal and Raft snapshot backups
  (both instances), with scoped tokens stored outside the repo
- README: full runbook (auto-unseal, PKI, renewal, backups, DR/restore)

Secrets (init/unsealer keys, tokens, seal stanza) stay gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:28:49 +02:00

76 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Renew the openbao.famfi.home leaf cert from OpenBAO's pki_int engine and
# install it where Traefik serves it. Idempotent: only renews when the current
# cert expires within $RENEW_WINDOW_DAYS. Run as root (writes Traefik's tls dir).
#
# Installed as a systemd timer (openbao-cert-renew.timer). Manual run:
# sudo /home/lutz/Projects/OpenBAO/scripts/renew-openbao-cert.sh [--force]
set -euo pipefail
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
TOKEN_FILE="/etc/openbao-cert-renew.token"
ROLE="pki_int/issue/famfi-home"
CN="openbao.famfi.home"
TTL="2160h" # 90 days
DEST="/srv/TRAEFIK/etc/traefik/tls/openbao"
DYN="/srv/TRAEFIK/etc/traefik/traefik.d/tls-openbao.yml"
RENEW_WINDOW_DAYS="${RENEW_WINDOW_DAYS:-21}" # renew when <= this many days left
FORCE="${1:-}"
log() { printf '%s [renew-cert] %s\n' "$(date '+%F %T')" "$*"; }
die() { log "ERROR: $*"; exit 1; }
[ -r "$TOKEN_FILE" ] || die "token file $TOKEN_FILE not readable (run as root?)"
TOKEN="$(cat "$TOKEN_FILE")"
# Skip if the current cert is still good (unless --force)
if [ "$FORCE" != "--force" ] && [ -f "$DEST/fullchain.pem" ]; then
end="$(openssl x509 -in "$DEST/fullchain.pem" -noout -enddate | cut -d= -f2)"
days_left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
if [ "$days_left" -gt "$RENEW_WINDOW_DAYS" ]; then
log "cert valid ${days_left}d (> ${RENEW_WINDOW_DAYS}d) — nothing to do"
exit 0
fi
log "cert has ${days_left}d left (<= ${RENEW_WINDOW_DAYS}d) — renewing"
fi
# Issue a fresh cert
resp="$(curl -sS --fail-with-body --max-time 15 \
-H "X-Vault-Token: ${TOKEN}" \
--data "{\"common_name\":\"${CN}\",\"ttl\":\"${TTL}\"}" \
"${ADDR}/v1/${ROLE}")" || die "issue request failed"
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
export RESP="$resp"
python3 - "$tmp" <<PY
import json, sys, os
d = json.loads(os.environ["RESP"])["data"]
t = sys.argv[1]
open(f"{t}/fullchain.pem", "w").write(d["certificate"] + "\n" + "\n".join(d.get("ca_chain", [])) + "\n")
open(f"{t}/privkey.pem", "w").write(d["private_key"] + "\n")
PY
# Validate cert/key match before installing
cmod="$(openssl x509 -in "$tmp/fullchain.pem" -noout -modulus | openssl md5)"
kmod="$(openssl rsa -in "$tmp/privkey.pem" -noout -modulus 2>/dev/null | openssl md5)"
[ "$cmod" = "$kmod" ] || die "cert/key modulus mismatch — refusing to install"
install -d -o root -g root -m 0755 "$DEST"
install -o root -g root -m 0644 "$tmp/fullchain.pem" "$DEST/fullchain.pem"
install -o root -g root -m 0600 "$tmp/privkey.pem" "$DEST/privkey.pem"
# Rewrite the watched dynamic config (with a fresh timestamp) so Traefik's file
# provider reparses and reloads the cert from disk — changing the cert file
# alone does NOT trigger a reload.
cat > "$DYN" <<YML
# AUTO-MANAGED by renew-openbao-cert.sh — last renewed $(date -Iseconds).
# Loads OpenBAO's leaf cert for openbao.famfi.home; the openbao router uses
# \`tls: {}\` and serves this by SNI.
tls:
certificates:
- certFile: /etc/traefik/tls/openbao/fullchain.pem
keyFile: /etc/traefik/tls/openbao/privkey.pem
YML
chmod 0644 "$DYN"
log "installed new cert ($(openssl x509 -in "$DEST/fullchain.pem" -noout -enddate | cut -d= -f2)); Traefik reload triggered"