FLEX-DEC-2026-020; the old path is a stub that exits 2. Runtime names unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 736651@bnt-lap001 Assistant-Session: f6189624-fd64-4109-b53e-f8a59ca8a2d8
333 lines
19 KiB
YAML
333 lines
19 KiB
YAML
spec: runbook-pack/v0.2
|
|
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.
|
|
engine: native
|
|
framing:
|
|
- 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.
|
|
actors:
|
|
- id: operator
|
|
role: Operator
|
|
description: Runs the offline evaluation and the live caller-authentication probes with their own cluster access.
|
|
privileges: [run-offline-checks, open-port-forward, mint-short-lived-token]
|
|
- id: system-owner
|
|
role: Owner of the protected system
|
|
description: Enforces the decision envelope in their own system and keeps it failing closed.
|
|
privileges: [enforce-decisions]
|
|
datapoints:
|
|
- id: flexauth_home
|
|
label: access-engine checkout (repository formerly flex-auth)
|
|
type: path
|
|
default: ~/access-engine
|
|
dimensions: {actor: operator, kind: location, subsystem: flex-auth}
|
|
ask_in: offline
|
|
- 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.
|
|
dimensions: {kind: location, subsystem: flex-auth}
|
|
ask_in: offline
|
|
- 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.
|
|
dimensions: {kind: target, subsystem: flex-auth}
|
|
ask_in: live
|
|
- id: caller_ns
|
|
label: Caller namespace
|
|
type: string
|
|
default: informed-decision
|
|
pattern: "[a-z0-9-]+"
|
|
dimensions: {kind: target, subsystem: kubernetes}
|
|
ask_in: live
|
|
- id: caller_sa
|
|
label: Bound ServiceAccount
|
|
type: string
|
|
default: review
|
|
pattern: "[a-z0-9-]+"
|
|
help: The ServiceAccount named in the pin's --caller-binding.
|
|
dimensions: {actor: operator, kind: identity, subsystem: kubernetes}
|
|
ask_in: live
|
|
- 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."
|
|
dimensions: {actor: operator, kind: identity, subsystem: kubernetes}
|
|
ask_in: live
|
|
- id: local_port
|
|
label: Local forward port
|
|
type: int
|
|
default: 18080
|
|
dimensions: {kind: target, subsystem: kubernetes}
|
|
ask_in: live
|
|
stages:
|
|
- id: offline
|
|
title: "Offline: the policy is sound"
|
|
goal: The protected system's manifest, policy and registry validate, and the policy gives the decisions the vocabulary promises.
|
|
framing:
|
|
- Read access-engine/docs/decision-record-contract.md and access-engine/docs/operator-caller-access-path.md first.
|
|
steps:
|
|
- id: prerequisites
|
|
title: Confirm the prerequisites
|
|
actor: operator
|
|
owner: workstation
|
|
description: "A flex-auth checkout with Go, to run go run ./cmd/access-engine (flex-auth); kubectl access to the cluster through the k3s-api tunnel (ops-bridge); the live pin runs with --caller-auth-mode enforce and its bound ServiceAccount exists (package owner, ADR-0015)."
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: All three prerequisites hold
|
|
expect: manual
|
|
- id: read-manifest
|
|
title: Read the protected-system manifest of the example
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [prerequisites]
|
|
uses: [flexauth_home, example]
|
|
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
|
|
idempotent: idempotent
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [read-manifest]
|
|
uses: [flexauth_home, example]
|
|
command: cd {{flexauth_home}} && go run ./cmd/access-engine validate -kind policy -file {{example}}/policy_package.md
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: validate exits 0
|
|
command: cd {{flexauth_home}} && go run ./cmd/access-engine validate -kind policy -file {{example}}/policy_package.md
|
|
expect: exit-0
|
|
- id: load-registry
|
|
title: Load the registry snapshot offline
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [validate-policy]
|
|
uses: [flexauth_home, example]
|
|
command: cd {{flexauth_home}} && go run ./cmd/access-engine load-registry -file {{example}}/registry_snapshot.json
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: load-registry exits 0
|
|
command: cd {{flexauth_home}} && go run ./cmd/access-engine load-registry -file {{example}}/registry_snapshot.json
|
|
expect: exit-0
|
|
- id: check-allow
|
|
title: A permitted request is allowed
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [load-registry]
|
|
uses: [flexauth_home, example]
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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'))"
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: The decision is allow with reason catalog_lane_policy_matched
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [load-registry]
|
|
uses: [flexauth_home, example]
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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'))"
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: The decision is deny with reason wrong_tenant
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [load-registry]
|
|
uses: [flexauth_home, example]
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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'))"
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: The decision is deny with reason unknown_action
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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
|
|
actor: operator
|
|
owner: flex-auth
|
|
requires: [load-registry]
|
|
uses: [flexauth_home, example]
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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'))"
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: The decision is deny with reason unknown_subject
|
|
command: |
|
|
cd {{flexauth_home}} && go run ./cmd/access-engine 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
|
|
gates:
|
|
- id: policy-sound
|
|
title: The policy evaluates as the vocabulary promises
|
|
goal: correct-authorization-decisions
|
|
blocking: true
|
|
downstream_risk: A policy that allows what it should deny, or denies for the wrong reason, makes the live caller tests meaningless.
|
|
checks:
|
|
- done_when: The policy package validates and the registry loads
|
|
command: cd {{flexauth_home}} && go run ./cmd/access-engine validate -kind policy -file {{example}}/policy_package.md && go run ./cmd/access-engine load-registry -file {{example}}/registry_snapshot.json
|
|
expect: exit-0
|
|
- done_when: One allow and three denials (wrong tenant, unknown action, unknown subject) each gave the expected decision and reason
|
|
expect: manual
|
|
- id: live
|
|
title: "Live: callers are authenticated"
|
|
goal: A live flex-auth pin accepts a short-lived token of the bound ServiceAccount and refuses every other caller before policy is evaluated.
|
|
requires: ["gate:policy-sound"]
|
|
framing:
|
|
- A port-forward bypasses NetworkPolicy, so caller authentication is the only gate on this path.
|
|
- These steps use your own cluster access and mint short-lived tokens; auto-confirm never starts them.
|
|
steps:
|
|
- id: open-forward
|
|
title: Open a port-forward to the live pin
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
uses: [pin_service, local_port]
|
|
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
|
|
idempotent: conditional
|
|
idempotent_note: A second forward fails while the local port is still in use.
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
requires: [open-forward]
|
|
uses: [caller_ns, caller_sa, local_port]
|
|
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":{}}'
|
|
idempotent: idempotent
|
|
idempotent_note: Mints a new short-lived token each time; nothing persists.
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
requires: [live-positive]
|
|
uses: [local_port]
|
|
command: |
|
|
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:{{local_port}}/v1/check -H 'Content-Type: application/json' -d '{}'
|
|
idempotent: idempotent
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
requires: [live-positive]
|
|
uses: [caller_ns, other_sa, local_port]
|
|
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 '{}'
|
|
idempotent: idempotent
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
requires: [live-positive]
|
|
uses: [caller_ns, caller_sa, local_port]
|
|
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 '{}'
|
|
idempotent: idempotent
|
|
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
|
|
actor: operator
|
|
owner: flex-auth
|
|
auto_confirm: manual-only
|
|
requires: [negative-no-header, negative-wrong-sa, negative-wrong-audience]
|
|
uses: [local_port]
|
|
description: Press Ctrl-C in the second terminal.
|
|
idempotent: idempotent
|
|
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
|
|
gates:
|
|
- id: caller-auth-enforced
|
|
title: Only the bound caller is accepted
|
|
goal: authenticated-callers
|
|
blocking: true
|
|
downstream_risk: A pin that accepts an unauthenticated or wrongly bound caller is not protecting the system it fronts.
|
|
checks:
|
|
- done_when: The bound ServiceAccount was accepted (200) and no credential (401), the wrong ServiceAccount (403) and the wrong audience (401) were each refused before policy
|
|
expect: manual
|
|
- id: own
|
|
title: Enforce it in your system
|
|
goal: Your own system enforces the decision it gets and fails closed.
|
|
requires: ["gate:caller-auth-enforced"]
|
|
steps:
|
|
- id: enforce-in-your-system
|
|
title: Confirm your own system enforces the decision and fails closed
|
|
actor: system-owner
|
|
owner: the protected system's repo
|
|
auto_confirm: manual-only
|
|
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."
|
|
idempotent: idempotent
|
|
verify:
|
|
done_when: For your system, you have seen each of those behaviours in code or a test
|
|
expect: manual
|
|
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}
|