access-engine/examples/ops-warden-fetch/README.md
tegwick 0ced2995ee
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m27s
FLEX-WP-0036: binary renamed to access-engine; flex-auth is a stub naming the rename (FLEX-DEC-2026-020)
- cmd/flex-auth -> cmd/access-engine; usage and serving lines say access-engine
- cmd/flex-auth: prints the rename and exits 2, with a test
- image: entrypoint /usr/local/bin/access-engine, flex-auth stub alongside;
  live pins pass only args, so their next roll is unaffected
- Makefile, CI, verify-posture, tenancy.yaml, live docs updated; workplans,
  decisions and ADRs left as written
- runtime names (module, audience, k8s, image package, /opt paths) unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 735967@bnt-lap001
Assistant-Session: 5971967f-77f8-4ced-9e02-c6ef67040ffc
2026-10-10 13:26:19 +02:00

4.1 KiB

ops-warden secret-read example (flex-auth-ops-warden-fetch)

Policy, registry and fixtures for ops-warden's secret-fetch policy check (check_fetch_policy in ops-warden src/warden/policy.py): before warden access <need> --fetch/--exec proxies a non-SSH credential read, it asks whether read on secret:<need_id>[/<domain>] owned by <owner_repo> is allowed. Opened by the-custodian bug b792c5d0 (CUST-WP-0075): those checks were sent to the SSH signing pin flex-auth-ops-warden, whose package admits only resource.system == "ops-warden" and whose caller bindings know only that system, so every other owner got 403.

File What it is
policy_package.md ops-warden.secret.read v1, allow_ttl: 5m; the catalog_lanes block is generated
registry_snapshot.json generated: one system per owner, one secret:<need_id> resource per lane
policy_fixtures.yaml 15 fixtures: 4 allows, 11 denial branches
check_request_*.json standalone requests for POST /v1/check
../../values/ops-warden-fetch.yaml the pin; caller bindings generated, image digest set by hand after CI

Why a separate pin

One decision class per pin, independently rollable (FLEX-WP-0016). The server serves one policy package per process. SSH signing (sign on ssh-certificate, system ops-warden) and secret reads (read on secret, system = owner repo) differ in action, resource type, systems and caller bindings. Folding reads into the signing pin would redeploy the lane the ops-bridge tunnels depend on for every catalog change.

The rule

Allow when all hold, otherwise deny with the first failing reason: action == read (wrong_action), resource.type == secret (wrong_resource_type), tenant tenant:platform (wrong_tenant), subject.type == operator (wrong_subject_type), non-empty subject id (missing_subject), context.need_id present (missing_need_id) and in the catalog set (unknown_need), resource.system a catalog owner (unknown_system) and the owner of that need (wrong_system), resource.id == secret:<need_id> or secret:<need_id>/<context.domain> (resource_need_mismatch), context.owner_repo == resource.system (owner_repo_mismatch).

Caller authentication is enforce: every owner system is bound to system:serviceaccount:ops-warden:ops-warden. Another caller, an anonymous call, or an owner system missing from the bindings is rejected with 401/403 before the package runs.

For a registered resource the registry's type wins over the caller's (FLEX-DEC-2026-012), so a non-secret type only reaches the package on an unregistered id; the package denies it either way.

Regenerate when the catalog changes

The readable set is every ops-warden catalog entry with status: active, exec_capable: true and lane: secret. After a lane is added or retired:

python3 tools/gen_ops_warden_fetch.py            # rewrites the three generated files
python3 tools/gen_ops_warden_fetch.py --check    # exit 1 on drift (tests/stage1.sh runs it)
go run ./cmd/access-engine test-policy --file examples/ops-warden-fetch/policy_package.md

--catalog PATH overrides the default ~/ops-warden/registry/routing/catalog.yaml. A new owner system adds a caller binding, so it needs a pin redeploy. A new lane under an existing owner changes only the package and registry, which are baked into the image, so it needs a new image too (deploy/README.md).

Verify

go run ./cmd/access-engine test-policy --file examples/ops-warden-fetch/policy_package.md   # 6 Rego tests, 15 fixtures
go run ./cmd/access-engine load-registry -file examples/ops-warden-fetch/registry_snapshot.json
go test ./cmd/access-engine -run OpsWardenFetch

Not yet deployed

values/ops-warden-fetch.yaml carries no image digest until CI builds an image containing this directory. The deploy steps (founder go required) are in workplans/ADHOC-2026-10-03.md.

Residual (build mode, close-down checklist row 1): subject.id is whatever ops-warden sends (its subject env or operator); it is not bound to an attended human identity. The caller binding proves that ops-warden asked, not which operator did.