#!/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."