NK-WP-0056-T01 done: Bounded Execution Admission v0.1 (proposed)
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:
parent
27b4ea6933
commit
989d5d4f5d
15 changed files with 2237 additions and 2 deletions
7
Makefile
7
Makefile
|
|
@ -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)
|
||||
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)
|
||||
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 \
|
||||
iam-profile-conformance-test playbook-contract-test posture-feedback-test \
|
||||
custody-record-test service-identity-test service-identity-check security-tools \
|
||||
bounded-execution-admission-test bounded-execution-admission-check \
|
||||
emission-cadence-profile-test \
|
||||
posture-feedback-reference security-scenario-compose-lightweight \
|
||||
security-bootstrap-console-test security-bootstrap-scripts-syntax \
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@
|
|||
| 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-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-T03 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
| task | NK-WP-0056-T04 | done | — | workplans/NK-WP-0056-bounded-executor-path.md |
|
||||
|
|
|
|||
582
canon/schemas/bounded-execution-admission_v0.1.schema.json
Normal file
582
canon/schemas/bounded-execution-admission_v0.1.schema.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
232
canon/standards/bounded-execution-admission_v0.1.md
Normal file
232
canon/standards/bounded-execution-admission_v0.1.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
18
tools/bounded-execution-admission/README.md
Normal file
18
tools/bounded-execution-admission/README.md
Normal 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.
|
||||
|
|
@ -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
|
||||
144
tools/bounded-execution-admission/generate_examples.py
Normal file
144
tools/bounded-execution-admission/generate_examples.py
Normal 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)
|
||||
195
tools/bounded-execution-admission/tests/test_validate.py
Normal file
195
tools/bounded-execution-admission/tests/test_validate.py
Normal 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()
|
||||
323
tools/bounded-execution-admission/validate.py
Normal file
323
tools/bounded-execution-admission/validate.py
Normal 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())
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
{"what": "Who and what unlocks recovery", "where": "docs/crisis-custody-list.md"}
|
||||
],
|
||||
"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?",
|
||||
"run": "make service-identity-check", "test": "make service-identity-test", "agent_may_run": "yes",
|
||||
"standard": "canon/standards/service-identity-privilege_v0.1.md"},
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ and step 6 had to be done by a one-off path.
|
|||
|
||||
```task
|
||||
id: NK-WP-0056-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
assignee: claude-code
|
||||
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
|
||||
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
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue