Port the NetKingdom packs to runbook-pack/v0.2 (ssh, openbao, flex-auth); update NK-WP-0044
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
parent
504c0a81fb
commit
90112f3ca7
4 changed files with 698 additions and 315 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue