Files
mcpctl/scripts/provision-openbao.sh

97 lines
4.8 KiB
Bash
Raw Normal View History

#!/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 <<EOF || true
path "$MOUNT/data/$PREFIX/*" { capabilities = ["create", "read", "update"] }
path "$MOUNT/metadata/$PREFIX/*" { capabilities = ["list", "delete"] }
path "$MOUNT/metadata/$PREFIX/" { capabilities = ["list"] }
path "auth/token/create/$ROLE" { capabilities = ["create", "update"] }
path "auth/token/revoke-accessor" { capabilities = ["update"] }
path "auth/token/lookup-self" { capabilities = ["read"] }
EOF
say "Plan: mount=$MOUNT (kv2) prefix=$PREFIX policy=$POLICY role=$ROLE (periodic ${PERIOD}s) seed=$SEED dry-run=$DRY"
if [ "$DRY" = true ]; then printf '%s\n' "$POLICY_HCL"; say "dry-run: no writes."; exit 0; fi
# ── 1. KV v2 mount ──
say "1. Ensure KV v2 mount '$MOUNT/'"
if bao -o /dev/null "$BAO_ADDR/v1/sys/mounts/$MOUNT" 2>/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."