spec: runbook-pack/v0.1 id: nk.protected-system-flex-auth title: Add a protected system to flex-auth owner: net-kingdom outcome: You have seen a protected system's manifests and policy evaluated offline, and a live flex-auth pin refuse callers with no credential, the wrong ServiceAccount, and the wrong audience. Expired-token refusal (negative N4) is not exercised here because it needs a ten-minute wait. exercise_status: unexercised engine: native parameters: - {id: flexauth_home, label: flex-auth checkout, type: path, default: ~/flex-auth} - {id: example, label: Offline example directory, type: path, default: examples/secrets-engine, help: Relative to the flex-auth checkout. The offline steps only read files.} - {id: pin_service, label: Live pin Service, type: string, default: flex-auth-informed-decision-sitting, pattern: "[a-z0-9-]+", help: A pin that enforces caller authentication.} - {id: caller_ns, label: Caller namespace, type: string, default: informed-decision, pattern: "[a-z0-9-]+"} - {id: caller_sa, label: Bound ServiceAccount, type: string, default: review, pattern: "[a-z0-9-]+", help: The ServiceAccount named in the pin's --caller-binding.} - {id: other_sa, label: Unbound ServiceAccount, type: string, default: default, pattern: "[a-z0-9-]+", help: "Any other ServiceAccount in the same namespace, for the wrong-identity test."} - {id: local_port, label: Local forward port, type: int, default: 18080} prerequisites: - {text: "A flex-auth checkout with Go, to run go run ./cmd/flex-auth", owner: flex-auth} - {text: kubectl access to the cluster through the k3s-api tunnel, owner: ops-bridge} - {text: "The live pin runs with --caller-auth-mode enforce and its bound ServiceAccount exists", owner: package owner (ADR-0015)} - {text: "Read flex-auth/docs/decision-record-contract.md and flex-auth/docs/operator-caller-access-path.md first", owner: flex-auth} steps: - id: read-manifest title: Read the protected-system manifest of the example owner: flex-auth description: Resource types and the action vocabulary belong to the protected system. Note that a verb that is not a real gate (revoke) is deliberately not an action. command: sed -n 1,50p {{flexauth_home}}/{{example}}/protected_system_manifest.yaml verify: done_when: You can name the resource type, and three actions with their planes expect: manual - id: validate-policy title: Validate the policy package offline owner: flex-auth command: cd {{flexauth_home}} && go run ./cmd/flex-auth validate -kind policy -file {{example}}/policy_package.md verify: done_when: validate exits 0 command: cd {{flexauth_home}} && go run ./cmd/flex-auth validate -kind policy -file {{example}}/policy_package.md expect: exit-0 - id: load-registry title: Load the registry snapshot offline owner: flex-auth command: cd {{flexauth_home}} && go run ./cmd/flex-auth load-registry -file {{example}}/registry_snapshot.json verify: done_when: load-registry exits 0 command: cd {{flexauth_home}} && go run ./cmd/flex-auth load-registry -file {{example}}/registry_snapshot.json expect: exit-0 - id: check-allow title: A permitted request is allowed owner: flex-auth command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_allow_rotate.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" verify: done_when: The decision is allow with reason catalog_lane_policy_matched command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_allow_rotate.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" expect: output-contains contains: allow catalog_lane_policy_matched - id: check-wrong-tenant title: The wrong tenant is denied owner: flex-auth command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_wrong_tenant.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" verify: done_when: The decision is deny with reason wrong_tenant command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_wrong_tenant.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" expect: output-contains contains: deny wrong_tenant - id: check-unknown-action title: A verb that is not an action is denied owner: flex-auth command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_revoke_not_an_action.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" verify: done_when: The decision is deny with reason unknown_action command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_revoke_not_an_action.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" expect: output-contains contains: deny unknown_action - id: check-unknown-subject title: An unknown subject is denied owner: flex-auth command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_unknown_subject.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" verify: done_when: The decision is deny with reason unknown_subject command: | cd {{flexauth_home}} && go run ./cmd/flex-auth check -registry {{example}}/registry_snapshot.json -policy {{example}}/policy_package.md -request {{example}}/check_request_deny_unknown_subject.json | python3 -c "import sys,json;d=json.load(sys.stdin);print(d['effect'],d.get('reason'))" expect: output-contains contains: deny unknown_subject - id: open-forward title: Open a port-forward to the live pin owner: flex-auth description: "Run the command in a SECOND terminal and leave it running. A port-forward bypasses NetworkPolicy, so caller authentication is the only gate on this path." command: kubectl -n flex-auth port-forward svc/{{pin_service}} {{local_port}}:8080 risk: changes-state rollback: Press Ctrl-C in the second terminal. verify: done_when: The forward answers on localhost command: curl -s -o /dev/null --max-time 5 http://localhost:{{local_port}}/ expect: exit-0 - id: live-positive title: A valid short-lived token is accepted owner: flex-auth description: The token is minted for one call, used, and never printed or stored. The decision may well be deny for this subject; what matters is that the call is authenticated (HTTP 200). command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{caller_sa}} --audience=flex-auth --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"id":"check:tutorial-probe","tenant":"tenant:platform","subject":{"id":"unknown","type":"human"},"action":"accept","resource":{"id":"memo:unrelated","type":"decision-memo","system":"informed-decision"},"context":{}}' risk: changes-state rollback: The token expires in ten minutes; there is nothing to undo. verify: done_when: HTTP 200 command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{caller_sa}} --audience=flex-auth --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"id":"check:tutorial-probe","tenant":"tenant:platform","subject":{"id":"unknown","type":"human"},"action":"accept","resource":{"id":"memo:unrelated","type":"decision-memo","system":"informed-decision"},"context":{}}' expect: output-contains contains: "200" - id: negative-no-header title: N1 - no credential is refused owner: flex-auth command: | curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H 'Content-Type: application/json' -d '{}' verify: done_when: HTTP 401 command: | curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H 'Content-Type: application/json' -d '{}' expect: output-contains contains: "401" - id: negative-wrong-sa title: N2 - a valid token for the wrong ServiceAccount is refused owner: flex-auth description: Any of thousands of ServiceAccounts can produce a well-formed token; only the bound one may represent the system. command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{other_sa}} --audience=flex-auth --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' risk: changes-state rollback: The token expires in ten minutes; there is nothing to undo. verify: done_when: HTTP 403 command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{other_sa}} --audience=flex-auth --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' expect: output-contains contains: "403" - id: negative-wrong-audience title: N3 - a token without the flex-auth audience is refused owner: flex-auth command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{caller_sa}} --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' risk: changes-state rollback: The token expires in ten minutes; there is nothing to undo. verify: done_when: HTTP 401 command: | TOKEN=$(kubectl -n {{caller_ns}} create token {{caller_sa}} --duration=10m); curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' expect: output-contains contains: "401" - id: close-forward title: Close the port-forward owner: flex-auth description: Press Ctrl-C in the second terminal. risk: changes-state rollback: Reopen it with the open-forward step if you still need it. verify: done_when: Nothing answers on the local port any more command: "! curl -s -o /dev/null --max-time 3 http://localhost:{{local_port}}/" expect: exit-0 - id: enforce-in-your-system title: Confirm your own system enforces the decision and fails closed owner: the protected system's repo description: "allow proceeds; deny blocks; redact and audit_only apply their obligations; an error or a missing decision fails closed; the decision id is stored with every deny, redaction, export and privileged action." verify: done_when: For your system, you have seen each of those behaviours in code or a test expect: manual threat_checks: - Never send a permanent token; mint short-lived tokens per use, and never print or store them. - Never point a consumer at a bare in-cluster service name; workstation resolvers can answer it with an unrelated host. Use the trailing-dot FQDN. - A port-forward bypasses NetworkPolicy; caller authentication is the only gate on an operator path. - Never put secret values in a resource attribute or a check request. - Do not give raw upstream group names platform meaning; map them explicitly per tenant. - Do not apply stale tenant-engine reference YAML; take manifests from the current owner package. ownership: - {concern: "Action vocabulary, resource manifests, enforcement of decisions", owner: "the protected system's repo"} - {concern: "Policy packages, decision envelope, caller-auth verification", owner: flex-auth} - {concern: "ServiceAccount, package and runtime declaration", owner: "package owner under ADR-0015"} - {concern: "Identity contract (IAM Profile v0.3)", owner: net-kingdom}