diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 5360f9b..1b1b7dc 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -24,10 +24,10 @@ Hands-on paths for operating the canonical NetKingdom security patterns | --- | --- | --- | --- | | [OpenBao: consume, attend, recover](openbao-operating-path.md) | T03 | railiance-platform, net-kingdom | unexercised | | [Short-lived SSH credentials](ssh-certificates-and-tunnels.md) | T04 | ops-warden, ops-bridge | unexercised | +| [Add a protected system to flex-auth](protected-system-flex-auth.md) | T05 | flex-auth, package owner | unexercised (offline part run) | Deferred (see NK-WP-0009): T02 object-storage STS (needs an owner-backed -issuer and refusal/lease proof — ADR-0008 is architecture, not evidence) and -T05 flex-auth protected consumer. +issuer and refusal/lease proof — ADR-0008 is architecture, not evidence). ## Pattern mapping diff --git a/docs/tutorials/protected-system-flex-auth.md b/docs/tutorials/protected-system-flex-auth.md new file mode 100644 index 0000000..88e75d2 --- /dev/null +++ b/docs/tutorials/protected-system-flex-auth.md @@ -0,0 +1,124 @@ +# Add a protected system to flex-auth + +Exercise status: unexercised +Workplan task: NK-WP-0009-T05 +Pattern(s): authorization boundary (`docs/platform-identity-security-architecture.md`, ADR-0006); IAM Profile v0.3 consumption + +Exercise notes: the offline commands in steps 4 and 5 were run once by Claude +on 2026-09-28 against the `secrets-engine` example in a local flex-auth +checkout and produced the outcomes listed under Verification. The live +caller-authentication steps (6 and 7) have not been run. Nobody has yet +completed the whole tutorial, so it stays `unexercised`. + +## Outcome + +Your system publishes its resources and actions to flex-auth, sends check +requests carrying a projected caller identity, enforces the decision it gets +back, and is refused when the caller is wrong. + +## Prerequisites + +- **[owner: flex-auth]** A local checkout of `flex-auth` with Go, to run + `go run ./cmd/flex-auth`. Read `flex-auth/docs/decision-record-contract.md`. +- **[owner: your system's repo]** A system id and a list of the things it + protects and the actions it performs on them. +- **[owner: railiance-platform / package owner, ADR-0015]** A Kubernetes + ServiceAccount that will represent your system. Package and runtime + declarations belong to the owner, not to this repo. +- **[owner: net-kingdom]** Accepted IAM Profile v0.3 for the identity you + present (`canon/standards/iam-profile_v0.3.md`). + +## Architecture context + +flex-auth is the policy decision point; your system is the enforcement point. +flex-auth never sees secret material, only coordinates (ids, types, +attributes). The worked example throughout is `secrets-engine` +(`flex-auth/examples/secrets-engine/`). Two gates protect a consumer: +caller authentication (who is asking) and the policy decision (may they). A +consumer running with caller auth in `warn` mode is not protected. + +## Steps + +1. **[owner: your system's repo]** Define the resource types and the action + vocabulary. Actions are your system's own strings; do not infer them. + Copy the shape of `flex-auth/examples/secrets-engine/protected_system_manifest.yaml`: + `resource_types` (name, scope level, planes) and `actions` (name, + capabilities, planes, exposure modes, required context). Keep a verb that + is not a real gate out of the list (`revoke` is deliberately not an action + in the example; the CLI verb gates as `deactivate`). +2. **[owner: your system's repo]** Publish resource manifests and, for + callers, a subject manifest: one service identity per protected system. + Send empty lists rather than omitting or guessing attributes. +3. **[owner: flex-auth]** Write the policy package (Markdown with Rego) and + fixtures covering every allow and every denial branch, including wrong + tenant, unknown subject and unknown action. +4. **[owner: flex-auth]** Validate and load offline: + `go run ./cmd/flex-auth validate -kind policy -file ` + then `go run ./cmd/flex-auth load-registry -file `. +5. **[owner: flex-auth]** Evaluate a check request locally: + `go run ./cmd/flex-auth check -registry -policy -request `. + A request is `tenant`, `subject`, `action`, `resource` and `context`. The + result is a decision envelope (`flex-auth.decision-record.v1`) with + `effect`, `reason`, and a deterministic decision `id`. +6. **[owner: package owner]** Give the caller a projected identity: a + short-lived, audience-scoped ServiceAccount token, and register the exact + binding `=system:serviceaccount::` on the + flex-auth pin with audience `flex-auth`. Send the token as + `Authorization: Bearer`. Use the in-cluster trailing-dot FQDN of the pin, never + a bare service name. +7. **[owner: flex-auth]** Set caller authentication to `enforce` and run the + four negative tests from `flex-auth/docs/operator-caller-access-path.md` + (no header, wrong ServiceAccount, wrong audience, expired token). +8. **[owner: your system's repo]** Enforce the envelope: `allow` proceeds; + `deny` blocks; `redact` and `audit_only` apply their obligations; any + error or missing decision fails closed. Persist the decision id with every + deny, redaction, export and privileged action. + +## Verification + +Done when: + +- `validate` and `load-registry` exit 0 on your package and snapshot. +- Offline `check` returns `allow` for a permitted request, and `deny` with + reason `wrong_tenant`, `unknown_subject` and `unknown_action` for the + matching negative requests. (Observed on the `secrets-engine` example: + `allow` / `catalog_lane_policy_matched`; `deny` / `wrong_tenant`; + `deny` / `unknown_action` for `revoke`; `deny` / `unknown_subject`.) +- Against the live pin under `enforce`: a valid token returns 200 and an + `allow`; no header returns 401; a token for the wrong ServiceAccount returns + 403; a wrong-audience token returns 401; an expired token returns 401. + All four negatives must be refused before policy is evaluated. +- A pin whose logs still show `warn` is reported as not enforcing. That is a + failed check, not a pass. + +## Rollback + +- Offline artifacts are files; revert them in git. +- On the live pin, set caller authentication back only as a recorded, + time-boxed exception owned by the package owner, and never leave a + consumer in `warn` as the resting state. +- Removing a protected system means removing its binding and manifests; + consumers must then fail closed. + +## Threat checks + +- Never send a permanent token; mint short-lived tokens per use. +- 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, so 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 notes + +| Concern | Owner | +| --- | --- | +| Action vocabulary, resource manifests, enforcement of decisions | the protected system's repo | +| Policy packages, decision envelope, caller-auth verification | flex-auth | +| ServiceAccount, package and runtime declaration | package owner under ADR-0015 | +| Identity contract (IAM Profile v0.3) | net-kingdom | +| Credential routing for the token | ops-warden (pointer only) | diff --git a/workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md b/workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md index 8a05cc2..cde82d4 100644 --- a/workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md +++ b/workplans/NK-WP-0009-netkingdom-security-pattern-tutorials.md @@ -122,7 +122,7 @@ implementation. ```task id: NK-WP-0009-T05 -status: todo +status: progress priority: medium state_hub_task_id: "c380ef19-4bc2-5ade-b127-baf18c64bf35" ```