From 90112f3ca7f555740e8a031022bad24a9fb296b6 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 29 Sep 2026 21:40:47 +0200 Subject: [PATCH] Port the NetKingdom packs to runbook-pack/v0.2 (ssh, openbao, flex-auth); update NK-WP-0044 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 --- runbooks/openbao-operating-path/pack.yaml | 323 ++++++++---- runbooks/protected-system-flex-auth/pack.yaml | 481 ++++++++++++------ runbooks/ssh-certificates/pack.yaml | 180 +++++-- ...044-runbook-packs-for-runbook-tutorials.md | 29 +- 4 files changed, 698 insertions(+), 315 deletions(-) diff --git a/runbooks/openbao-operating-path/pack.yaml b/runbooks/openbao-operating-path/pack.yaml index 1a355ce..4384405 100644 --- a/runbooks/openbao-operating-path/pack.yaml +++ b/runbooks/openbao-operating-path/pack.yaml @@ -1,108 +1,237 @@ -spec: runbook-pack/v0.1 +spec: runbook-pack/v0.2 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. 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: +framing: - 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. + - This pack never initializes or unseals the live estate. Greenfield deployment is a lab exercise and is not part of it. +actors: + - id: operator + role: Operator + description: Reaches OpenBao through the named tunnel with their own identity and reads what they are entitled to. + privileges: [open-tunnel, read-routed-secret] +datapoints: + - id: need + label: The credential you need + type: text + default: read a database password + help: Plain words; used only to look up the owner. + dimensions: {actor: operator, kind: request, subsystem: ops-warden} + ask_in: access + - 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). + dimensions: {kind: target, subsystem: ops-bridge} + ask_in: access + - id: record_state + label: Has an attended ceremony produced a record yet? + type: enum + values: [exists, none-yet] + default: none-yet + help: Decides which way the record step is recommended. Only meaningful after an attended ceremony. + dimensions: {kind: state, subsystem: openbao} + ask_in: records + - id: evidence + label: Ceremony record path + type: path + default: .local/openbao-ceremony-record.json + help: Relative to the net-kingdom checkout. + dimensions: {kind: location, subsystem: openbao} + ask_in: records + - 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. + dimensions: {kind: location, subsystem: openbao} + ask_in: records +stages: + - id: access + title: Reach OpenBao + goal: You know who owns the credential and the private tunnel to OpenBao is up and healthy. + steps: + - id: prerequisites + title: Confirm the prerequisites + actor: operator + owner: workstation + description: "The bridge CLI and the named tunnel definition (ops-bridge); the warden CLI for credential routing (ops-warden); OpenBao already deployed and private (railiance-platform); a net-kingdom checkout, because commands run from its root (net-kingdom)." + idempotent: idempotent + verify: + done_when: All four prerequisites hold + expect: manual + - id: route + title: Find who owns the credential + actor: operator + owner: ops-warden + requires: [prerequisites] + uses: [need] + command: warden route find "{{need}}" --json | head -30 + idempotent: idempotent + evidence: [owner_repo] + verify: + done_when: You know which repository owns the credential and that warden only routes, it does not vend + expect: manual + - id: tunnel-status + title: Check the OpenBao tunnel is connected + actor: operator + owner: ops-bridge + requires: [route] + uses: [tunnel] + command: bridge status + idempotent: idempotent + 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 + actor: operator + owner: ops-bridge + requires: [tunnel-status] + uses: [tunnel] + command: bridge check {{tunnel}} + idempotent: idempotent + verify: + done_when: The diagnostic passes + command: bridge check {{tunnel}} + expect: exit-0 + gates: + - id: access-ready + title: OpenBao is reachable over its private tunnel + goal: private-access + blocking: true + downstream_risk: Reading secrets over an unhealthy or public path would put values where they must not go. + checks: + - done_when: The tunnel shows connected + command: bridge status | grep -E "{{tunnel}} +connected" + expect: exit-0 + - done_when: The end-to-end diagnostic passes + command: bridge check {{tunnel}} + expect: exit-0 + - id: custody + title: Know the custody + goal: You know the unseal custody model in force and the attended recovery path. + requires: [access] + steps: + - id: custody-models + title: See the unseal custody models + actor: operator + owner: net-kingdom + command: python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-unseal-custody-models + idempotent: idempotent + 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 + actor: operator + owner: net-kingdom + requires: [custody-models] + command: make security-bootstrap-console + idempotent: idempotent + 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 + actor: operator + owner: net-kingdom + requires: [console-gates] + command: sed -n 1,82p docs/openbao-attended-ceremony-runbook.md + idempotent: idempotent + verify: + done_when: You can state who must be present, where each unseal share goes, and when the root token is revoked + expect: manual + gates: + - id: custody-understood + title: The custody path is understood + goal: attended-custody + blocking: true + checks: + - done_when: You can name the custody model, who must be present at a ceremony, and where each unseal share goes + expect: manual + - id: records + title: The evidence rules + goal: You have seen the ceremony-record validator refuse a secret marker, and know where a real record stands. + requires: [custody] + steps: + - id: probe-refused + title: Watch the ceremony-record validator refuse a secret marker + actor: operator + owner: net-kingdom + uses: [probe] + 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}} + idempotent: idempotent + idempotent_note: Writes a scratch file and removes it in the same command. + 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: ceremony-record + title: Look at the ceremony record + actor: operator + owner: net-kingdom + requires: [probe-refused] + uses: [record_state, evidence] + description: A real record exists only after an attended ceremony. Choose the way that matches where you are; the recommended one follows the answer you gave. + idempotent: idempotent + one_of: + - id: validate-record + title: Validate the real ceremony record + recommended_when: {record_state: [exists]} + 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: no-record-yet + title: No ceremony has been held yet + recommended_when: {record_state: [none-yet]} + verify: + done_when: You know that no attended ceremony has produced a record, so there is nothing to validate yet + expect: manual + gates: + - id: evidence-rules-seen + title: The evidence rules are understood + goal: no-secrets-in-evidence + blocking: true + checks: + - done_when: The validator refused a secret-looking marker, and you know where a real record stands + expect: manual + - id: use + title: Read one secret + goal: You read one secret you are entitled to, from the path routing named, with your own identity. + requires: ["gate:access-ready", records] + steps: + - id: read-secret + title: Read one secret you are entitled to + actor: operator + owner: railiance-platform + auto_confirm: manual-only + 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. + idempotent: idempotent + 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 ownership: - {concern: "OpenBao deployment, configuration and unseal execution", owner: railiance-platform} - {concern: Custody canon and the ceremony-record validator, owner: net-kingdom} diff --git a/runbooks/protected-system-flex-auth/pack.yaml b/runbooks/protected-system-flex-auth/pack.yaml index ef293d7..e33d5b6 100644 --- a/runbooks/protected-system-flex-auth/pack.yaml +++ b/runbooks/protected-system-flex-auth/pack.yaml @@ -1,178 +1,331 @@ -spec: runbook-pack/v0.1 +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 -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: +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: flex-auth checkout + type: path + default: ~/flex-auth + 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 flex-auth/docs/decision-record-contract.md and flex-auth/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/flex-auth (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/flex-auth validate -kind policy -file {{example}}/policy_package.md + idempotent: idempotent + 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 + actor: operator + owner: flex-auth + requires: [validate-policy] + uses: [flexauth_home, example] + command: cd {{flexauth_home}} && go run ./cmd/flex-auth load-registry -file {{example}}/registry_snapshot.json + idempotent: idempotent + 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 + actor: operator + owner: flex-auth + requires: [load-registry] + uses: [flexauth_home, example] + 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'))" + idempotent: idempotent + 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 + actor: operator + owner: flex-auth + requires: [load-registry] + uses: [flexauth_home, example] + 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'))" + idempotent: idempotent + 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 + actor: operator + owner: flex-auth + requires: [load-registry] + uses: [flexauth_home, example] + 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'))" + idempotent: idempotent + 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 + actor: operator + owner: flex-auth + requires: [load-registry] + uses: [flexauth_home, example] + 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'))" + idempotent: idempotent + 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 + 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/flex-auth validate -kind policy -file {{example}}/policy_package.md && go run ./cmd/flex-auth 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} diff --git a/runbooks/ssh-certificates/pack.yaml b/runbooks/ssh-certificates/pack.yaml index 98c720c..a07523b 100644 --- a/runbooks/ssh-certificates/pack.yaml +++ b/runbooks/ssh-certificates/pack.yaml @@ -1,59 +1,143 @@ -spec: runbook-pack/v0.1 +spec: runbook-pack/v0.2 id: nk.ssh-certificates title: Short-lived SSH credentials for admins, agents and automations owner: net-kingdom outcome: An actor obtains a short-lived CA-signed SSH certificate and uses it through an ops-bridge tunnel, with no static key doing the work. engine: native -parameters: - - {id: actor, label: Actor name, type: string, default: agt-claude-railiance01, pattern: "(adm|agt|atm)-[a-z0-9-]+", help: "Actor type prefix decides the maximum TTL: adm 48h, agt 24h, atm 8h."} - - {id: pubkey, label: Public key path, type: path, default: ~/.ssh/id_ed25519.pub} - - {id: tunnel, label: Tunnel name, type: string, default: k3s-api-railiance01, pattern: "[a-z0-9-]+"} -prerequisites: - - {text: warden CLI installed and actor present in the principals inventory, owner: ops-warden} - - {text: bridge CLI and a tunnel definition, owner: ops-bridge} - - {text: Target hosts trust the SSH CA and carry the actor principal, owner: railiance-infra} -steps: - - id: status-before - title: Look at current certificates - owner: ops-warden - command: warden status - verify: {done_when: You know which certificates are current and which are expired, expect: manual} - - id: sign - title: Sign a public key for the actor - owner: ops-warden - command: warden sign {{actor}} --pubkey {{pubkey}} > /tmp/{{actor}}-cert.pub - risk: changes-state - rollback: Delete /tmp/{{actor}}-cert.pub; the certificate expires on its own. - verify: - done_when: A certificate file exists and names the expected principal - command: ssh-keygen -L -f /tmp/{{actor}}-cert.pub - expect: exit-0 - evidence: [actor, certificate_valid_before] - - id: inspect-ttl - title: Check the certificate lifetime - owner: ops-warden - command: ssh-keygen -L -f /tmp/{{actor}}-cert.pub | grep -E "Key ID|Principals|Valid" - verify: {done_when: "Valid before is within the actor-type TTL (adm 48h, agt 24h, atm 8h)", expect: manual} - - id: tunnel-up - title: Bring up the tunnel that uses cert_command - owner: ops-bridge - command: bridge up {{tunnel}} - risk: changes-state - rollback: bridge down {{tunnel}} - verify: - done_when: The tunnel shows connected - command: bridge status - expect: output-contains - contains: "{{tunnel}}" - - id: audit - title: Confirm the signing was audited - owner: ops-warden - command: warden log | tail -5 - verify: {done_when: Your signing appears in the history, expect: manual} -threat_checks: +framing: - Certificate files must be mode 600 and are never reused across reconnects. - A non-zero cert_command exit is a failure and must trigger backoff. - ops-warden never vends API keys or passwords; route them with warden route find. +actors: + - id: operator + role: Operator + description: Signs and inspects the certificate and brings the tunnel up, using their own identity. + privileges: [sign-certificate, open-tunnel] +datapoints: + - id: actor_name + label: Actor name + type: string + default: agt-claude-railiance01 + pattern: "(adm|agt|atm)-[a-z0-9-]+" + help: "Actor type prefix decides the maximum TTL: adm 48h, agt 24h, atm 8h." + dimensions: {actor: operator, kind: identity, subsystem: ops-warden} + ask_in: ready + - id: pubkey + label: Public key path + type: path + default: ~/.ssh/id_ed25519.pub + dimensions: {actor: operator, kind: location, subsystem: ops-warden} + ask_in: ready + - id: tunnel + label: Tunnel name + type: string + default: k3s-api-railiance01 + pattern: "[a-z0-9-]+" + dimensions: {kind: target, subsystem: ops-bridge} + ask_in: ready +stages: + - id: ready + title: Before you start + goal: You know what is in place and which certificates exist already. + steps: + - id: prerequisites + title: Confirm the prerequisites + actor: operator + owner: workstation + description: "In place before you start: the warden CLI is installed and your actor is in the principals inventory (ops-warden); the bridge CLI and a tunnel definition exist (ops-bridge); the target hosts trust the SSH CA and carry the actor principal (railiance-infra)." + idempotent: idempotent + verify: + done_when: All three prerequisites hold + expect: manual + - id: status-before + title: Look at current certificates + actor: operator + owner: ops-warden + requires: [prerequisites] + command: warden status + idempotent: idempotent + verify: + done_when: You know which certificates are current and which are expired + expect: manual + - id: get-certificate + title: Get a certificate + goal: A short-lived certificate for the actor exists and its lifetime is within the limit for the actor type. + requires: [ready] + steps: + - id: sign + title: Sign a public key for the actor + actor: operator + owner: ops-warden + uses: [actor_name, pubkey] + command: warden sign {{actor_name}} --pubkey {{pubkey}} > /tmp/{{actor_name}}-cert.pub + idempotent: idempotent + idempotent_note: Each run issues a new certificate and overwrites the file; the old one expires on its own. + rollback: Delete /tmp/{{actor_name}}-cert.pub; the certificate expires on its own. + evidence: [certificate_valid_before] + verify: + done_when: A certificate file exists and is readable as a certificate + command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub + expect: exit-0 + - id: inspect-ttl + title: Check the certificate lifetime + actor: operator + owner: ops-warden + requires: [sign] + uses: [actor_name] + command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub | grep -E "Key ID|Principals|Valid" + idempotent: idempotent + verify: + done_when: "Valid before is within the actor-type TTL (adm 48h, agt 24h, atm 8h)" + expect: manual + gates: + - id: certificate-valid + title: The certificate is valid and short-lived + goal: short-lived-credentials + blocking: true + downstream_risk: A tunnel opened with a certificate that outlives its limit defeats the point of short-lived credentials. + checks: + - done_when: The lifetime is within the limit for the actor type + expect: manual + - done_when: The certificate file can be read + command: ssh-keygen -L -f /tmp/{{actor_name}}-cert.pub + expect: exit-0 + - id: connect + title: Use it through a tunnel + goal: The tunnel that uses the certificate is up and the signing is on record. + requires: ["gate:certificate-valid"] + steps: + - id: tunnel-up + title: Bring up the tunnel that uses cert_command + actor: operator + owner: ops-bridge + uses: [tunnel] + command: bridge up {{tunnel}} + idempotent: idempotent + rollback: bridge down {{tunnel}} + verify: + done_when: The tunnel shows connected + command: bridge status + expect: output-contains + contains: "{{tunnel}}" + - id: audit + title: Confirm the signing was audited + actor: operator + owner: ops-warden + requires: [tunnel-up] + command: warden log | tail -5 + idempotent: idempotent + verify: + done_when: Your signing appears in the history + expect: manual + gates: + - id: tunnel-connected + title: The tunnel is connected + goal: private-access + blocking: true + checks: + - done_when: bridge reports the tunnel as connected + command: bridge status | grep -E "{{tunnel}} +connected" + expect: exit-0 ownership: - {concern: Certificate issuance and TTL policy, owner: ops-warden} - {concern: Tunnel lifecycle and refresh, owner: ops-bridge} 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 063ef48..ba5a17b 100644 --- a/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md +++ b/workplans/NK-WP-0044-runbook-packs-for-runbook-tutorials.md @@ -53,7 +53,10 @@ priority: high state_hub_task_id: "ba1a41a7-b40d-5312-851e-93c613ac9b27" ``` -`docs/tutorials/openbao-operating-path.md` and `protected-system-flex-auth.md` become +**Ported to v0.2 on 2026-09-29 (RBT-WP-0009-T06)**: all three NetKingdom packs now use stages, actors, datapoints with dimensions, gates, +idempotency and framing; the OpenBao pack models "validate a real ceremony record" as alternatives recommended by a datapoint (there is no +skip in v0.2); the live flex-auth steps are `auto_confirm: manual-only` so auto-confirm never mints tokens on its own. All 13 cluster-free +checks were re-run through the engine and pass. `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 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 @@ -79,14 +82,14 @@ checkout (`RTUT_HOME`, default `~/runbook-tutorials`). id: NK-WP-0044-T05 status: wait needs_human: true -depends_on: [RBT-WP-0004] -blocking_reason: "Blocked on RBT-WP-0004 (UI); Bernd runs the packs and the engine records outcomes." +blocking_reason: "Bernd runs the three v0.2 packs in the UI against the cluster; the engine records the outcomes." priority: high state_hub_task_id: "2d99180e-25ff-51db-8208-b0320c8ec7e3" ``` -Blocked on `RBT-WP-0004` (UI). Bernd runs the SSH, OpenBao and flex-auth packs in the -UI; the engine records outcomes, and NK-WP-0009 T03-T05 close on those receipts. +The UI, project runs and maturity are ready (RBT-WP-0004 and RBT-WP-0008 are finished). Bernd runs the SSH, OpenBao and flex-auth packs in the UI, +ideally inside a rehearsal project so the clean runs count as maturity evidence; the engine records outcomes, and NK-WP-0009 T03-T05 close on +those receipts. Start with `rtut check .` and open the packs from the runbook list. ## Retire the markdown verifier @@ -102,7 +105,21 @@ state_hub_task_id: "1b747058-d4df-5f5c-bd63-e85d46dd094e" Once the tutorials are packs, `tools/tutorial-verify` and `make tutorials-verify` are replaced by `runbooks-validate`, and `docs/tutorials/` becomes a pointer. +## Port the seven console runbooks + +```task +id: NK-WP-0044-T07 +status: wait +needs_human: true +blocking_reason: "Content work, not scheduled: needs a decision to start. The seven runbooks map without gaps (runbook-tutorials/docs/console-mapping.md)." +priority: low +``` + +The security bootstrap console's seven runbooks (privacyIDEA realm repair, key material compromised, generate new unseal keys, emergency lock-down, +restore drill, token revocation, user lifecycle dry-run) can become native v0.2 packs. They are linear procedures with 23 command blocks and need +none of the gaps G1 to G7 in `runbook-tutorials/docs/console-mapping.md`. The console itself stays a permanent legacy pack. + ## Acceptance Criteria - The console still works and is launched by the engine without changes to it. -- Every NetKingdom pack validates; each is honestly labelled exercised or unexercised. +- Every NetKingdom pack validates; maturity is derived from run history, not declared.