Files
OpenBAO/scripts/apply-policies.sh
Lutz Finsterle 5e5e7f779f Add governance blueprint: policy-as-code, operator/auditor, identity setup
Reviewable governance scaffolding — NOTHING applied to live OpenBAO yet:

- policies/: materialize all existing policies as code (faithfully fetched
  from live) + new `operator` (use engines, no admin) and `auditor`
  (read-only governance visibility, no secret material)
- scripts/apply-policies.sh: idempotent policy-as-code apply, with a
  read-only --dry-run that diffs files vs live (ignores comments)
- scripts/setup-identity.sh: identity-as-code — policy-bound groups
  (g-admins/operators/auditors/personal) + a human entity/alias; DEFAULT
  DRY-RUN, --apply to execute
- GOVERNANCE.md: the layered model, policy catalog, naming, apply order,
  and cross-cutting controls (audit device, root offline, AppRole migration)

Dry-runs verified read-only: apply-policies shows operator/auditor as NEW,
all others unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:05:38 +02:00

40 lines
2.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Policy-as-code: apply every policies/*.hcl to OpenBAO. Idempotent.
# apply-policies.sh --dry-run # read-only: show NEW/CHANGED/unchanged, change nothing
# apply-policies.sh # apply (writes policies that are NEW or CHANGED)
#
# Auth: needs a token allowed to write sys/policies/acl/* (root or admin).
# Uses $BAO_TOKEN if set, else falls back to the root token in init-output.json.
# Built-in `default`/`root` policies have no file here and are never touched.
set -euo pipefail
SELF="$(cd "$(dirname "$0")" && pwd)"; ROOT_DIR="$(cd "$SELF/.." && pwd)"
DIR="$ROOT_DIR/policies"; INIT="$ROOT_DIR/init-output.json"
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
DRY=0; [ "${1:-}" = "--dry-run" ] && DRY=1
TOKEN="${BAO_TOKEN:-}"
[ -z "$TOKEN" ] && [ -r "$INIT" ] && TOKEN="$(python3 -c "import json;print(json.load(open('$INIT'))['root_token'])")"
[ -n "$TOKEN" ] || { echo "no token (set BAO_TOKEN or provide init-output.json)" >&2; exit 1; }
# functional comparison: ignore comments and blank lines
norm(){ grep -vE '^[[:space:]]*#' | grep -vE '^[[:space:]]*$' | sed 's/[[:space:]]*$//'; }
echo "${DRY:+[dry-run] }policies under $DIR -> $ADDR"
for f in "$DIR"/*.hcl; do
name="$(basename "$f" .hcl)"; filetxt="$(cat "$f")"
live="$(curl -sS -H "X-Vault-Token: $TOKEN" "$ADDR/v1/sys/policies/acl/$name" \
| python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('data',{}).get('policy','') if 'data' in d else '')" 2>/dev/null || true)"
if [ -z "$live" ]; then status="NEW"
elif [ "$(printf '%s' "$filetxt" | norm)" = "$(printf '%s' "$live" | norm)" ]; then status="unchanged"
else status="CHANGED"; fi
if [ "$DRY" = 1 ] || [ "$status" = unchanged ]; then
printf ' %-10s %s\n' "$status" "$name"; continue
fi
code="$(printf '%s' "$filetxt" | python3 -c 'import json,sys;print(json.dumps({"policy":sys.stdin.read()}))' \
| curl -sS -o /dev/null -w '%{http_code}' -H "X-Vault-Token: $TOKEN" --data @- "$ADDR/v1/sys/policies/acl/$name")"
printf ' %-10s %s (HTTP %s)\n' "applied[$status]" "$name" "$code"
done
[ "$DRY" = 1 ] && echo "(dry-run — nothing changed)" || true