diff --git a/docs/openbao-public-listener-transition.md b/docs/openbao-public-listener-transition.md index c9e6068..9757882 100644 --- a/docs/openbao-public-listener-transition.md +++ b/docs/openbao-public-listener-transition.md @@ -29,6 +29,22 @@ http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback Record only a value-safe attended-login result. Do not record the authorization code, token, accessor, callback query, browser storage, or screenshots. +KeyCape admission is applied by the Net Kingdom owner procedure. Apply the +OpenBao half through the governed contained login lane; the child command is +silent and Warden self-revokes the attended session: + +```bash +warden plan \ + "attended OpenBao platform administration to add the exact operator-tunneled OIDC callback to auth/netkingdom/role/platform-admin" \ + --json +warden access openbao-platform-admin-login --exec -- \ + scripts/openbao-apply-operator-loopback-callback.sh +``` + +The plan must return `founder_required` and select +`openbao-platform-admin-login`. Do not run the owner command directly with a +persistent token. + ## Guarded sequence ```bash diff --git a/scripts/openbao-apply-operator-loopback-callback.sh b/scripts/openbao-apply-operator-loopback-callback.sh new file mode 100755 index 0000000..a632be5 --- /dev/null +++ b/scripts/openbao-apply-operator-loopback-callback.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Silent owner command for the governed openbao-platform-admin-login lane. +# Warden rejects any child output and self-revokes the attended session after +# this command exits. The role payload contains no secret values. + +set -euo pipefail + +ROLE_PATH="auth/netkingdom/role/platform-admin" +CALLBACK_URI="http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback" +PAYLOAD="$(mktemp "${TMPDIR:-/tmp}/openbao-platform-admin-role.XXXXXX.json")" +READBACK="$(mktemp "${TMPDIR:-/tmp}/openbao-platform-admin-readback.XXXXXX.json")" + +cleanup() { + rm -f "$PAYLOAD" "$READBACK" +} +trap cleanup EXIT INT TERM +chmod 0600 "$PAYLOAD" "$READBACK" + +command -v bao >/dev/null 2>&1 +command -v python3 >/dev/null 2>&1 + +cat >"$PAYLOAD" <<'ROLE_JSON' +{ + "role_type": "oidc", + "user_claim": "sub", + "groups_claim": "groups", + "oidc_scopes": ["openid", "profile", "email", "groups"], + "allowed_redirect_uris": [ + "http://localhost:8250/oidc/callback", + "http://127.0.0.1:8250/oidc/callback", + "http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback", + "https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback", + "https://bao.coulomb.social/ui/vault/auth/keycape/oidc/callback" + ], + "bound_claims": { + "groups": ["net-kingdom-admins"] + }, + "claim_mappings": { + "email": "email", + "preferred_username": "username" + }, + "policies": ["platform-admin"], + "ttl": "1h" +} +ROLE_JSON + +bao write "$ROLE_PATH" @"$PAYLOAD" >/dev/null 2>&1 +bao read -format=json "$ROLE_PATH" >"$READBACK" 2>/dev/null +python3 - "$READBACK" "$CALLBACK_URI" <<'PY' >/dev/null 2>&1 +import json +import sys + +path, callback = sys.argv[1:] +with open(path, encoding="utf-8") as handle: + role = json.load(handle).get("data") or {} + +if callback not in role.get("allowed_redirect_uris", []): + raise SystemExit(1) +if role.get("role_type") != "oidc": + raise SystemExit(1) +if "platform-admin" not in role.get("token_policies", role.get("policies", [])): + raise SystemExit(1) +PY diff --git a/tests/test_openbao_operator_loopback_callback.py b/tests/test_openbao_operator_loopback_callback.py new file mode 100644 index 0000000..5f9a481 --- /dev/null +++ b/tests/test_openbao_operator_loopback_callback.py @@ -0,0 +1,78 @@ +import json +import os +import subprocess +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "scripts/openbao-apply-operator-loopback-callback.sh" +CALLBACK = "http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback" + + +def _fake_bao(tmp_path: Path) -> tuple[Path, Path]: + capture = tmp_path / "written-role.json" + executable = tmp_path / "bao" + executable.write_text( + """#!/bin/sh +set -eu +if [ "$1" = "write" ]; then + cp "${3#@}" "$BAO_CAPTURE" + exit 0 +fi +if [ "$1" = "read" ]; then + if [ "${BAO_FAKE_MISSING_CALLBACK:-false}" = "true" ]; then + printf '%s\\n' '{"data":{"role_type":"oidc","token_policies":["platform-admin"],"allowed_redirect_uris":[]}}' + else + printf '%s\\n' '{"data":{"role_type":"oidc","token_policies":["platform-admin"],"allowed_redirect_uris":["http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback"]}}' + fi + exit 0 +fi +exit 2 +""", + encoding="utf-8", + ) + executable.chmod(0o755) + return executable, capture + + +def _run(tmp_path: Path, *, missing_callback: bool = False) -> tuple[subprocess.CompletedProcess[str], Path]: + _, capture = _fake_bao(tmp_path) + env = dict(os.environ) + env.update( + { + "PATH": f"{tmp_path}:{env['PATH']}", + "TMPDIR": str(tmp_path), + "BAO_CAPTURE": str(capture), + "BAO_FAKE_MISSING_CALLBACK": str(missing_callback).lower(), + } + ) + result = subprocess.run( + [str(SCRIPT)], + cwd=REPO_ROOT, + env=env, + text=True, + capture_output=True, + check=False, + ) + return result, capture + + +def test_contained_command_is_silent_and_writes_exact_role(tmp_path: Path) -> None: + result, capture = _run(tmp_path) + assert result.returncode == 0 + assert result.stdout == "" + assert result.stderr == "" + + role = json.loads(capture.read_text(encoding="utf-8")) + assert CALLBACK in role["allowed_redirect_uris"] + assert role["policies"] == ["platform-admin"] + assert role["bound_claims"] == {"groups": ["net-kingdom-admins"]} + assert not list(tmp_path.glob("openbao-platform-admin-*.json")) + + +def test_verification_failure_remains_silent_and_nonzero(tmp_path: Path) -> None: + result, _ = _run(tmp_path, missing_callback=True) + assert result.returncode != 0 + assert result.stdout == "" + assert result.stderr == "" + assert not list(tmp_path.glob("openbao-platform-admin-*.json")) diff --git a/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md b/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md index 7e44267..2143fc6 100644 --- a/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md +++ b/workplans/RAILIANCE-WP-0027-openbao-operator-only-access.md @@ -69,5 +69,12 @@ lifecycle-healthy and reaches the expected overlay. Then execute the guarded retraction, coordinate public DNS withdrawal with railiance-infra, and return non-secret acceptance evidence to Railiance Master. +Net Kingdom revision `61aeafe` additionally applied the exact KeyCape callback +live and proved the public authorization endpoint accepts it. Railiance +Platform now carries the silent, narrowly scoped +`scripts/openbao-apply-operator-loopback-callback.sh` owner command for the +governed `openbao-platform-admin-login` lane. The remaining hold is one +attended OIDC/MFA execution of that command followed by one loopback UI login. + This workplan authorizes no OpenBao seal/unseal, policy broadening, PVC or Secret mutation, reboot, restore, or RMASTER-WP-0020-T08 cleanup.