From 24e7826d2d7efe4eb605db2c71617092ab212307 Mon Sep 17 00:00:00 2001 From: Anatoly Koyfman Date: Tue, 4 Aug 2026 16:47:51 +0000 Subject: [PATCH 01/19] opa-kind-driver.sh Signed-off-by: Anatoly Koyfman --- aiac/docs/examples/opa-team1-policy.yaml | 1 + aiac/docs/opa-kind-runbook.md | 41 +- scripts/opa-kind-driver.sh | 525 +++++++++++++++++++++++ 3 files changed, 551 insertions(+), 16 deletions(-) create mode 100755 scripts/opa-kind-driver.sh diff --git a/aiac/docs/examples/opa-team1-policy.yaml b/aiac/docs/examples/opa-team1-policy.yaml index 755ae8f1d..fdc14afa6 100644 --- a/aiac/docs/examples/opa-team1-policy.yaml +++ b/aiac/docs/examples/opa-team1-policy.yaml @@ -90,6 +90,7 @@ spec: } source_ok if { not input.identity.client_id } + source_ok if { input.identity.client_id == "rossoctl"} source_ok if { some role in source_roles[input.identity.client_id] some scope in role_scopes[role] diff --git a/aiac/docs/opa-kind-runbook.md b/aiac/docs/opa-kind-runbook.md index b70dd34dd..e4392245c 100644 --- a/aiac/docs/opa-kind-runbook.md +++ b/aiac/docs/opa-kind-runbook.md @@ -334,34 +334,43 @@ TOK=$(curl -s -X POST "http://keycloak.localtest.me:8080/realms/rossoctl/protoco | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])') cat > /tmp/probe.py < **The example CR's `outbound/request.rego` denies this `tools/list` probe.** +> **The example CR's `outbound/request.rego` denies this `tools/list` probe** — +> and because the outbound pipeline includes `mcp-parser`, that denial is +> surfaced the MCP-correct way: a **JSON-RPC 2.0 error frame at HTTP 200** +> (`error.code: -32000`, `error.data.plugin: "opa"`), not an HTTP error status. +> The forward proxy renders a `Reject` for an MCP JSON-RPC request (one with a +> `method` and an `id`) as an application-layer error frame so the caller's MCP +> client sees a single failed tool call rather than a transport break — see +> `writeMCPRejection` in +> `authbridge/authlib/listener/httpx/render.go`. The request is **denied and +> never reaches `github-tool`**; the `HTTP 200` is only the JSON-RPC transport +> envelope. Classify the outcome by the response **body** (an `error` frame = +> denied, a `result` frame = allowed), not the HTTP status. +> > The rule admits a call only when the delegated user's role and the target > service both list the request's `input.mcp.params.name` (the invoked tool). -> A `tools/list` call carries no `params.name`, so neither gate matches, `allow` -> is `false`, and the probe returns `403`/`503` instead of `404`. To see the -> `404` success path, apply only the inbound tier of the CR, or drive a real -> tool invocation whose tool name is present in `subject_role_scopes` and -> `target_scopes` in the outbound rego. +> A `tools/list` call carries no `params.name`, so neither gate matches and +> `allow` is `false`. A non-MCP-shaped rejection (no parser, or a JSON-RPC +> *notification* with no `id`) instead falls through to a plain HTTP `403`; a +> `token-exchange` failure surfaces as `503` before OPA is even consulted. To +> see the full allow path (a `result` frame at HTTP 200), apply only the inbound +> tier of the CR, or drive a real tool invocation whose tool name is present in +> `subject_role_scopes` and `target_scopes` in the outbound rego. ## B.5 — The outbound OPA input, exactly diff --git a/scripts/opa-kind-driver.sh b/scripts/opa-kind-driver.sh new file mode 100755 index 000000000..4f125b59f --- /dev/null +++ b/scripts/opa-kind-driver.sh @@ -0,0 +1,525 @@ +#!/usr/bin/env bash +# opa-kind-driver.sh — execute aiac/docs/opa-kind-runbook.md end-to-end. +# +# This is an automated driver for the AIAC "OPA Kind Cluster Runbook" +# (aiac/docs/opa-kind-runbook.md). It runs every step of that runbook in +# order, prints each step and the result it obtained, prints the OPA `input` +# documents for BOTH the inbound and the outbound legs, and FAILS with a clear +# message the moment an observed result does not match the runbook's stated +# expectation (instead of silently continuing). +# +# What it does, mirroring the runbook 1:1: +# Step 1 enable OPA in both legs (scripts/opa-kind-enable.sh) +# Step 2 verify the starting point (pods, bundle-service, client-id) +# Part A inbound authorization +# A.1 dev-user token carries sub=dev-user +# A.2 baseline: dev-user AND alice both reach the app (HTTP 200) +# A.3 apply the client-scoped policy CR +# A.4 enforced: dev-user -> 200, alice -> 403 +# A.5 print the INBOUND OPA input + assert the decision result +# Part B outbound token-exchange + OPA +# B.1 add the github-tool outbound route +# B.2 grant github-agent the exchange scope (expect HTTP 204) +# B.3 restart github-agent to load the route +# B.4 outbound tools/list probe -> DENIED (JSON-RPC error frame at +# HTTP 200, or 403/503; outbound OPA present) +# B.5 print the OUTBOUND OPA input +# +# Requires: kubectl, helm, kind, python3, curl, and docker (or podman). +# Env vars (runbook defaults shown): +# OPERATOR_DIR path to the rossoctl/operator clone (default: ../operator) +# ROSSOCTL_DIR path to the rossoctl/rossoctl clone (default: ../rossoctl) +# NS agent namespace (default: team1) +# SYS_NS platform namespace (default: rossoctl-system) +# KC Keycloak base URL (default: http://keycloak.localtest.me:8080) +# REALM Keycloak realm (default: rossoctl) +# POLL_SECS max seconds to wait for OPA to poll a new bundle (default: 60) +# SKIP_ENABLE if set to 1, skip Step 1's image rebuild + opa-kind-enable.sh +# and only verify OPA is already wired (fast path when iterating +# on the policy CR against an already-enabled cluster). +# +# Run from the repo root (cortex/): +# OPERATOR_DIR=../operator ROSSOCTL_DIR=../rossoctl ./scripts/opa-kind-driver.sh +# SKIP_ENABLE=1 ./scripts/opa-kind-driver.sh # skip the rebuild, just re-test + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORTEX_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ── Configuration (runbook defaults) ──────────────────────────────────────── +# Sibling repo clones the enable step needs; default to ../operator and +# ../rossoctl (relative to the cortex repo root) when not set, mirroring +# opa-kind-enable.sh. +OPERATOR_DIR="${OPERATOR_DIR:-$(cd "$CORTEX_DIR/../operator" 2>/dev/null && pwd || echo "")}" +ROSSOCTL_DIR="${ROSSOCTL_DIR:-$(cd "$CORTEX_DIR/../rossoctl" 2>/dev/null && pwd || echo "")}" + +NS="${NS:-team1}" +SYS_NS="${SYS_NS:-rossoctl-system}" +KC="${KC:-http://keycloak.localtest.me:8080}" +REALM="${REALM:-rossoctl}" +POLL_SECS="${POLL_SECS:-60}" + +AGENT_LABEL="app.kubernetes.io/name=github-agent" +EXPECTED_SPIFFE="spiffe://localtest.me/ns/${NS}/sa/github-agent" +POLICY_FILE="${CORTEX_DIR}/aiac/docs/examples/opa-team1-policy.yaml" +ENABLE_SCRIPT="${SCRIPT_DIR}/opa-kind-enable.sh" +RESTORE_SCRIPT="${SCRIPT_DIR}/opa-kind-restore.sh" + +# ── Output helpers ────────────────────────────────────────────────────────── +if [ -t 1 ]; then + C_RED=$'\033[31m'; C_GRN=$'\033[32m'; C_YEL=$'\033[33m' + C_CYN=$'\033[36m'; C_BLD=$'\033[1m'; C_RST=$'\033[0m' +else + C_RED=""; C_GRN=""; C_YEL=""; C_CYN=""; C_BLD=""; C_RST="" +fi + +STEP_N=0 +step() { STEP_N=$((STEP_N + 1)); printf '\n%s==> [%02d] %s%s\n' "$C_BLD$C_CYN" "$STEP_N" "$*" "$C_RST"; } +info() { printf ' %s\n' "$*"; } +pass() { printf ' %sPASS%s %s\n' "$C_GRN" "$C_RST" "$*"; } +warn() { printf ' %sWARN%s %s\n' "$C_YEL" "$C_RST" "$*"; } +die() { printf '\n%sFAIL:%s %s\n' "$C_RED$C_BLD" "$C_RST" "$*" >&2; exit 1; } + +# expect_eq