NK-WP-0056-T01 done: Bounded Execution Admission v0.1 (proposed)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Standard, schema and validator for the founder's admission of one bounded
executor run: exact plan by digest, window by risk class, SI MODE @ SCOPE
ceiling with least-privilege coverage, blast radius by consumer, named
source-pinned checks, revert per mutating step, budget, single use, and the
NK-WP-0040 receipt. check-approval verifies a carrier record (Approval
Engine or founder SSH signature): aal2 human approver, not the executor,
consumed at most once in the window. Examples express the bind split;
step 10 is refused as human-intrinsic. 22 tests; make targets; tool index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 994874@bnt-lap001
Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
This commit is contained in:
tegwick 2026-10-31 13:55:18 +01:00
parent 27b4ea6933
commit 989d5d4f5d
15 changed files with 2237 additions and 2 deletions

View file

@ -203,6 +203,12 @@ service-identity-test: ## Run service-identity schema/validator tests (NK-WP-005
service-identity-check: ## Evaluate the checked-in service identity declarations (read-only, offline) service-identity-check: ## Evaluate the checked-in service identity declarations (read-only, offline)
python3 tools/service-identity/validate.py tools/service-identity/declarations/*.yaml python3 tools/service-identity/validate.py tools/service-identity/declarations/*.yaml
bounded-execution-admission-test: ## Run bounded-execution admission validator tests (NK-WP-0056-T01)
python3 -m pytest tools/bounded-execution-admission/tests
bounded-execution-admission-check: ## Validate the example admissions (read-only, offline)
python3 tools/bounded-execution-admission/validate.py validate examples/bounded-execution-admission/*.yaml
bind-split-test: ## LLDAP bind-split helper tests on a throwaway pinned LLDAP (NK-WP-0054-T05; needs docker) bind-split-test: ## LLDAP bind-split helper tests on a throwaway pinned LLDAP (NK-WP-0054-T05; needs docker)
uv run --with pytest --with ldap3 --with pyyaml python -m pytest sso-mfa/k8s/lldap/test_bind_split.py sso-mfa/k8s/lldap/test_identity_provisioner_reconcile.py uv run --with pytest --with ldap3 --with pyyaml python -m pytest sso-mfa/k8s/lldap/test_bind_split.py sso-mfa/k8s/lldap/test_identity_provisioner_reconcile.py
@ -384,6 +390,7 @@ security-bootstrap-ui: security-bootstrap-metadata-init ## Serve local custody a
openbao-init-unseal openbao-init-unseal-dry-run \ openbao-init-unseal openbao-init-unseal-dry-run \
iam-profile-conformance-test playbook-contract-test posture-feedback-test \ iam-profile-conformance-test playbook-contract-test posture-feedback-test \
custody-record-test service-identity-test service-identity-check security-tools \ custody-record-test service-identity-test service-identity-check security-tools \
bounded-execution-admission-test bounded-execution-admission-check \
emission-cadence-profile-test \ emission-cadence-profile-test \
posture-feedback-reference security-scenario-compose-lightweight \ posture-feedback-reference security-scenario-compose-lightweight \
security-bootstrap-console-test security-bootstrap-scripts-syntax \ security-bootstrap-console-test security-bootstrap-scripts-syntax \

View file

@ -340,7 +340,7 @@
| task | NK-WP-0055-T14 | todo | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | task | NK-WP-0055-T14 | todo | — | workplans/NK-WP-0055-security-platform-production-readiness.md |
| task | NK-WP-0055-T15 | done | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | task | NK-WP-0055-T15 | done | — | workplans/NK-WP-0055-security-platform-production-readiness.md |
| task | NK-WP-0055-T16 | wait | — | workplans/NK-WP-0055-security-platform-production-readiness.md | | task | NK-WP-0055-T16 | wait | — | workplans/NK-WP-0055-security-platform-production-readiness.md |
| task | NK-WP-0056-T01 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T01 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T02 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T02 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T03 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T03 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
| task | NK-WP-0056-T04 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T04 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |

View file

@ -0,0 +1,582 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://netkingdom.io/schemas/bounded-execution-admission_v0.1.schema.json",
"title": "NetKingdom Bounded Execution Admission v0.1",
"description": "An immutable request that a person admits, by digest, for one bounded executor run. Structure only; the semantic rules (BEA-1..BEA-14) are in tools/bounded-execution-admission/validate.py and the standard.",
"type": "object",
"additionalProperties": false,
"required": [
"apiVersion",
"kind",
"spec"
],
"properties": {
"apiVersion": {
"const": "netkingdom.io/bounded-execution-admission/v0.1"
},
"kind": {
"const": "BoundedExecutionAdmission"
},
"spec": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"title",
"purpose",
"risk_class",
"window",
"approval",
"executor",
"scope_ceiling",
"blast_radius",
"trust_requires",
"plan",
"plan_digest",
"checks",
"stop_and_revert",
"budget",
"consumption",
"receipt",
"not_authorized"
],
"properties": {
"id": {
"type": "string",
"pattern": "^BEA-[0-9]{4}-[0-9]{4}$"
},
"title": {
"type": "string",
"minLength": 1
},
"purpose": {
"type": "string",
"minLength": 1
},
"work_ref": {
"type": "string"
},
"risk_class": {
"enum": [
"low",
"medium",
"high"
]
},
"window": {
"type": "object",
"additionalProperties": false,
"required": [
"not_before",
"not_after"
],
"properties": {
"not_before": {
"type": "string",
"format": "date-time"
},
"not_after": {
"type": "string",
"format": "date-time"
}
}
},
"approval": {
"type": "object",
"additionalProperties": false,
"required": [
"approvers",
"principal_type",
"assurance",
"carriers"
],
"properties": {
"approvers": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
},
"principal_type": {
"const": "HUMAN"
},
"assurance": {
"const": "aal2"
},
"carriers": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"enum": [
"approval-engine",
"founder-ssh-signature"
]
}
}
}
},
"executor": {
"type": "object",
"additionalProperties": false,
"required": [
"identity",
"principal_type",
"runtime"
],
"properties": {
"identity": {
"type": "string",
"minLength": 1
},
"principal_type": {
"enum": [
"WORKLOAD",
"AGENT",
"SERVICE",
"HUMAN"
]
},
"runtime": {
"type": "string",
"minLength": 1
},
"harness_launched": {
"type": "boolean"
}
}
},
"scope_ceiling": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/grant"
}
},
"blast_radius": {
"type": "object",
"additionalProperties": false,
"required": [
"unit",
"consumers",
"max_concurrent"
],
"properties": {
"unit": {
"enum": [
"consumer",
"account",
"host",
"tenant"
]
},
"consumers": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"type": "string",
"minLength": 1
}
},
"max_concurrent": {
"type": "integer",
"minimum": 1
}
}
},
"trust_requires": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Playbook Capability Contract trust states the executor must find satisfied before the first step.",
"items": {
"enum": [
"bare_host_trust",
"cluster_trust",
"bootstrap_secret_trust",
"bootstrap_identity_trust",
"runtime_secret_trust",
"runtime_identity_trust",
"runtime_authorization_trust",
"tenant_onboarding_trust"
]
}
},
"plan": {
"type": "object",
"additionalProperties": false,
"required": [
"source",
"steps"
],
"properties": {
"source": {
"type": "object",
"additionalProperties": false,
"required": [
"repo",
"commit",
"files"
],
"properties": {
"repo": {
"type": "string"
},
"commit": {
"type": "string",
"pattern": "^[0-9a-f]{7,40}$"
},
"files": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/pinned_file"
}
}
}
},
"steps": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/step"
}
},
"revert_steps": {
"type": "array",
"items": {
"$ref": "#/$defs/step"
}
}
}
},
"plan_digest": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"checks": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/check"
}
},
"stop_and_revert": {
"type": "object",
"additionalProperties": false,
"required": [
"on_check_failure",
"on_budget_exhausted",
"on_unknown",
"on_revert_failure"
],
"properties": {
"on_check_failure": {
"enum": [
"revert",
"stop"
]
},
"on_budget_exhausted": {
"enum": [
"revert",
"stop"
]
},
"on_unknown": {
"const": "stop"
},
"on_revert_failure": {
"const": "stop-and-alert"
},
"alert": {
"type": "string"
}
}
},
"budget": {
"type": "object",
"additionalProperties": false,
"required": [
"wall_time_seconds",
"attempts",
"cost"
],
"properties": {
"wall_time_seconds": {
"type": "integer",
"minimum": 1
},
"attempts": {
"type": "integer",
"minimum": 1
},
"cost": {
"type": "object",
"additionalProperties": false,
"required": [
"compute_cpu_seconds",
"egress_bytes",
"currency",
"amount"
],
"properties": {
"compute_cpu_seconds": {
"type": "integer",
"minimum": 0
},
"egress_bytes": {
"type": "integer",
"minimum": 0
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"amount": {
"type": "number",
"minimum": 0
}
}
}
}
},
"consumption": {
"type": "object",
"additionalProperties": false,
"required": [
"single_use",
"consume_before_first_step"
],
"properties": {
"single_use": {
"const": true
},
"consume_before_first_step": {
"const": true
}
}
},
"receipt": {
"type": "object",
"additionalProperties": false,
"required": [
"schema",
"emit_to",
"retain"
],
"properties": {
"schema": {
"const": "netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt"
},
"emit_to": {
"type": "string",
"minLength": 1
},
"retain": {
"const": "by-hash-no-secret-values"
},
"independent_verifier": {
"type": "string"
}
}
},
"not_authorized": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}
}
}
},
"$defs": {
"grant": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"scope",
"environments"
],
"properties": {
"mode": {
"enum": [
"USE",
"OBSERVE",
"OPERATE",
"ADMINISTER",
"GOVERN",
"CONSTRUCT",
"SOVEREIGN"
]
},
"scope": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$"
},
"environments": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {
"enum": [
"DEVELOPMENT",
"TEST",
"STAGING",
"PRODUCTION",
"RECOVERY"
]
}
}
}
},
"pinned_file": {
"type": "object",
"additionalProperties": false,
"required": [
"path",
"sha256"
],
"properties": {
"path": {
"type": "string",
"minLength": 1
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
}
}
},
"step": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"operation",
"target",
"consumer",
"class",
"mutates",
"requires"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"operation": {
"type": "string",
"minLength": 1
},
"target": {
"type": "string",
"minLength": 1
},
"consumer": {
"type": "string",
"minLength": 1
},
"class": {
"enum": [
"delegable",
"root-class",
"human-intrinsic"
]
},
"mutates": {
"type": "boolean"
},
"requires": {
"$ref": "#/$defs/grant_need"
},
"checks_after": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"revert": {
"type": "string"
},
"irreversible": {
"type": "boolean"
}
}
},
"grant_need": {
"type": "object",
"additionalProperties": false,
"required": [
"mode",
"scope"
],
"properties": {
"mode": {
"enum": [
"USE",
"OBSERVE",
"OPERATE",
"ADMINISTER",
"GOVERN",
"CONSTRUCT",
"SOVEREIGN"
]
},
"scope": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$"
}
}
},
"check": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"description",
"ref",
"sha256",
"timeout_seconds",
"expect"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"description": {
"type": "string",
"minLength": 1
},
"ref": {
"type": "string",
"minLength": 1
},
"sha256": {
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"timeout_seconds": {
"type": "integer",
"minimum": 1
},
"settle_seconds": {
"type": "integer",
"minimum": 0
},
"expect": {
"const": "pass"
}
}
}
}
}

View file

@ -0,0 +1,232 @@
---
id: netkingdom-bounded-execution-admission-v0.1
type: standard
title: "NetKingdom Bounded Execution Admission v0.1"
domain: netkingdom
status: proposed
version: "0.1"
owner: net-kingdom
created: "2026-10-31"
updated: "2026-10-31"
last_reviewed: "2026-10-31"
review_interval: 3m
scope: accountable-automation
schema:
- canon/schemas/bounded-execution-admission_v0.1.schema.json
validator:
- tools/bounded-execution-admission/validate.py
examples:
- examples/bounded-execution-admission/
related:
- canon/standards/service-identity-privilege_v0.1.md
- canon/standards/playbook-capability-contract_v0.2.md
- workplans/NK-WP-0056-bounded-executor-path.md
- workplans/NK-WP-0040-execution-attribution-receipt.md
---
# NetKingdom Bounded Execution Admission v0.1
## 0. Status
Proposed (NK-WP-0056-T01). Nothing here is enforced by a running service yet. The executor runtime
is T02/T03, budget and kill-switch enforcement is T07, and the first live use is the T06 pilot. A
document that passes the validator is a well-formed **request**. It is an **admission** only together
with a carrier record that a person approved (section 5).
## 1. Purpose
SI-15 names the golden path *prepare → admit → execute → verify → retain*, and allows "a bounded
executor under the admission" at step 3. This standard defines what that admission is. The founder
approves, at `aal2`:
- an exact plan, by digest;
- a validity window;
- a scope ceiling in SI `MODE @ SCOPE` terms;
- a blast-radius rule;
- a stop-and-revert policy tied to named machine checks;
- a risk class;
- an expense budget.
The executor consumes the admission once. Top-level decisions and guardrails stay with the person,
and execution below them is delegated, bounded and reconstructable.
Out of scope: who runs the executor (T02), how a carrier stores approvals (Approval Engine owns that),
and the receipt's emission and custody (NK-WP-0040-T02).
## 2. Vocabulary
| Term | Meaning |
| --- | --- |
| Admission request | The immutable document defined here (`BoundedExecutionAdmission`), identified by `spec.id` and by its digest |
| Carrier | The system that records a person's approval of one request and its single consumption: Approval Engine, or a founder SSH signature (NK-WP-0049 practice) |
| Admission | A request plus a carrier record that passes BEA-C1..C7 |
| Executor | The non-human identity that runs the plan: a `WORKLOAD`, `SERVICE` or harness-launched `AGENT` (SI-14) |
| Step class | `delegable` (the executor may run it under an admission), `root-class` (the founder admits each run; high risk only), `human-intrinsic` (never in an admission: for example a person's own password) |
| Check | A named, source-pinned machine check that must pass after a mutating step |
| Consumer | The unit the blast radius counts: a service whose behaviour a step can change |
## 3. The admission request
A YAML or JSON document validated by `canon/schemas/bounded-execution-admission_v0.1.schema.json`:
| Field | Meaning |
| --- | --- |
| `spec.id`, `title`, `purpose`, `work_ref` | Identity and intent. `id` is `BEA-YYYY-NNNN` |
| `risk_class` | `low`, `medium` or `high`. Bounds the window, the attempts, the ceiling and the step classes |
| `window.not_before` / `not_after` | When the admission can be consumed. At most 72 h (low), 24 h (medium) or 4 h (high) |
| `approval` | Who may approve (`approvers`, `HUMAN`, `aal2`) and through which carriers |
| `executor` | The executing identity, its principal type and runtime |
| `scope_ceiling` | Grants `{mode, scope, environments}` that bound every step. Scopes are `/`-separated paths, and a grant covers its own scope and anything below it |
| `blast_radius` | `unit`, the `consumers` the plan may touch, and `max_concurrent` |
| `trust_requires` | Playbook Capability Contract trust states the executor must find satisfied before the first step |
| `plan.source` | Repository, commit, and the sha256 of every file the run executes or checks with |
| `plan.steps`, `plan.revert_steps` | Ordered steps: `operation`, `target`, `consumer`, `class`, `mutates`, `requires` (the grant it needs), `checks_after`, `revert` or `irreversible` |
| `plan_digest` | `sha256:` over the canonical JSON of `plan` (sorted keys, no whitespace, UTF-8) |
| `checks` | Named checks: `ref` (a pinned file), `sha256`, `timeout_seconds`, optional `settle_seconds`, `expect: pass` |
| `stop_and_revert` | `on_check_failure` and `on_budget_exhausted` (`revert` or `stop`); `on_unknown: stop`; `on_revert_failure: stop-and-alert` |
| `budget` | `wall_time_seconds`, `attempts`, and `cost` (CPU seconds, egress bytes, currency, amount) |
| `consumption` | `single_use: true`, `consume_before_first_step: true` |
| `receipt` | The NK-WP-0040 execution-attribution receipt, where it goes, and that it is retained by hash with no secret values |
| `not_authorized` | What this admission explicitly does not cover |
`settle_seconds` exists because of the T08 finding: privacyIDEA's resolver cache hides a broken bind
for up to 120 s, so a check run sooner proves nothing.
## 4. Rules (validator: `validate`)
| Rule | Requirement |
| --- | --- |
| BEA-1 | The document matches the schema (`HUMAN` approvers at `aal2`, single use, `on_unknown: stop`) |
| BEA-2 | `plan_digest` equals the digest of `plan`: the person admits exactly this plan |
| BEA-3 | The window is positive and no longer than the risk class allows |
| BEA-4 | The executor is not `HUMAN` and not an approver. An `AGENT` must be harness-launched (SI-14) |
| BEA-5 | No `SOVEREIGN` or `GOVERN` in the ceiling. No `CONSTRUCT` in `PRODUCTION`. `ADMINISTER` needs `max_concurrent: 1`; at a whole-service scope (no `/`) it needs `high` risk and only `root-class` steps |
| BEA-6 | Least privilege: every step and revert step is covered by a ceiling grant of the same mode, and every ceiling grant covers at least one step |
| BEA-7 | No `human-intrinsic` step. A `root-class` step needs `high` risk |
| BEA-8 | Every step's consumer is in the blast radius. `medium` and `high` risk change one consumer at a time |
| BEA-9 | Every mutating step names checks. Every check is used and pinned: its `ref` and `sha256` appear in `plan.source.files` |
| BEA-10 | Every mutating step has a revert step, or is `irreversible` at `high` risk. Revert steps mutate and do not nest. Step ids are unique |
| BEA-11 | A revertable plan reverts on check failure. A plan with nothing to revert stops |
| BEA-12 | The wall time fits the window, the checks (timeouts plus settle times) fit the wall time, and attempts are at most 3 (1 at `high` risk) |
| BEA-13 | The executed source is pinned by file and commit |
| BEA-14 | No secret-shaped value anywhere in the document |
Findings name the rule and a location, never a value.
## 5. Carrier and consumption (validator: `check-approval`)
A carrier record (`netkingdom.io/bounded-execution-approval/v0.1`) states the carrier, its approval
reference, the admission id and digest, the approver (name, principal type, assurance), the approval
time, and the consumptions.
The **admission digest** is `sha256:` over the canonical JSON of the whole request, including
`plan_digest`. That digest is what a carrier binds.
| Rule | Requirement |
| --- | --- |
| BEA-C1 | The carrier is one the request allows, and the record has the expected shape |
| BEA-C2 | The record names this admission's id and digest |
| BEA-C3 | The approver is a listed `HUMAN` approver and not the executor |
| BEA-C4 | The approval was made at `aal2` |
| BEA-C5 | The approval was made before the window closed |
| BEA-C6 | Consumed at most once |
| BEA-C7 | The consumption happened in the window, after approval, by the executor identity |
**Approval Engine (preferred carrier).** Approval Engine already stores an approval that binds action,
actor, principal, purpose and target by digest, carries a validity window, and is consumed exactly
once (`POST /v1/approvals/{id}/consume`), read back through `GET /v1/approvals/{id}`. The proposed
mapping, which is for agreement with Approval Engine and Access under T02, not decided here:
- `action: bounded-execution`;
- `actor`: the executor identity;
- `principal`: the approver;
- `purpose`: `spec.purpose`;
- `target: {admission_id, admission_digest}`;
- validity equal to `window`;
- the executor consumes before its first step (`consume_before_first_step`).
Approval Engine's own contract notes that `consume` does not check that the request digest is
equivalent to the decision. An independent mediator must therefore fetch the approval and compare
`target.admission_digest` itself. A `consumed: true` returned by the caller is not proof.
**Founder SSH signature (transitional carrier).** This is the NK-WP-0049 practice:
- the founder signs the canonical request with `ssh-keygen -Y sign`, namespace
`netkingdom-bounded-execution`;
- the signature is verified against the repository's `allowed_signers`;
- `aal2` is established by the signing key's custody, not by a token.
Single use is then only as strong as the executor's own consumption ledger. That is acceptable for
the transition and is recorded as such in the receipt.
## 6. Mapping
**SI-15 golden path.**
| Golden-path step | What it means here |
| --- | --- |
| Prepare | `plan.source` (source-reviewed, pinned files), tested on synthetic fixtures. For bind changes, NK-WP-0056-T08's synthetic stack |
| Admit | Carrier approval of the admission digest at `aal2` |
| Execute | The executor, inside `scope_ceiling`, `blast_radius`, `window` and `budget`, consuming once |
| Verify | The named checks during the run. Afterwards, an independent verifier checks the carrier record (BEA-C1..C7) and the receipt's source hashes |
| Retain | The receipt, kept by hash with no secret values |
**Playbook Capability Contract.** `trust_requires` uses the contract's trust-state vocabulary. The
executor refuses to start unless each state is satisfied, using the readiness checks of the
capability declarations it runs. A step's `operation` should name a declared capability operation
once Railiance declarations exist for it.
**Execution-attribution receipt (Playbook Capability Contract v0.2, NK-WP-0040).** One receipt per run:
| Receipt field | Filled from |
| --- | --- |
| `initiator` | The carrier's approver (the founder). The person authorized the run; the executor ran it |
| `delegation` | Absent unless the approver acted through an agent |
| `executing_workload`, `runtime_principal` | `executor.identity`, `executor.runtime` |
| `run_id` | New per run |
| `source_commit` | `plan.source.commit` |
| `artifact_digest` | `plan_digest` |
| `target`, `action` | Per step |
| `decision_refs` | `spec.id`, the admission digest and the carrier's approval reference |
| `time_interval` | Measured |
The receipt adds the outcome per step, check results, whether a revert ran, and the budget used. It
never holds a secret value.
## 7. The bind split as admissions
NK-WP-0056 tagged each step of the 2026-10-30 LLDAP bind split. The examples express them:
| Runbook step | Class | Example |
| --- | --- | --- |
| 2, 4, 10 (verify) | delegable, read-only | `bind-split-read-only-verify.yaml`: low risk, `OBSERVE` only, nothing to revert |
| 3 create `svc-*` accounts | root-class | `bind-split-create-service-accounts.yaml`: high, whole-directory `ADMINISTER` (no narrower LLDAP grant, SI-X6), revert deletes what was created |
| 5–8 switch one consumer | delegable | `bind-rotation-svc-authelia.yaml`: medium, one consumer, a synthetic-journey check after the restart, a revert per step. This is the T06 pilot shape |
| 9 change LLDAP's super-admin | root-class | `bind-split-switch-super-admin.yaml`: high, one attempt, `settle_seconds: 125` on the journey check (privacyIDEA cache) |
| 10 `admin`'s own password; seal `ld-admin` | human-intrinsic | Refused: `tools/bounded-execution-admission/fixtures/invalid/human-intrinsic-step.yaml` (BEA-7) |
The examples are illustrative. They pin real files at a real commit, but the executor and its
operations do not exist yet.
## 8. Conformance
```bash
make bounded-execution-admission-test # validator tests
make bounded-execution-admission-check # validate the examples
python3 tools/bounded-execution-admission/validate.py check-approval <admission> <carrier-record>
python3 tools/bounded-execution-admission/validate.py digest <admission>
```
Agents may run these: they are offline and read-only. Writing an admission is preparation, and
approving one is the founder's act.
## 9. Open items
- T02: the carrier mapping with Approval Engine and Access (action name, actor and representation,
digest equivalence, consume scope for the executor), and the executor runtime owner.
- T03: the executor identity and its grants, matching the ceilings in the examples.
- T07: enforcement of budget and attempts at run time, the founder's kill switch, and aggregate
review through posture feedback (NK-WP-0031).
- NK-WP-0040-T02: receipt emission, custody and schema in audit-core.
- Recurring admissions (one approval for repeated runs of the same plan) are deliberately excluded
from v0.1: every admission is single use.

View file

@ -0,0 +1,197 @@
# NK-WP-0056-T01 example: the bind split (NK-WP-0054) as admissions. Illustrative: the executor
# (T03) and its operations do not exist yet; files are pinned at the commit named in plan.source.
apiVersion: netkingdom.io/bounded-execution-admission/v0.1
kind: BoundedExecutionAdmission
spec:
id: BEA-2026-0001
title: Rotate the svc-authelia directory bind (bind-split steps 5-8, one consumer)
purpose: 'Pilot shape for NK-WP-0056-T06: generate a new bind password into OpenBao, set it in LLDAP, let ESO deliver it,
restart Authelia, prove sign-in with the synthetic journey; revert on any failed check.'
work_ref: NK-WP-0056-T01 example
risk_class: medium
window:
not_before: '2026-11-03T09:00:00Z'
not_after: '2026-11-03T11:00:00Z'
approval:
approvers:
- admin
principal_type: HUMAN
assurance: aal2
carriers:
- approval-engine
- founder-ssh-signature
executor:
identity: service:nk-bind-executor
principal_type: WORKLOAD
runtime: k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)
scope_ceiling:
- mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia
environments: &id001
- PRODUCTION
- mode: ADMINISTER
scope: lldap/people/svc-authelia
environments: *id001
- mode: OPERATE
scope: k8s/sso/externalsecret/authelia-secrets
environments: *id001
- mode: OPERATE
scope: k8s/sso/deployment/authelia
environments: *id001
blast_radius:
unit: consumer
consumers:
- authelia
max_concurrent: 1
trust_requires:
- runtime_secret_trust
- runtime_identity_trust
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
files:
- path: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
- path: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
steps:
- id: generate
operation: openbao.kv.write-next-version
target: openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia
checks_after:
- kv-advanced
revert: restore-kv
- id: set-directory
operation: lldap.user.set-password-from-kv
target: lldap:svc-authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: ADMINISTER
scope: lldap/people/svc-authelia
checks_after:
- bind-signs-in
revert: restore-directory
- id: deliver
operation: eso.force-sync
target: k8s:sso/externalsecret/authelia-secrets
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/externalsecret/authelia-secrets
checks_after:
- secret-equals-kv
revert: redeliver
- id: restart
operation: k8s.rollout-restart
target: k8s:sso/deployment/authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/authelia
checks_after:
- synthetic-journey
revert: restart-again
revert_steps:
- id: restore-kv
operation: openbao.kv.write-previous-value-as-next-version
target: openbao:platform/workloads/net-kingdom/directory-binds/svc-authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia
- id: restore-directory
operation: lldap.user.set-password-from-kv
target: lldap:svc-authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: ADMINISTER
scope: lldap/people/svc-authelia
- id: redeliver
operation: eso.force-sync
target: k8s:sso/externalsecret/authelia-secrets
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/externalsecret/authelia-secrets
- id: restart-again
operation: k8s.rollout-restart
target: k8s:sso/deployment/authelia
consumer: authelia
class: delegable
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/authelia
plan_digest: sha256:33129ab753da95f549fb27cfb5ba622e36fcba3c197f28b08fe3d4f9ff1d64a4
checks:
- id: kv-advanced
description: KV svc-authelia has exactly one new version, CAS on the previous one
ref: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
timeout_seconds: 30
expect: pass
- id: bind-signs-in
description: the new KV value signs in to LLDAP as svc-authelia
ref: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
timeout_seconds: 30
expect: pass
- id: secret-equals-kv
description: 'bind-lane verify: ESO Ready and the delivered Secret equals KV, compared in memory'
ref: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
timeout_seconds: 120
expect: pass
- id: synthetic-journey
description: syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2
ref: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
timeout_seconds: 90
expect: pass
stop_and_revert:
on_check_failure: revert
on_budget_exhausted: revert
on_unknown: stop
on_revert_failure: stop-and-alert
alert: founder via State Hub message and audit-core
budget:
wall_time_seconds: 1200
attempts: 2
cost:
compute_cpu_seconds: 120
egress_bytes: 5000000
currency: EUR
amount: 0.05
consumption:
single_use: true
consume_before_first_step: true
receipt:
schema: netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt
emit_to: audit-core
retain: by-hash-no-secret-values
independent_verifier: tools/bounded-execution-admission/validate.py check-approval + receipt source hashes
not_authorized:
- any bind other than svc-authelia
- any LLDAP group change
- changing LLDAP's configured super-admin
- reading or changing any human account
- any change outside the sso namespace

View file

@ -0,0 +1,135 @@
# NK-WP-0056-T01 example: the bind split (NK-WP-0054) as admissions. Illustrative: the executor
# (T03) and its operations do not exist yet; files are pinned at the commit named in plan.source.
apiVersion: netkingdom.io/bounded-execution-admission/v0.1
kind: BoundedExecutionAdmission
spec:
id: BEA-2026-0002
title: Create the service bind accounts (bind-split step 3, root-class)
purpose: 'Create svc-authelia, svc-keycape and svc-privacyidea in LLDAP as read-only binds. Needs lldap_admin: LLDAP has
no narrower grant that can create users (SI-X6), so the founder admits this run and the executor runs it.'
work_ref: NK-WP-0056-T01 example
risk_class: high
window:
not_before: '2026-11-03T09:00:00Z'
not_after: '2026-11-03T12:00:00Z'
approval:
approvers:
- admin
principal_type: HUMAN
assurance: aal2
carriers:
- approval-engine
- founder-ssh-signature
executor:
identity: service:nk-bind-executor
principal_type: WORKLOAD
runtime: k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)
scope_ceiling:
- mode: ADMINISTER
scope: lldap
environments: &id001
- PRODUCTION
- mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds
environments: *id001
blast_radius:
unit: account
consumers:
- lldap
max_concurrent: 1
trust_requires:
- runtime_secret_trust
- runtime_identity_trust
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
- path: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
steps:
- id: create-accounts
operation: lldap.user.create-read-only-binds
target: lldap:svc-authelia,svc-keycape,svc-privacyidea
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
checks_after:
- accounts-read-only
revert: delete-accounts
- id: hold-passwords
operation: openbao.kv.write-cas0
target: openbao:platform/workloads/net-kingdom/directory-binds
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds
checks_after:
- accounts-sign-in
revert: destroy-kv
revert_steps:
- id: delete-accounts
operation: lldap.user.delete-created
target: lldap:svc-authelia,svc-keycape,svc-privacyidea
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
- id: destroy-kv
operation: openbao.kv.destroy-created
target: openbao:platform/workloads/net-kingdom/directory-binds
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds
plan_digest: sha256:94910771c288437dbaeeba80e32a4f61ef1cdb7d438e7352d21a5e10fe92e77e
checks:
- id: accounts-read-only
description: each account exists in lldap_strict_readonly only and cannot write
ref: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
timeout_seconds: 60
expect: pass
- id: accounts-sign-in
description: each KV value signs in to LLDAP as its account
ref: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
timeout_seconds: 60
expect: pass
stop_and_revert:
on_check_failure: revert
on_budget_exhausted: revert
on_unknown: stop
on_revert_failure: stop-and-alert
alert: founder via State Hub message
budget:
wall_time_seconds: 900
attempts: 1
cost:
compute_cpu_seconds: 60
egress_bytes: 1000000
currency: EUR
amount: 0.02
consumption:
single_use: true
consume_before_first_step: true
receipt:
schema: netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt
emit_to: audit-core
retain: by-hash-no-secret-values
independent_verifier: tools/bounded-execution-admission/validate.py check-approval + receipt source hashes
not_authorized:
- switching any consumer to the new accounts
- any human account
- changing LLDAP's configured super-admin

View file

@ -0,0 +1,106 @@
# NK-WP-0056-T01 example: the bind split (NK-WP-0054) as admissions. Illustrative: the executor
# (T03) and its operations do not exist yet; files are pinned at the commit named in plan.source.
apiVersion: netkingdom.io/bounded-execution-admission/v0.1
kind: BoundedExecutionAdmission
spec:
id: BEA-2026-0004
title: Read-only verification of the directory split (bind-split steps 2, 4 and 10 verify)
purpose: 'Plan and verify reads that the founder used to do signed in as admin: account inventory, group membership, bind
identities of the consumers. No mutation, so nothing to revert.'
work_ref: NK-WP-0056-T01 example
risk_class: low
window:
not_before: '2026-11-03T09:00:00Z'
not_after: '2026-11-04T09:00:00Z'
approval:
approvers:
- admin
principal_type: HUMAN
assurance: aal2
carriers:
- approval-engine
- founder-ssh-signature
executor:
identity: service:nk-bind-executor
principal_type: WORKLOAD
runtime: k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)
scope_ceiling:
- mode: OBSERVE
scope: lldap/people
environments: &id001
- PRODUCTION
- mode: OBSERVE
scope: k8s/sso
environments: *id001
blast_radius:
unit: consumer
consumers:
- lldap
- authelia
- keycape
- privacyidea
max_concurrent: 4
trust_requires:
- runtime_secret_trust
- runtime_identity_trust
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
steps:
- id: inventory
operation: lldap.read.users-and-groups
target: lldap:people
consumer: lldap
class: delegable
mutates: false
requires:
mode: OBSERVE
scope: lldap/people
checks_after:
- split-shape
- id: consumer-binds
operation: k8s.read.bind-identities
target: k8s:sso
consumer: authelia
class: delegable
mutates: false
requires:
mode: OBSERVE
scope: k8s/sso
checks_after:
- split-shape
plan_digest: sha256:e4867e97a9957d543d5a7e265e56678ce22f3bb87bab64022b381dbee3e2610a
checks:
- id: split-shape
description: every consumer binds as its own svc- account; no service binds as admin
ref: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
timeout_seconds: 60
expect: pass
stop_and_revert:
on_check_failure: stop
on_budget_exhausted: stop
on_unknown: stop
on_revert_failure: stop-and-alert
budget:
wall_time_seconds: 300
attempts: 3
cost:
compute_cpu_seconds: 30
egress_bytes: 500000
currency: EUR
amount: 0.01
consumption:
single_use: true
consume_before_first_step: true
receipt:
schema: netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt
emit_to: audit-core
retain: by-hash-no-secret-values
independent_verifier: tools/bounded-execution-admission/validate.py check-approval + receipt source hashes
not_authorized:
- any write to LLDAP, Kubernetes or OpenBao

View file

@ -0,0 +1,136 @@
# NK-WP-0056-T01 example: the bind split (NK-WP-0054) as admissions. Illustrative: the executor
# (T03) and its operations do not exist yet; files are pinned at the commit named in plan.source.
apiVersion: netkingdom.io/bounded-execution-admission/v0.1
kind: BoundedExecutionAdmission
spec:
id: BEA-2026-0003
title: Switch LLDAP's configured super-admin to ld-admin (bind-split step 9, root-class)
purpose: Change LLDAP_LDAP_USER_DN from admin to ld-admin and restart LLDAP; admin keeps its rights through lldap_admin
membership. Changes who administers the directory, so the founder admits the run.
work_ref: NK-WP-0056-T01 example
risk_class: high
window:
not_before: '2026-11-03T09:00:00Z'
not_after: '2026-11-03T11:00:00Z'
approval:
approvers:
- admin
principal_type: HUMAN
assurance: aal2
carriers:
- approval-engine
- founder-ssh-signature
executor:
identity: service:nk-bind-executor
principal_type: WORKLOAD
runtime: k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)
scope_ceiling:
- mode: ADMINISTER
scope: lldap
environments: &id001
- PRODUCTION
- mode: OPERATE
scope: k8s/sso/deployment/lldap
environments: *id001
blast_radius:
unit: consumer
consumers:
- lldap
max_concurrent: 1
trust_requires:
- runtime_secret_trust
- runtime_identity_trust
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
- path: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
steps:
- id: switch-user-dn
operation: lldap.config.set-ldap-user-dn
target: k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
checks_after:
- ld-admin-administers
revert: switch-back
- id: restart-lldap
operation: k8s.rollout-restart
target: k8s:sso/deployment/lldap
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
checks_after:
- synthetic-journey
revert: restart-lldap-again
revert_steps:
- id: switch-back
operation: lldap.config.set-ldap-user-dn
target: k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
- id: restart-lldap-again
operation: k8s.rollout-restart
target: k8s:sso/deployment/lldap
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
plan_digest: sha256:c15793ccaad212eaf24de12a1d13709970be84ee917a994463b68880e905dedf
checks:
- id: ld-admin-administers
description: ld-admin signs in and administers; admin keeps lldap_admin membership
ref: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
timeout_seconds: 60
expect: pass
- id: synthetic-journey
description: 'all three binds still serve sign-in: syn-journey reaches aal2'
ref: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
timeout_seconds: 90
expect: pass
settle_seconds: 125
stop_and_revert:
on_check_failure: revert
on_budget_exhausted: revert
on_unknown: stop
on_revert_failure: stop-and-alert
alert: founder via State Hub message; crisis list item for ld-admin
budget:
wall_time_seconds: 900
attempts: 1
cost:
compute_cpu_seconds: 60
egress_bytes: 1000000
currency: EUR
amount: 0.02
consumption:
single_use: true
consume_before_first_step: true
receipt:
schema: netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt
emit_to: audit-core
retain: by-hash-no-secret-values
independent_verifier: tools/bounded-execution-admission/validate.py check-approval + receipt source hashes
not_authorized:
- changing admin's own password
- sealing ld-admin's password (human-intrinsic, step 10)
- any service bind change

View file

@ -0,0 +1,18 @@
# Bounded Execution Admission validator (NK-WP-0056-T01)
Offline, deterministic checks for `canon/standards/bounded-execution-admission_v0.1.md`.
```bash
python3 tools/bounded-execution-admission/validate.py validate [--as-of T] FILE... # BEA-1..BEA-14
python3 tools/bounded-execution-admission/validate.py check-approval ADMISSION RECORD # BEA-C1..BEA-C7
python3 tools/bounded-execution-admission/validate.py digest FILE # plan and admission digests
python3 tools/bounded-execution-admission/generate_examples.py # re-pin and regenerate examples
make bounded-execution-admission-test bounded-execution-admission-check
```
A valid document is a well-formed request, not an admission: an admission also needs a carrier
record approved by a person (Approval Engine, or a founder SSH signature). Findings name the rule
and a location, never a value. Needs `jsonschema` and `PyYAML`.
Examples (`examples/bounded-execution-admission/`) express the NK-WP-0054 bind split as admissions.
`generate_examples.py` pins real files at the current commit; re-run it when those files change.

View file

@ -0,0 +1,147 @@
apiVersion: netkingdom.io/bounded-execution-admission/v0.1
kind: BoundedExecutionAdmission
spec:
id: BEA-2026-0099
title: Switch LLDAP's configured super-admin to ld-admin (bind-split step 9, root-class)
purpose: Change LLDAP_LDAP_USER_DN from admin to ld-admin and restart LLDAP; admin keeps its rights through lldap_admin
membership. Changes who administers the directory, so the founder admits the run.
work_ref: NK-WP-0056-T01 example
risk_class: high
window:
not_before: '2026-11-03T09:00:00Z'
not_after: '2026-11-03T11:00:00Z'
approval:
approvers:
- admin
principal_type: HUMAN
assurance: aal2
carriers:
- approval-engine
- founder-ssh-signature
executor:
identity: service:nk-bind-executor
principal_type: WORKLOAD
runtime: k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)
scope_ceiling:
- mode: ADMINISTER
scope: lldap
environments:
- PRODUCTION
- mode: OPERATE
scope: k8s/sso/deployment/lldap
environments:
- PRODUCTION
blast_radius:
unit: consumer
consumers:
- lldap
max_concurrent: 1
trust_requires:
- runtime_secret_trust
- runtime_identity_trust
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
- path: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
steps:
- id: switch-user-dn
operation: lldap.config.set-ldap-user-dn
target: k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
checks_after:
- ld-admin-administers
revert: switch-back
- id: restart-lldap
operation: k8s.rollout-restart
target: k8s:sso/deployment/lldap
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
checks_after:
- synthetic-journey
revert: restart-lldap-again
- id: change-admin-password
operation: lldap.user.set-password
target: lldap:admin
consumer: lldap
class: human-intrinsic
mutates: true
requires:
mode: ADMINISTER
scope: lldap
checks_after:
- ld-admin-administers
revert: switch-back
revert_steps:
- id: switch-back
operation: lldap.config.set-ldap-user-dn
target: k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN
consumer: lldap
class: root-class
mutates: true
requires:
mode: ADMINISTER
scope: lldap
- id: restart-lldap-again
operation: k8s.rollout-restart
target: k8s:sso/deployment/lldap
consumer: lldap
class: root-class
mutates: true
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
plan_digest: sha256:eb53d0d611a22d43214f7e4afb450e2a1c858110c9d5330b7d562fc98a1c8fe8
checks:
- id: ld-admin-administers
description: ld-admin signs in and administers; admin keeps lldap_admin membership
ref: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
timeout_seconds: 60
expect: pass
- id: synthetic-journey
description: 'all three binds still serve sign-in: syn-journey reaches aal2'
ref: sso-mfa/k8s/synthetic-journey/journey.py
sha256: c639054597be7647a3ae88ac7da53490a43b38d9f4f95e24cf4e9f54b18f997c
timeout_seconds: 90
expect: pass
settle_seconds: 125
stop_and_revert:
on_check_failure: revert
on_budget_exhausted: revert
on_unknown: stop
on_revert_failure: stop-and-alert
alert: founder via State Hub message; crisis list item for ld-admin
budget:
wall_time_seconds: 900
attempts: 1
cost:
compute_cpu_seconds: 60
egress_bytes: 1000000
currency: EUR
amount: 0.02
consumption:
single_use: true
consume_before_first_step: true
receipt:
schema: netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt
emit_to: audit-core
retain: by-hash-no-secret-values
independent_verifier: tools/bounded-execution-admission/validate.py check-approval + receipt source hashes
not_authorized:
- changing admin's own password
- sealing ld-admin's password (human-intrinsic, step 10)
- any service bind change

View file

@ -0,0 +1,144 @@
"""Regenerate examples/bounded-execution-admission/ and the human-intrinsic invalid fixture (NK-WP-0056-T01).
Pins the named files at the current commit and computes plan_digest. Run after changing those files or the schema.
"""
import hashlib, json, pathlib, subprocess, sys, yaml
ROOT = pathlib.Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools/bounded-execution-admission"))
import validate as V
commit = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "--short=12", "HEAD"], capture_output=True, text=True).stdout.strip()
def pin(*paths): return [{"path": p, "sha256": hashlib.sha256((ROOT / p).read_bytes()).hexdigest()} for p in paths]
def sha(p): return hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
JOURNEY = "sso-mfa/k8s/synthetic-journey/journey.py"
LANE = "tools/service-binds/bind-lane.py"
SPLIT = "sso-mfa/k8s/lldap/bind-split.py"
COMMON = {
"approval": {"approvers": ["admin"], "principal_type": "HUMAN", "assurance": "aal2",
"carriers": ["approval-engine", "founder-ssh-signature"]},
"consumption": {"single_use": True, "consume_before_first_step": True},
"receipt": {"schema": "netkingdom.playbook-capability-contract/v0.2#execution-attribution-receipt",
"emit_to": "audit-core", "retain": "by-hash-no-secret-values",
"independent_verifier": "tools/bounded-execution-admission/validate.py check-approval + receipt source hashes"},
}
EXECUTOR = {"identity": "service:nk-bind-executor", "principal_type": "WORKLOAD",
"runtime": "k8s sso/serviceaccount nk-bind-executor (planned, NK-WP-0056-T03)"}
P = ["PRODUCTION"]
def check(cid, desc, ref, timeout, settle=0):
c = {"id": cid, "description": desc, "ref": ref, "sha256": sha(ref), "timeout_seconds": timeout, "expect": "pass"}
if settle: c["settle_seconds"] = settle
return c
def doc(id_, title, purpose, risk, window, ceiling, radius, files, steps, reverts, checks, policy, budget, not_auth, trust=("runtime_secret_trust", "runtime_identity_trust")):
spec = {"id": id_, "title": title, "purpose": purpose, "work_ref": "NK-WP-0056-T01 example", "risk_class": risk,
"window": window, **{"approval": COMMON["approval"]}, "executor": EXECUTOR, "scope_ceiling": ceiling,
"blast_radius": radius, "trust_requires": list(trust),
"plan": {"source": {"repo": "net-kingdom", "commit": commit, "files": pin(*files)}, "steps": steps,
**({"revert_steps": reverts} if reverts else {})},
"plan_digest": "", "checks": checks, "stop_and_revert": policy, "budget": budget,
"consumption": COMMON["consumption"], "receipt": COMMON["receipt"], "not_authorized": not_auth}
d = {"apiVersion": "netkingdom.io/bounded-execution-admission/v0.1", "kind": "BoundedExecutionAdmission", "spec": spec}
spec["plan_digest"] = V.plan_digest(d)
return d
def step(id_, op, target, consumer, cls, mutates, mode, scope, checks=None, revert=None, irreversible=None):
s = {"id": id_, "operation": op, "target": target, "consumer": consumer, "class": cls, "mutates": mutates,
"requires": {"mode": mode, "scope": scope}}
if checks: s["checks_after"] = checks
if revert: s["revert"] = revert
if irreversible: s["irreversible"] = True
return s
KV = "openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia"
rotation = doc(
"BEA-2026-0001", "Rotate the svc-authelia directory bind (bind-split steps 5-8, one consumer)",
"Pilot shape for NK-WP-0056-T06: generate a new bind password into OpenBao, set it in LLDAP, let ESO deliver it, restart Authelia, prove sign-in with the synthetic journey; revert on any failed check.",
"medium", {"not_before": "2026-11-03T09:00:00Z", "not_after": "2026-11-03T11:00:00Z"},
[{"mode": "OPERATE", "scope": KV, "environments": P},
{"mode": "ADMINISTER", "scope": "lldap/people/svc-authelia", "environments": P},
{"mode": "OPERATE", "scope": "k8s/sso/externalsecret/authelia-secrets", "environments": P},
{"mode": "OPERATE", "scope": "k8s/sso/deployment/authelia", "environments": P}],
{"unit": "consumer", "consumers": ["authelia"], "max_concurrent": 1},
[LANE, JOURNEY],
[step("generate", "openbao.kv.write-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV, ["kv-advanced"], "restore-kv"),
step("set-directory", "lldap.user.set-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "ADMINISTER", "lldap/people/svc-authelia", ["bind-signs-in"], "restore-directory"),
step("deliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["secret-equals-kv"], "redeliver"),
step("restart", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia", ["synthetic-journey"], "restart-again")],
[step("restore-kv", "openbao.kv.write-previous-value-as-next-version", "openbao:" + KV[len("openbao/"):], "authelia", "delegable", True, "OPERATE", KV),
step("restore-directory", "lldap.user.set-password-from-kv", "lldap:svc-authelia", "authelia", "delegable", True, "ADMINISTER", "lldap/people/svc-authelia"),
step("redeliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets"),
step("restart-again", "k8s.rollout-restart", "k8s:sso/deployment/authelia", "authelia", "delegable", True, "OPERATE", "k8s/sso/deployment/authelia")],
[check("kv-advanced", "KV svc-authelia has exactly one new version, CAS on the previous one", LANE, 30),
check("bind-signs-in", "the new KV value signs in to LLDAP as svc-authelia", LANE, 30),
check("secret-equals-kv", "bind-lane verify: ESO Ready and the delivered Secret equals KV, compared in memory", LANE, 120),
check("synthetic-journey", "syn-journey signs in through KeyCape, Authelia and privacyIDEA at aal2", JOURNEY, 90)],
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
"on_revert_failure": "stop-and-alert", "alert": "founder via State Hub message and audit-core"},
{"wall_time_seconds": 1200, "attempts": 2, "cost": {"compute_cpu_seconds": 120, "egress_bytes": 5000000, "currency": "EUR", "amount": 0.05}},
["any bind other than svc-authelia", "any LLDAP group change", "changing LLDAP's configured super-admin",
"reading or changing any human account", "any change outside the sso namespace"])
create = doc(
"BEA-2026-0002", "Create the service bind accounts (bind-split step 3, root-class)",
"Create svc-authelia, svc-keycape and svc-privacyidea in LLDAP as read-only binds. Needs lldap_admin: LLDAP has no narrower grant that can create users (SI-X6), so the founder admits this run and the executor runs it.",
"high", {"not_before": "2026-11-03T09:00:00Z", "not_after": "2026-11-03T12:00:00Z"},
[{"mode": "ADMINISTER", "scope": "lldap", "environments": P},
{"mode": "OPERATE", "scope": "openbao/platform/workloads/net-kingdom/directory-binds", "environments": P}],
{"unit": "account", "consumers": ["lldap"], "max_concurrent": 1},
[SPLIT, LANE],
[step("create-accounts", "lldap.user.create-read-only-binds", "lldap:svc-authelia,svc-keycape,svc-privacyidea", "lldap", "root-class", True, "ADMINISTER", "lldap", ["accounts-read-only"], "delete-accounts"),
step("hold-passwords", "openbao.kv.write-cas0", "openbao:platform/workloads/net-kingdom/directory-binds", "lldap", "root-class", True, "OPERATE", "openbao/platform/workloads/net-kingdom/directory-binds", ["accounts-sign-in"], "destroy-kv")],
[step("delete-accounts", "lldap.user.delete-created", "lldap:svc-authelia,svc-keycape,svc-privacyidea", "lldap", "root-class", True, "ADMINISTER", "lldap"),
step("destroy-kv", "openbao.kv.destroy-created", "openbao:platform/workloads/net-kingdom/directory-binds", "lldap", "root-class", True, "OPERATE", "openbao/platform/workloads/net-kingdom/directory-binds")],
[check("accounts-read-only", "each account exists in lldap_strict_readonly only and cannot write", SPLIT, 60),
check("accounts-sign-in", "each KV value signs in to LLDAP as its account", LANE, 60)],
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
"on_revert_failure": "stop-and-alert", "alert": "founder via State Hub message"},
{"wall_time_seconds": 900, "attempts": 1, "cost": {"compute_cpu_seconds": 60, "egress_bytes": 1000000, "currency": "EUR", "amount": 0.02}},
["switching any consumer to the new accounts", "any human account", "changing LLDAP's configured super-admin"])
superadmin = doc(
"BEA-2026-0003", "Switch LLDAP's configured super-admin to ld-admin (bind-split step 9, root-class)",
"Change LLDAP_LDAP_USER_DN from admin to ld-admin and restart LLDAP; admin keeps its rights through lldap_admin membership. Changes who administers the directory, so the founder admits the run.",
"high", {"not_before": "2026-11-03T09:00:00Z", "not_after": "2026-11-03T11:00:00Z"},
[{"mode": "ADMINISTER", "scope": "lldap", "environments": P},
{"mode": "OPERATE", "scope": "k8s/sso/deployment/lldap", "environments": P}],
{"unit": "consumer", "consumers": ["lldap"], "max_concurrent": 1},
[SPLIT, JOURNEY],
[step("switch-user-dn", "lldap.config.set-ldap-user-dn", "k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN", "lldap", "root-class", True, "ADMINISTER", "lldap", ["ld-admin-administers"], "switch-back"),
step("restart-lldap", "k8s.rollout-restart", "k8s:sso/deployment/lldap", "lldap", "root-class", True, "OPERATE", "k8s/sso/deployment/lldap", ["synthetic-journey"], "restart-lldap-again")],
[step("switch-back", "lldap.config.set-ldap-user-dn", "k8s:sso/secret/lldap-secrets#LLDAP_LDAP_USER_DN", "lldap", "root-class", True, "ADMINISTER", "lldap"),
step("restart-lldap-again", "k8s.rollout-restart", "k8s:sso/deployment/lldap", "lldap", "root-class", True, "OPERATE", "k8s/sso/deployment/lldap")],
[check("ld-admin-administers", "ld-admin signs in and administers; admin keeps lldap_admin membership", SPLIT, 60),
check("synthetic-journey", "all three binds still serve sign-in: syn-journey reaches aal2", JOURNEY, 90, settle=125)],
{"on_check_failure": "revert", "on_budget_exhausted": "revert", "on_unknown": "stop",
"on_revert_failure": "stop-and-alert", "alert": "founder via State Hub message; crisis list item for ld-admin"},
{"wall_time_seconds": 900, "attempts": 1, "cost": {"compute_cpu_seconds": 60, "egress_bytes": 1000000, "currency": "EUR", "amount": 0.02}},
["changing admin's own password", "sealing ld-admin's password (human-intrinsic, step 10)", "any service bind change"])
verify = doc(
"BEA-2026-0004", "Read-only verification of the directory split (bind-split steps 2, 4 and 10 verify)",
"Plan and verify reads that the founder used to do signed in as admin: account inventory, group membership, bind identities of the consumers. No mutation, so nothing to revert.",
"low", {"not_before": "2026-11-03T09:00:00Z", "not_after": "2026-11-04T09:00:00Z"},
[{"mode": "OBSERVE", "scope": "lldap/people", "environments": P},
{"mode": "OBSERVE", "scope": "k8s/sso", "environments": P}],
{"unit": "consumer", "consumers": ["lldap", "authelia", "keycape", "privacyidea"], "max_concurrent": 4},
[SPLIT],
[step("inventory", "lldap.read.users-and-groups", "lldap:people", "lldap", "delegable", False, "OBSERVE", "lldap/people", ["split-shape"]),
step("consumer-binds", "k8s.read.bind-identities", "k8s:sso", "authelia", "delegable", False, "OBSERVE", "k8s/sso", ["split-shape"])],
[],
[check("split-shape", "every consumer binds as its own svc- account; no service binds as admin", SPLIT, 60)],
{"on_check_failure": "stop", "on_budget_exhausted": "stop", "on_unknown": "stop", "on_revert_failure": "stop-and-alert"},
{"wall_time_seconds": 300, "attempts": 3, "cost": {"compute_cpu_seconds": 30, "egress_bytes": 500000, "currency": "EUR", "amount": 0.01}},
["any write to LLDAP, Kubernetes or OpenBao"])
out = ROOT / "examples/bounded-execution-admission"
for name, d in [("bind-rotation-svc-authelia", rotation), ("bind-split-create-service-accounts", create),
("bind-split-switch-super-admin", superadmin), ("bind-split-read-only-verify", verify)]:
(out / f"{name}.yaml").write_text("# NK-WP-0056-T01 example: the bind split (NK-WP-0054) as admissions. Illustrative: the executor\n"
"# (T03) and its operations do not exist yet; files are pinned at the commit named in plan.source.\n"
+ yaml.safe_dump(d, sort_keys=False, width=120))
# Invalid: step 10, the founder's own password, is human-intrinsic.
bad = json.loads(json.dumps(superadmin)); s = bad["spec"]; s["id"] = "BEA-2026-0099"
s["plan"]["steps"].append(step("change-admin-password", "lldap.user.set-password", "lldap:admin", "lldap", "human-intrinsic", True, "ADMINISTER", "lldap", ["ld-admin-administers"], "switch-back"))
s["plan_digest"] = V.plan_digest(bad)
fx = ROOT / "tools/bounded-execution-admission/fixtures"; (fx / "invalid").mkdir(parents=True, exist_ok=True)
(fx / "invalid/human-intrinsic-step.yaml").write_text(yaml.safe_dump(bad, sort_keys=False, width=120))
print("commit", commit)

View file

@ -0,0 +1,195 @@
"""Tests for the Bounded Execution Admission validator (NK-WP-0056-T01). Synthetic data only."""
from __future__ import annotations
import copy
import json
import pathlib
import sys
import unittest
HERE = pathlib.Path(__file__).resolve().parent
TOOL = HERE.parent
ROOT = TOOL.parents[1]
sys.path.insert(0, str(TOOL))
import validate as V # noqa: E402
SCHEMA = json.loads(V.SCHEMA.read_text())
EXAMPLES = ROOT / "examples/bounded-execution-admission"
ROTATION = V.load(EXAMPLES / "bind-rotation-svc-authelia.yaml")
SUPERADMIN = V.load(EXAMPLES / "bind-split-switch-super-admin.yaml")
VERIFY = V.load(EXAMPLES / "bind-split-read-only-verify.yaml")
def mutate(document, fn, redigest=True):
d = copy.deepcopy(document)
fn(d["spec"])
if redigest:
d["spec"]["plan_digest"] = V.plan_digest(d)
return d
def rules(document):
return {f["rule"] for f in V.evaluate(document, SCHEMA)["findings"]}
def record(document, **over):
r = {"apiVersion": V.CARRIER_RECORD, "carrier": "approval-engine", "approval_ref": "apr-synthetic-0001",
"admission_id": document["spec"]["id"], "admission_digest": V.admission_digest(document),
"approver": {"name": "admin", "principal_type": "HUMAN", "assurance": "aal2"},
"approved_at": "2026-11-03T08:30:00Z",
"consumptions": [{"at": "2026-11-03T09:05:00Z", "by": document["spec"]["executor"]["identity"]}]}
r.update(over)
return r
class ExampleTests(unittest.TestCase):
def test_all_examples_valid(self):
for path in sorted(EXAMPLES.glob("*.yaml")):
with self.subTest(path=path.name):
report = V.evaluate(V.load(path), SCHEMA)
self.assertTrue(report["valid"], report["findings"])
def test_invalid_fixtures_refused(self):
for path in sorted((TOOL / "fixtures/invalid").glob("*.yaml")):
with self.subTest(path=path.name):
self.assertFalse(V.evaluate(V.load(path), SCHEMA)["valid"])
def test_examples_pin_current_source(self):
"""Pinned hashes are real files of this repository (they drift when the files change)."""
for path in sorted(EXAMPLES.glob("*.yaml")):
for f in V.load(path)["spec"]["plan"]["source"]["files"]:
self.assertTrue((ROOT / f["path"]).is_file(), f["path"])
class RuleTests(unittest.TestCase):
def test_bea1_schema(self):
self.assertEqual(rules(mutate(ROTATION, lambda s: s.pop("budget"), redigest=False)), {"BEA-1"})
self.assertEqual(rules(mutate(ROTATION, lambda s: s["approval"].update(assurance="aal1"))), {"BEA-1"})
self.assertEqual(rules(mutate(ROTATION, lambda s: s["consumption"].update(single_use=False))), {"BEA-1"})
def test_bea2_plan_digest_binds_the_exact_plan(self):
changed = mutate(ROTATION, lambda s: s["plan"]["steps"][0].update(operation="something-else"), redigest=False)
self.assertEqual(rules(changed), {"BEA-2"})
def test_bea3_window(self):
self.assertIn("BEA-3", rules(mutate(ROTATION, lambda s: s["window"].update(not_after="2026-11-04T10:00:00Z"))))
self.assertIn("BEA-3", rules(mutate(ROTATION, lambda s: s["window"].update(not_after="2026-11-03T08:00:00Z"))))
self.assertIn("BEA-3", rules(mutate(SUPERADMIN, lambda s: s["window"].update(not_after="2026-11-03T14:00:00Z"))))
def test_window_state(self):
at = V.parse_time
self.assertEqual(V.evaluate(ROTATION, SCHEMA, at("2026-11-03T10:00:00Z"))["window_state"], "open")
self.assertEqual(V.evaluate(ROTATION, SCHEMA, at("2026-11-03T11:00:00Z"))["window_state"], "expired")
self.assertEqual(V.evaluate(ROTATION, SCHEMA, at("2026-11-02T10:00:00Z"))["window_state"], "not-yet")
def test_bea4_executor(self):
self.assertIn("BEA-4", rules(mutate(ROTATION, lambda s: s["executor"].update(principal_type="HUMAN"))))
self.assertIn("BEA-4", rules(mutate(ROTATION, lambda s: s["executor"].update(identity="admin"))))
self.assertIn("BEA-4", rules(mutate(ROTATION, lambda s: s["executor"].update(principal_type="AGENT"))))
harnessed = mutate(ROTATION, lambda s: s["executor"].update(principal_type="AGENT", harness_launched=True))
self.assertEqual(rules(harnessed), set())
def test_bea5_ceiling(self):
self.assertIn("BEA-5", rules(mutate(ROTATION, lambda s: s["scope_ceiling"].append(
{"mode": "GOVERN", "scope": "lldap", "environments": ["PRODUCTION"]}))))
# whole-directory ADMINISTER for a delegable step at medium risk
def widen(s):
s["scope_ceiling"][1]["scope"] = "lldap"
self.assertIn("BEA-5", rules(mutate(ROTATION, widen)))
def test_bea6_least_privilege(self):
self.assertIn("BEA-6", rules(mutate(ROTATION, lambda s: s["scope_ceiling"].pop(3)))) # restart uncovered
self.assertIn("BEA-6", rules(mutate(ROTATION, lambda s: s["scope_ceiling"].append(
{"mode": "OPERATE", "scope": "k8s/sso/deployment/keycape", "environments": ["PRODUCTION"]})))) # unused
def test_bea7_root_class_needs_high_risk(self):
self.assertIn("BEA-7", rules(mutate(ROTATION, lambda s: s["plan"]["steps"][0].update({"class": "root-class"}))))
def test_bea8_blast_radius(self):
self.assertIn("BEA-8", rules(mutate(ROTATION, lambda s: s["plan"]["steps"][3].update(consumer="keycape"))))
def two_at_once(s):
s["blast_radius"].update(consumers=["authelia", "keycape"], max_concurrent=2)
self.assertIn("BEA-8", rules(mutate(ROTATION, two_at_once)))
def test_bea9_checks(self):
self.assertIn("BEA-9", rules(mutate(ROTATION, lambda s: s["plan"]["steps"][3].pop("checks_after"))))
self.assertIn("BEA-9", rules(mutate(ROTATION, lambda s: s["checks"][3].update(sha256="0" * 64))))
self.assertIn("BEA-9", rules(mutate(ROTATION, lambda s: s["checks"].append(dict(s["checks"][0], id="spare")))))
def test_bea10_revert(self):
self.assertIn("BEA-10", rules(mutate(ROTATION, lambda s: s["plan"]["steps"][1].pop("revert"))))
def irreversible(s):
s["plan"]["steps"][1].pop("revert")
s["plan"]["steps"][1]["irreversible"] = True
self.assertIn("BEA-10", rules(mutate(ROTATION, irreversible))) # medium risk
def test_bea11_stop_policy(self):
self.assertIn("BEA-11", rules(mutate(ROTATION, lambda s: s["stop_and_revert"].update(on_check_failure="stop"))))
self.assertIn("BEA-11", rules(mutate(VERIFY, lambda s: s["stop_and_revert"].update(on_check_failure="revert"))))
def test_bea12_budget(self):
self.assertIn("BEA-12", rules(mutate(ROTATION, lambda s: s["budget"].update(wall_time_seconds=8000))))
self.assertIn("BEA-12", rules(mutate(SUPERADMIN, lambda s: s["budget"].update(attempts=2))))
self.assertIn("BEA-12", rules(mutate(ROTATION, lambda s: s["budget"].update(wall_time_seconds=100))))
def test_bea14_secret_shaped_values(self):
self.assertIn("BEA-14", rules(mutate(ROTATION, lambda s: s.update(purpose="token hvs.abcdefghijklmnop"))))
def test_findings_never_echo_values(self):
marker = "hvs.SYNTHETICMARKER12345"
report = V.evaluate(mutate(ROTATION, lambda s: s.update(purpose=marker)), SCHEMA)
self.assertNotIn(marker, json.dumps(report))
class ApprovalTests(unittest.TestCase):
def check(self, document, rec):
return V.check_approval(document, rec)
def test_valid_record_admits_and_is_consumed(self):
report = self.check(ROTATION, record(ROTATION))
self.assertTrue(report["admitted"], report)
self.assertTrue(report["consumed"])
self.assertTrue(self.check(ROTATION, record(ROTATION, consumptions=[]))["admitted"])
def test_refusals(self):
other = mutate(ROTATION, lambda s: s.update(title="a different request"))
cases = {
"BEA-C1": record(ROTATION, carrier="email"),
"BEA-C2": record(ROTATION, admission_digest=V.admission_digest(other)),
"BEA-C3": record(ROTATION, approver={"name": "tegwick", "principal_type": "HUMAN", "assurance": "aal2"}),
"BEA-C4": record(ROTATION, approver={"name": "admin", "principal_type": "HUMAN", "assurance": "aal1"}),
"BEA-C5": record(ROTATION, approved_at="2026-11-03T12:00:00Z", consumptions=[]),
"BEA-C6": record(ROTATION, consumptions=[{"at": "2026-11-03T09:05:00Z", "by": "service:nk-bind-executor"},
{"at": "2026-11-03T09:40:00Z", "by": "service:nk-bind-executor"}]),
"BEA-C7": record(ROTATION, consumptions=[{"at": "2026-11-03T11:30:00Z", "by": "service:nk-bind-executor"}]),
}
for rule, rec in cases.items():
with self.subTest(rule=rule):
report = self.check(ROTATION, rec)
self.assertFalse(report["admitted"])
self.assertIn(rule, {f["rule"] for f in report["findings"]})
def test_consumed_by_someone_else(self):
rec = record(ROTATION, consumptions=[{"at": "2026-11-03T09:05:00Z", "by": "service:other"}])
self.assertIn("BEA-C7", {f["rule"] for f in self.check(ROTATION, rec)["findings"]})
class CliTests(unittest.TestCase):
def test_cli(self):
import contextlib
import io
import tempfile
out = io.StringIO()
with contextlib.redirect_stdout(out):
self.assertEqual(V.main(["validate", str(EXAMPLES / "bind-rotation-svc-authelia.yaml")]), 0)
self.assertEqual(V.main(["validate", str(TOOL / "fixtures/invalid/human-intrinsic-step.yaml")]), 1)
with tempfile.TemporaryDirectory() as tmp:
path = pathlib.Path(tmp) / "record.json"
path.write_text(json.dumps(record(ROTATION)))
self.assertEqual(V.main(["check-approval", str(EXAMPLES / "bind-rotation-svc-authelia.yaml"), str(path)]), 0)
self.assertEqual(V.main(["digest", str(EXAMPLES / "bind-rotation-svc-authelia.yaml")]), 0)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,323 @@
#!/usr/bin/env python3
"""Validate NetKingdom Bounded Execution Admissions (standard v0.1, NK-WP-0056-T01).
Offline and deterministic. Three commands:
validate [--as-of T] FILE... schema plus rules BEA-1..BEA-14 for each admission
check-approval ADMISSION RECORD rules BEA-C1..BEA-C7: does a carrier record (Approval Engine
approval or founder SSH signature) admit exactly this admission,
and was it consumed at most once, by the executor, in the window
digest FILE print plan_digest and admission_digest (for authoring)
Findings name rules and locations, never field values. Nothing here contacts a service, reads a
secret or authorizes anything: a valid admission is a well-formed request, not an admission.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import json
import pathlib
import re
import sys
from typing import Any
import jsonschema
import yaml
ROOT = pathlib.Path(__file__).resolve().parents[2]
SCHEMA = ROOT / "canon/schemas/bounded-execution-admission_v0.1.schema.json"
SECRET_VALUE = re.compile(
r"(\bhv[sbr]\.[A-Za-z0-9_-]{8,}|\bs\.[A-Za-z0-9]{20,}"
r"|-----BEGIN [A-Z ]*PRIVATE KEY-----|AGE-SECRET-KEY-1[A-Z0-9]{10,}"
r"|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\."
r"|://[^/\s:@]+:[^/\s@]{3,}@)"
)
MAX_WINDOW = {"low": dt.timedelta(hours=72), "medium": dt.timedelta(hours=24), "high": dt.timedelta(hours=4)}
MAX_ATTEMPTS = {"low": 3, "medium": 3, "high": 1}
CARRIER_RECORD = "netkingdom.io/bounded-execution-approval/v0.1"
# --------------------------------------------------------------------------------------------------
# Loading and digests
# --------------------------------------------------------------------------------------------------
def load(path: pathlib.Path) -> Any:
text = path.read_text(encoding="utf-8")
data = json.loads(text) if path.suffix == ".json" else yaml.safe_load(text)
return json.loads(json.dumps(data, default=str)) # YAML dates become strings, as JSON would carry them
def canonical(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def plan_digest(document: dict) -> str:
return "sha256:" + hashlib.sha256(canonical(document["spec"]["plan"])).hexdigest()
def admission_digest(document: dict) -> str:
"""What a carrier binds: the whole admission, including plan_digest."""
return "sha256:" + hashlib.sha256(canonical(document)).hexdigest()
def parse_time(value: str) -> dt.datetime:
return dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
def strings(value: Any):
if isinstance(value, str):
yield value
elif isinstance(value, dict):
for item in value.values():
yield from strings(item)
elif isinstance(value, list):
for item in value:
yield from strings(item)
# --------------------------------------------------------------------------------------------------
# Rules
# --------------------------------------------------------------------------------------------------
def covers(grant: dict, need: dict) -> bool:
return grant["mode"] == need["mode"] and (need["scope"] == grant["scope"]
or need["scope"].startswith(grant["scope"] + "/"))
def evaluate(document: Any, schema: dict, as_of: dt.datetime | None = None) -> dict:
findings: list[dict] = []
def fail(rule: str, where: str) -> None:
findings.append({"rule": rule, "where": where})
validator = jsonschema.Draft202012Validator(schema, format_checker=jsonschema.FormatChecker())
schema_errors = sorted(f"{'/'.join(str(p) for p in e.absolute_path) or '<root>'}:{e.validator}"
for e in validator.iter_errors(document))
if schema_errors:
return {"valid": False, "findings": [{"rule": "BEA-1", "where": e} for e in schema_errors]}
spec = document["spec"]
risk = spec["risk_class"]
steps = spec["plan"]["steps"]
reverts = spec["plan"].get("revert_steps", [])
every_step = steps + reverts
ceiling = spec["scope_ceiling"]
checks = {c["id"]: c for c in spec["checks"]}
pinned = {f["path"]: f["sha256"] for f in spec["plan"]["source"]["files"]}
# BEA-2 the admission binds the exact plan.
if spec["plan_digest"] != plan_digest(document):
fail("BEA-2", "spec/plan_digest")
# BEA-3 a bounded window, tighter for higher risk.
start, end = parse_time(spec["window"]["not_before"]), parse_time(spec["window"]["not_after"])
window = end - start
if window <= dt.timedelta(0):
fail("BEA-3", "spec/window")
elif window > MAX_WINDOW[risk]:
fail("BEA-3", "spec/window:longer-than-risk-class-allows")
# BEA-4 the executor is not a person and not an approver; an agent only when harness-launched (SI-14).
executor = spec["executor"]
if executor["principal_type"] == "HUMAN":
fail("BEA-4", "spec/executor/principal_type")
if executor["identity"] in spec["approval"]["approvers"]:
fail("BEA-4", "spec/executor/identity:is-an-approver")
if executor["principal_type"] == "AGENT" and executor.get("harness_launched") is not True:
fail("BEA-4", "spec/executor/harness_launched")
# BEA-5 the ceiling never holds sovereign or rule-changing authority; whole-service ADMINISTER is
# root-class, high risk and one consumer at a time.
for i, grant in enumerate(ceiling):
where = f"spec/scope_ceiling/{i}"
if grant["mode"] in ("SOVEREIGN", "GOVERN"):
fail("BEA-5", where + ":mode")
if grant["mode"] == "CONSTRUCT" and "PRODUCTION" in grant["environments"]:
fail("BEA-5", where + ":construct-in-production")
if grant["mode"] == "ADMINISTER":
if spec["blast_radius"]["max_concurrent"] != 1:
fail("BEA-5", where + ":administer-needs-max-concurrent-1")
if "/" not in grant["scope"]:
users = [s for s in every_step if covers(grant, s["requires"])]
if risk != "high" or any(s["class"] != "root-class" for s in users):
fail("BEA-5", where + ":whole-service-administer-is-root-class-high-risk")
# BEA-6 least privilege: every step fits the ceiling, and every ceiling grant is used.
for i, step in enumerate(every_step):
if not any(covers(g, step["requires"]) for g in ceiling):
fail("BEA-6", f"step/{step['id']}/requires:outside-ceiling")
for i, grant in enumerate(ceiling):
if not any(covers(grant, s["requires"]) for s in every_step):
fail("BEA-6", f"spec/scope_ceiling/{i}:unused")
# BEA-7 classes: human-intrinsic never; root-class only at high risk.
for step in every_step:
if step["class"] == "human-intrinsic":
fail("BEA-7", f"step/{step['id']}/class:human-intrinsic")
if step["class"] == "root-class" and risk != "high":
fail("BEA-7", f"step/{step['id']}/class:root-class-needs-high-risk")
# BEA-8 blast radius.
radius = spec["blast_radius"]
for step in every_step:
if step["consumer"] not in radius["consumers"]:
fail("BEA-8", f"step/{step['id']}/consumer:outside-blast-radius")
if radius["max_concurrent"] > len(radius["consumers"]):
fail("BEA-8", "spec/blast_radius/max_concurrent")
if risk in ("medium", "high") and radius["max_concurrent"] != 1:
fail("BEA-8", "spec/blast_radius/max_concurrent:one-at-a-time-for-medium-and-high")
# BEA-9 named machine checks, pinned to the admitted source.
if len(checks) != len(spec["checks"]):
fail("BEA-9", "spec/checks:duplicate-id")
used = set()
for step in steps:
if step["mutates"] and not step.get("checks_after"):
fail("BEA-9", f"step/{step['id']}/checks_after:missing")
for ref in step.get("checks_after", []):
used.add(ref)
if ref not in checks:
fail("BEA-9", f"step/{step['id']}/checks_after:undeclared")
for cid, check in checks.items():
if cid not in used:
fail("BEA-9", f"check/{cid}:unused")
if pinned.get(check["ref"]) != check["sha256"]:
fail("BEA-9", f"check/{cid}/ref:not-pinned-in-plan-source")
# BEA-10 every mutating step can be undone, or is declared irreversible at high risk.
ids = [s["id"] for s in every_step]
if len(set(ids)) != len(ids):
fail("BEA-10", "plan:duplicate-step-id")
revert_ids = {s["id"] for s in reverts}
for step in steps:
if not step["mutates"]:
continue
if step.get("irreversible"):
if risk != "high":
fail("BEA-10", f"step/{step['id']}/irreversible:needs-high-risk")
elif step.get("revert") not in revert_ids:
fail("BEA-10", f"step/{step['id']}/revert:missing")
for step in reverts:
if not step["mutates"] or step.get("revert") or step.get("irreversible"):
fail("BEA-10", f"revert_step/{step['id']}:must-mutate-and-not-nest")
# BEA-11 stop-and-revert follows the plan: a revertable plan reverts on failure.
policy = spec["stop_and_revert"]
if revert_ids and policy["on_check_failure"] != "revert":
fail("BEA-11", "spec/stop_and_revert/on_check_failure")
if not revert_ids and policy["on_check_failure"] == "revert":
fail("BEA-11", "spec/stop_and_revert/on_check_failure:nothing-to-revert")
# BEA-12 the budget fits the window and the checks fit the budget.
budget = spec["budget"]
if budget["wall_time_seconds"] > max(window.total_seconds(), 0):
fail("BEA-12", "spec/budget/wall_time_seconds:exceeds-window")
if budget["attempts"] > MAX_ATTEMPTS[risk]:
fail("BEA-12", "spec/budget/attempts")
check_time = sum(c["timeout_seconds"] + c.get("settle_seconds", 0) for c in spec["checks"])
if check_time > budget["wall_time_seconds"]:
fail("BEA-12", "spec/budget/wall_time_seconds:shorter-than-checks")
# BEA-13 the executed source is pinned and the plan names it.
if not spec["plan"]["source"]["files"]:
fail("BEA-13", "spec/plan/source/files")
# BEA-14 no secret-shaped values anywhere.
if any(SECRET_VALUE.search(s) for s in strings(document)):
fail("BEA-14", "<document>:secret-shaped-value")
report = {"valid": not findings, "findings": findings, "plan_digest": plan_digest(document),
"admission_digest": admission_digest(document)}
if as_of is not None:
report["window_state"] = ("not-yet" if as_of < start else "expired" if as_of >= end else "open")
return report
def check_approval(document: dict, record: Any) -> dict:
"""BEA-C1..C7: a carrier record admits exactly this admission, consumed at most once in the window."""
findings: list[dict] = []
def fail(rule: str, where: str) -> None:
findings.append({"rule": rule, "where": where})
spec = document["spec"]
required = ("apiVersion", "carrier", "approval_ref", "admission_id", "admission_digest", "approver",
"approved_at", "consumptions")
if not isinstance(record, dict) or any(k not in record for k in required) \
or record.get("apiVersion") != CARRIER_RECORD:
return {"admitted": False, "findings": [{"rule": "BEA-C1", "where": "<record>:shape"}]}
if record["carrier"] not in spec["approval"]["carriers"]:
fail("BEA-C1", "record/carrier")
if record["admission_id"] != spec["id"] or record["admission_digest"] != admission_digest(document):
fail("BEA-C2", "record/admission_digest")
approver = record["approver"] if isinstance(record["approver"], dict) else {}
if approver.get("name") not in spec["approval"]["approvers"] or approver.get("principal_type") != "HUMAN":
fail("BEA-C3", "record/approver")
if approver.get("assurance") != "aal2":
fail("BEA-C4", "record/approver/assurance")
if approver.get("name") == spec["executor"]["identity"]:
fail("BEA-C3", "record/approver:is-the-executor")
start, end = parse_time(spec["window"]["not_before"]), parse_time(spec["window"]["not_after"])
approved = parse_time(record["approved_at"])
if approved >= end:
fail("BEA-C5", "record/approved_at:after-window")
consumptions = record["consumptions"] if isinstance(record["consumptions"], list) else []
if len(consumptions) > 1:
fail("BEA-C6", "record/consumptions:more-than-once")
for i, use in enumerate(consumptions):
at = parse_time(use["at"])
if not (start <= at < end) or at < approved:
fail("BEA-C7", f"record/consumptions/{i}/at")
if use.get("by") != spec["executor"]["identity"]:
fail("BEA-C7", f"record/consumptions/{i}/by")
return {"admitted": not findings, "consumed": len(consumptions) == 1, "findings": findings}
# --------------------------------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------------------------------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
sub = parser.add_subparsers(dest="command", required=True)
v = sub.add_parser("validate")
v.add_argument("paths", nargs="+", type=pathlib.Path)
v.add_argument("--as-of", help="evaluate the window at this time (ISO 8601)")
c = sub.add_parser("check-approval")
c.add_argument("admission", type=pathlib.Path)
c.add_argument("record", type=pathlib.Path)
d = sub.add_parser("digest")
d.add_argument("path", type=pathlib.Path)
args = parser.parse_args(argv)
schema = json.loads(SCHEMA.read_text(encoding="utf-8"))
if args.command == "digest":
document = load(args.path)
print(json.dumps({"plan_digest": plan_digest(document), "admission_digest": admission_digest(document)}))
return 0
if args.command == "check-approval":
document = load(args.admission)
if not evaluate(document, schema)["valid"]:
print(json.dumps({"admitted": False, "findings": [{"rule": "BEA-C0", "where": "admission:invalid"}]}))
return 1
report = check_approval(document, load(args.record))
print(json.dumps(report, sort_keys=True))
return 0 if report["admitted"] else 1
as_of = parse_time(args.as_of) if args.as_of else None
worst = 0
for path in args.paths:
report = evaluate(load(path), schema, as_of)
report["file"] = str(path)
print(json.dumps(report, sort_keys=True))
worst = max(worst, 0 if report["valid"] else 1)
return worst
if __name__ == "__main__":
sys.exit(main())

View file

@ -17,6 +17,9 @@
{"what": "Who and what unlocks recovery", "where": "docs/crisis-custody-list.md"} {"what": "Who and what unlocks recovery", "where": "docs/crisis-custody-list.md"}
], ],
"tools": [ "tools": [
{"id": "bounded-execution-admission", "path": "tools/bounded-execution-admission", "answers": "Is this admission request well-formed and bounded (exact plan digest, window, scope ceiling, blast radius, checks, revert, budget), and does a carrier record admit it, consumed at most once?",
"run": "make bounded-execution-admission-check", "test": "make bounded-execution-admission-test", "agent_may_run": "yes",
"standard": "canon/standards/bounded-execution-admission_v0.1.md"},
{"id": "service-identity", "path": "tools/service-identity", "answers": "Do the declared service accounts conform to the identity and privilege standard?", {"id": "service-identity", "path": "tools/service-identity", "answers": "Do the declared service accounts conform to the identity and privilege standard?",
"run": "make service-identity-check", "test": "make service-identity-test", "agent_may_run": "yes", "run": "make service-identity-check", "test": "make service-identity-test", "agent_may_run": "yes",
"standard": "canon/standards/service-identity-privilege_v0.1.md"}, "standard": "canon/standards/service-identity-privilege_v0.1.md"},

View file

@ -54,7 +54,7 @@ and step 6 had to be done by a one-off path.
```task ```task
id: NK-WP-0056-T01 id: NK-WP-0056-T01
status: todo status: done
priority: high priority: high
assignee: claude-code assignee: claude-code
state_hub_task_id: "99df0ee2-2c17-55f2-9f3e-4277da52e01c" state_hub_task_id: "99df0ee2-2c17-55f2-9f3e-4277da52e01c"
@ -71,6 +71,16 @@ fixtures.
**Done when:** the standard is proposed with a schema, a validator, and the bind-split steps expressed **Done when:** the standard is proposed with a schema, a validator, and the bind-split steps expressed
as admissions in its examples. as admissions in its examples.
**Done 2026-10-31.** Proposed `canon/standards/bounded-execution-admission_v0.1.md` with
`canon/schemas/bounded-execution-admission_v0.1.schema.json`, `tools/bounded-execution-admission/validate.py`
(rules BEA-1..14 on the request, BEA-C1..C7 on a carrier record; 22 tests) and four examples in
`examples/bounded-execution-admission/`: the bind split's read-only verify (low), create `svc-*` accounts and switch
LLDAP's super-admin (root-class, high), and one-consumer bind rotation (delegable, medium, the T06 shape). Step 10 is
refused as human-intrinsic. Design choices: the request is immutable and bound by digest; the founder's approval
lives in a carrier (Approval Engine preferred, founder SSH signature transitional), so no second approval system;
`trust_requires` uses the Playbook Capability Contract's trust states; `settle_seconds` carries the T08
privacyIDEA-cache finding; recurring admissions are excluded from v0.1. The Approval Engine mapping is a proposal for T02.
## Owner agreement on the components ## Owner agreement on the components
```task ```task