2026-09-29 21:40:47 +02:00
spec : runbook-pack/v0.2
2026-09-29 08:34:34 +02:00
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
2026-09-29 21:40:47 +02:00
framing :
2026-09-29 08:34:34 +02:00
- 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.
2026-09-29 21:40:47 +02:00
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
2026-09-30 18:46:05 +02:00
label : access-engine checkout (repository formerly flex-auth)
2026-09-29 21:40:47 +02:00
type : path
2026-09-30 18:46:05 +02:00
default : ~/access-engine
2026-09-29 21:40:47 +02:00
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 :
2026-09-30 18:46:05 +02:00
- Read access-engine/docs/decision-record-contract.md and access-engine/docs/operator-caller-access-path.md first.
2026-09-29 21:40:47 +02:00
steps :
- id : prerequisites
title : Confirm the prerequisites
actor : operator
owner : workstation
2026-10-10 15:28:05 +02:00
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)."
2026-09-29 21:40:47 +02:00
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]
2026-10-10 15:28:05 +02:00
command : cd {{flexauth_home}} && go run ./cmd/access-engine validate -kind policy -file {{example}}/policy_package.md
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : validate exits 0
2026-10-10 15:28:05 +02:00
command : cd {{flexauth_home}} && go run ./cmd/access-engine validate -kind policy -file {{example}}/policy_package.md
2026-09-29 21:40:47 +02:00
expect : exit-0
- id : load-registry
title : Load the registry snapshot offline
actor : operator
owner : flex-auth
requires : [ validate-policy]
uses : [ flexauth_home, example]
2026-10-10 15:28:05 +02:00
command : cd {{flexauth_home}} && go run ./cmd/access-engine load-registry -file {{example}}/registry_snapshot.json
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : load-registry exits 0
2026-10-10 15:28:05 +02:00
command : cd {{flexauth_home}} && go run ./cmd/access-engine load-registry -file {{example}}/registry_snapshot.json
2026-09-29 21:40:47 +02:00
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 : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : The decision is allow with reason catalog_lane_policy_matched
command : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
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 : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : The decision is deny with reason wrong_tenant
command : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
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 : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : The decision is deny with reason unknown_action
command : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
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 : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
idempotent : idempotent
verify :
done_when : The decision is deny with reason unknown_subject
command : |
2026-10-10 15:28:05 +02:00
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'))"
2026-09-29 21:40:47 +02:00
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
2026-10-10 15:28:05 +02:00
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
2026-09-29 21:40:47 +02:00
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
2026-09-29 08:34:34 +02:00
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}