From 93661631ff2537e26ab54c647788bc3a8028fd76 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 29 Sep 2026 08:34:34 +0200 Subject: [PATCH] Convert OpenBao and flex-auth tutorials to runbook packs (NK-WP-0044-T03) Co-Authored-By: Claude Sonnet 5.5 Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477 --- docs/tutorials/README.md | 3 +- runbooks/openbao-operating-path/pack.yaml | 111 +++++++++++ runbooks/protected-system-flex-auth/pack.yaml | 181 ++++++++++++++++++ ...044-runbook-packs-for-runbook-tutorials.md | 9 +- 4 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 runbooks/openbao-operating-path/pack.yaml create mode 100644 runbooks/protected-system-flex-auth/pack.yaml diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 1ae7d54..543bebb 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -5,7 +5,8 @@ Hands-on paths for operating the canonical NetKingdom security patterns [`TEMPLATE.md`](TEMPLATE.md) and checked by `make tutorials-verify`. > **Moving.** Tutorials are becoming runbook packs in `runbooks/` for the -> `runbook-tutorials` engine (NK-WP-0044). `runbooks/ssh-certificates/` is the first. +> `runbook-tutorials` engine (NK-WP-0044). Packs: `runbooks/ssh-certificates/`, `runbooks/openbao-operating-path/`, +> `runbooks/protected-system-flex-auth/`. > These markdown files stay until their packs are exercised. ## Rules diff --git a/runbooks/openbao-operating-path/pack.yaml b/runbooks/openbao-operating-path/pack.yaml new file mode 100644 index 0000000..310dce0 --- /dev/null +++ b/runbooks/openbao-operating-path/pack.yaml @@ -0,0 +1,111 @@ +spec: runbook-pack/v0.1 +id: nk.openbao-operating-path +title: "OpenBao: consume, attend, recover" +owner: net-kingdom +outcome: You can reach the already-deployed private OpenBao, know which owner serves a credential, know the custody model and the attended recovery path, and have seen the ceremony-record validator refuse a secret marker. +exercise_status: unexercised +engine: native +parameters: + - {id: need, label: The credential you need, type: text, default: read a database password, help: Plain words; used only to look up the owner.} + - {id: tunnel, label: OpenBao tunnel, type: string, default: openbao-ui-railiance01, pattern: "[a-z0-9-]+", help: The named ops-bridge tunnel; never a public Bao URL (bao.coulomb.social is retired).} + - {id: evidence, label: Ceremony record path, type: path, default: .local/openbao-ceremony-record.json, help: Relative to the net-kingdom checkout. Only meaningful after an attended ceremony.} + - {id: probe, label: Scratch path for the negative probe, type: path, default: .local/ceremony-negative-probe.json, help: Created and removed by the probe step.} +prerequisites: + - {text: bridge CLI and the named tunnel definition, owner: ops-bridge} + - {text: warden CLI for credential routing, owner: ops-warden} + - {text: OpenBao already deployed and private. Greenfield deployment is a lab exercise only and never part of this pack, owner: railiance-platform} + - {text: A net-kingdom checkout; commands run from its root, owner: net-kingdom} +steps: + - id: route + title: Find who owns the credential + owner: ops-warden + command: warden route find "{{need}}" --json | head -30 + verify: + done_when: You know which repository owns the credential and that warden only routes, it does not vend + expect: manual + evidence: [owner_repo] + - id: tunnel-status + title: Check the OpenBao tunnel is connected + owner: ops-bridge + command: bridge status + verify: + done_when: The tunnel row shows connected + command: bridge status | grep -E "{{tunnel}} +connected" + expect: exit-0 + - id: tunnel-check + title: Run the end-to-end tunnel diagnostic + owner: ops-bridge + command: bridge check {{tunnel}} + verify: + done_when: The diagnostic passes + command: bridge check {{tunnel}} + expect: exit-0 + - id: custody-models + title: See the unseal custody models + owner: net-kingdom + command: python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-unseal-custody-models + verify: + done_when: attended-ceremony is listed as implemented + command: python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-unseal-custody-models | grep -q attended-ceremony + expect: exit-0 + - id: console-gates + title: Read the custody gates for the selected model + owner: net-kingdom + command: make security-bootstrap-console + verify: + done_when: You have read every gate and know which are met and which are not + expect: manual + - id: recovery-read + title: Read the attended recovery path + owner: net-kingdom + command: sed -n 1,82p docs/openbao-attended-ceremony-runbook.md + verify: + done_when: You can state who must be present, where each unseal share goes, and when the root token is revoked + expect: manual + - id: probe-refused + title: Watch the ceremony-record validator refuse a secret marker + owner: net-kingdom + description: Writes a scratch file holding a fake token-shaped marker, runs the validator on it, and removes the file. The marker is assembled at run time so this pack never contains one. + command: | + printf '{"note":"%s%s"}\n' hvs. AAAAAAAAAAAAAAAA > {{probe}} + make security-bootstrap-validate-openbao-ceremony-record EVIDENCE={{probe}} 2>&1 | grep "secret-looking marker present" + rm -f {{probe}} + risk: changes-state + rollback: rm -f the probe file; nothing else is written. + verify: + done_when: The validator names a secret-looking marker as a reason for refusal + command: | + printf '{"note":"%s%s"}\n' hvs. AAAAAAAAAAAAAAAA > {{probe}} + make security-bootstrap-validate-openbao-ceremony-record EVIDENCE={{probe}} 2>&1 | grep -q "secret-looking marker present" + r=$? + rm -f {{probe}} + exit $r + expect: exit-0 + - id: valid-record + title: Validate a real ceremony record + owner: net-kingdom + description: Only possible after an attended ceremony has produced a record. Without one, skip this step; a run with a skipped step cannot exercise the pack. + command: make security-bootstrap-validate-openbao-ceremony-record EVIDENCE={{evidence}} + verify: + done_when: The validator passes on the ceremony record + command: make security-bootstrap-validate-openbao-ceremony-record EVIDENCE={{evidence}} + expect: exit-0 + - id: read-secret + title: Read one secret you are entitled to + owner: railiance-platform + description: Authenticate with your own identity and read only the path the routing result names, following railiance-platform/docs/openbao.md. Never paste the value anywhere. + risk: attended + rollback: Close the session; the read changes no state. If a value was exposed, treat it as compromised and rotate it through its owner. + verify: + done_when: You read only the routed path with your own identity and no value was pasted into chat, logs, State Hub or a checkout + expect: manual +threat_checks: + - Init output, unseal shares and tokens go to the operator's screen only, never to chat, State Hub, logs or a Git checkout. + - Never use a public Bao URL; bao.coulomb.social is retired. + - Never place the root token and unseal shares in one artifact outside a lab. + - This pack never initializes or unseals the live estate. +ownership: + - {concern: "OpenBao deployment, configuration and unseal execution", owner: railiance-platform} + - {concern: Custody canon and the ceremony-record validator, owner: net-kingdom} + - {concern: Tunnel, owner: ops-bridge} + - {concern: Credential routing, owner: ops-warden} diff --git a/runbooks/protected-system-flex-auth/pack.yaml b/runbooks/protected-system-flex-auth/pack.yaml new file mode 100644 index 0000000..2a01439 --- /dev/null +++ b/runbooks/protected-system-flex-auth/pack.yaml @@ -0,0 +1,181 @@ +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} diff --git a/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md b/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md index 8fb9fcc..d663714 100644 --- a/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md +++ b/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md @@ -48,15 +48,18 @@ steps; verify and rollback). It validates; it is `unexercised`. ```task id: NK-WP-0044-T03 -status: todo +status: done priority: high state_hub_task_id: "ba1a41a7-b40d-5312-851e-93c613ac9b27" ``` `docs/tutorials/openbao-operating-path.md` and `protected-system-flex-auth.md` become native packs. The flex-auth pack's live part uses the informed-decision pin and the -four negative tests as parameterized steps. Keep the markdown until the packs are -exercised. +negative tests N1-N3 as parameterized steps. N4 (expired token) is not in the pack: +it needs a ten-minute wait and a held token, so it stays a documented manual check in +the markdown tutorial. The markdown stays until the packs are exercised. Verified +without a run record: the ten offline and read-only verify commands were executed +and passed; the cluster-touching steps (port-forward and token calls) have not been run. ## Validate packs in this repository