From 7e8568777e79ab071ff46a123e737c25f0b8975e Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 16 Jun 2026 23:21:08 +0100 Subject: [PATCH] feat(ops): idempotent OpenBao provisioning script scripts/provision-openbao.sh recreates the KV mount + app-mcpd ACL policy + periodic app-mcpd-role that mcpd's secret backend needs. These were hand-made and uncaptured, so an OpenBao re-init silently dropped the policy (root cause of the recurring BACKEND_TOKEN_DEAD / 403-on-secret-write). Now reproducible: run after any OpenBao (re)init; --seed also mints a token, writes bao-creds, and restarts mcpd. Mirrors src/shared/src/vault/policy.ts. Idempotent + --dry-run. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/provision-openbao.sh | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 scripts/provision-openbao.sh diff --git a/scripts/provision-openbao.sh b/scripts/provision-openbao.sh new file mode 100755 index 0000000..f833501 --- /dev/null +++ b/scripts/provision-openbao.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Idempotently (re)provision the OpenBao objects mcpd's secret backend needs. +# +# WHY THIS EXISTS: the KV mount + `app-mcpd` ACL policy + `app-mcpd-role` token +# role were hand-created and NOT captured anywhere, so an OpenBao rebuild/re-init +# silently dropped the policy — leaving mcpd with valid-looking tokens that +# granted nothing (403 on every secret write). This script makes that +# provisioning reproducible: run it after any OpenBao (re)init to restore mcpd's +# access. It is safe to run repeatedly. +# +# It mirrors mcpd's own wizard (src/shared/src/vault/policy.ts) — keep them in sync. +# +# Usage: +# scripts/provision-openbao.sh [--seed] [--dry-run] +# --seed also mint a fresh role token and write it into the `bao-creds` +# secret (mcpctl --direct ... --force), then restart mcpd. +# --dry-run print what would change; make no writes. +# +# Env (with sensible homelab defaults): +# BAO_ADDR=https://bao.ad.itaz.eu MOUNT=secret PREFIX=mcpctl ROLE=app-mcpd-role +# POLICY=app-mcpd PERIOD=2592000 KUBE_CONTEXT=worker0-k8s0 +# BAO_TOKEN (admin/root) — else read from the openbao-init-credentials secret. +set -euo pipefail + +BAO_ADDR="${BAO_ADDR:-https://bao.ad.itaz.eu}" +MOUNT="${MOUNT:-secret}" +PREFIX="${PREFIX:-mcpctl}" +ROLE="${ROLE:-app-mcpd-role}" +POLICY="${POLICY:-app-mcpd}" +PERIOD="${PERIOD:-2592000}" # 30d periodic token → renews forever +KUBE_CONTEXT="${KUBE_CONTEXT:-worker0-k8s0}" +MCPCTL_NS="${MCPCTL_NS:-mcpctl}" + +SEED=false; DRY=false +for a in "$@"; do case "$a" in --seed) SEED=true;; --dry-run) DRY=true;; esac; done + +say() { printf '\n\033[1;36m>>> %s\033[0m\n' "$*"; } +ok() { printf '\033[1;32m ✓ %s\033[0m\n' "$*"; } +die() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } + +# ── Admin token ── +if [ -z "${BAO_TOKEN:-}" ]; then + BAO_TOKEN="$(kubectl --context "$KUBE_CONTEXT" -n openbao get secret openbao-init-credentials -o jsonpath='{.data.root-token}' 2>/dev/null | base64 -d || true)" +fi +[ -n "${BAO_TOKEN:-}" ] || die "no BAO_TOKEN and could not read openbao-init-credentials root-token" +bao() { curl -fsS -m 15 -H "X-Vault-Token: $BAO_TOKEN" "$@"; } +bao -o /dev/null "$BAO_ADDR/v1/auth/token/lookup-self" || die "admin token rejected by $BAO_ADDR (OpenBao may have been re-initialized — update the init-credentials secret)" +ok "admin token valid" + +# Canonical policy HCL — must match src/shared/src/vault/policy.ts +read -r -d '' POLICY_HCL </dev/null; then + ok "mount '$MOUNT/' already exists" +else + bao -X POST "$BAO_ADDR/v1/sys/mounts/$MOUNT" -d '{"type":"kv","options":{"version":"2"}}' >/dev/null + ok "created KV v2 mount '$MOUNT/'" +fi + +# ── 2. Policy ── +say "2. Write ACL policy '$POLICY'" +bao -X PUT "$BAO_ADDR/v1/sys/policies/acl/$POLICY" \ + -d "$(python3 -c 'import json,sys; print(json.dumps({"policy": sys.stdin.read()}))' <<<"$POLICY_HCL")" >/dev/null +ok "policy '$POLICY' written ($(printf '%s' "$POLICY_HCL" | wc -l) rules)" + +# ── 3. Periodic token role ── +say "3. Write token role '$ROLE' (periodic, renewable)" +bao -X POST "$BAO_ADDR/v1/auth/token/roles/$ROLE" \ + -d "$(python3 -c 'import json,sys; print(json.dumps({"allowed_policies":[sys.argv[1]],"period":int(sys.argv[2]),"renewable":True,"token_type":"service","orphan":False}))' "$POLICY" "$PERIOD")" >/dev/null +ok "role '$ROLE' written (period=${PERIOD}s, allowed_policies=[$POLICY])" + +# ── 4. Optional: seed bao-creds + restart mcpd ── +if [ "$SEED" = true ]; then + say "4. Mint token + seed bao-creds + restart mcpd" + NEW_TOK="$(bao -X POST "$BAO_ADDR/v1/auth/token/create/$ROLE" | python3 -c 'import sys,json; print(json.load(sys.stdin)["auth"]["client_token"])')" + [ -n "$NEW_TOK" ] || die "failed to mint role token" + mcpctl --direct create secret bao-creds --data "token=$NEW_TOK" --force >/dev/null + ok "bao-creds updated" + kubectl --context "$KUBE_CONTEXT" -n "$MCPCTL_NS" rollout restart deployment/mcpd >/dev/null + kubectl --context "$KUBE_CONTEXT" -n "$MCPCTL_NS" rollout status deployment/mcpd --timeout=3m + ok "mcpd restarted — verify with: mcpctl status (Secrets line should be ✓)" +fi + +say "OpenBao provisioning complete."