NK-WP-0056-T03 done: executor identity and authority ceiling, in source
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Executor credentials declared (planned) with executor_subject; service-identity
gains convention executor (SI-15: approved-only grants, no person, not
interactive) and --admissions, checking that an admission's ceiling lies inside
the executor's declared grants (pilot covered). RBAC in sso-mfa/k8s/bounded-executor
(not applied): ServiceAccount without automount, a Role limited to the authelia
Deployment and ExternalSecret, no read of credential objects; tests tie Role,
declaration and pilot ceiling. The pilot example's delivery check now reads the
ExternalSecret status. railiance-apps and glas-harness answers recorded (T02).

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 994874@bnt-lap001
Assistant-Session: 6017c416-d619-47eb-bd1a-f670a02d7d87
This commit is contained in:
tegwick 2026-10-31 14:21:04 +01:00
parent 43a7890759
commit 2387409d08
16 changed files with 358 additions and 22 deletions

View file

@ -209,6 +209,11 @@ bounded-execution-admission-test: ## Run bounded-execution admission validator t
bounded-execution-admission-check: ## Validate the example admissions (read-only, offline)
python3 tools/bounded-execution-admission/validate.py validate examples/bounded-execution-admission/*.yaml
bounded-executor-test: ## Bounded executor RBAC and declaration tests; the pilot admission inside its declared grants (NK-WP-0056-T03)
python3 -m pytest sso-mfa/k8s/bounded-executor
python3 tools/service-identity/validate.py tools/service-identity/declarations/bounded-executor.yaml \
--admissions examples/bounded-execution-admission/bind-rotation-svc-authelia.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
@ -390,7 +395,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 \
bounded-execution-admission-test bounded-execution-admission-check bounded-executor-test \
emission-cadence-profile-test \
posture-feedback-reference security-scenario-compose-lightweight \
security-bootstrap-console-test security-bootstrap-scripts-syntax \

View file

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

View file

@ -147,7 +147,8 @@
"items": {
"enum": [
"bind",
"service-admin"
"service-admin",
"executor"
]
},
"description": "Estate naming conventions for recurring grant shapes (standard section 2). More than one is itself a finding."
@ -330,6 +331,12 @@
"maxLength": 200
}
}
},
"executor_subject": {
"type": "string",
"minLength": 1,
"maxLength": 120,
"description": "For a credential held by a bounded executor (NK-WP-0056): the subject the executor presents in its admissions (spec.executor.identity). Grants of all identities with the same subject form the executor's declared authority."
}
},
"allOf": [

View file

@ -21,8 +21,8 @@ Every answer below is the owner's agreement. Nothing here assigns work to anothe
| Executor credential lanes | secrets-engine | Lifecycle of the executor's OpenBao credentials | pending |
| OpenBao roles and CCR types | railiance-platform | Kubernetes auth role and policies for the executor; CCR request types | pending |
| Receipt holder | audit-core | Execution-attribution receipts from the executor | pending |
| Agent executors | glas-harness | Session contract so a harness-launched agent can be an executor | pending |
| Runtime package | railiance-apps | Where the new `rapp-*` package lives and who creates it | pending |
| Agent executors | glas-harness | Session contract so a harness-launched agent can be an executor | answered 2026-10-31 (glas-harness `92dd299`, `docs/bounded-execution-agent-executor.md`, GLAS-WP-0018-T07): not before GLAS-WP-0018-T04 (proposed; needs key-cape agent tokens with `act` and sandbox token delivery); a verifiable harness-launched assertion must be a key-cape-signed per-run token, claim set proposed; glas-harness proposes to own the agent-executor path later. Founder: approve GLAS-WP-0018 or not |
| Runtime package | railiance-apps | Where the new `rapp-*` package lives and who creates it | answered 2026-10-31 (railiance-apps `c733e86`, `docs/rapp-bounded-executor-intake.md`): not railiance-apps; `rmgr rapp wrap` (repo-manager) creates it locally, the Forgejo remote is the founder's; the image is built in the app repo (net-kingdom), the rapp pins its digest; deploy by `kubectl` server-side apply run by the founder; recommends one Job per admission. Founder decisions F1-F4 open |
## approval-engine

View file

@ -49,7 +49,7 @@ spec:
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
commit: 43a78907593a
files:
- path: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
@ -90,7 +90,7 @@ spec:
mode: OPERATE
scope: k8s/sso/externalsecret/authelia-secrets
checks_after:
- secret-equals-kv
- eso-synced
revert: redeliver
- id: restart
operation: k8s.rollout-restart
@ -141,7 +141,7 @@ spec:
requires:
mode: OPERATE
scope: k8s/sso/deployment/authelia
plan_digest: sha256:33129ab753da95f549fb27cfb5ba622e36fcba3c197f28b08fe3d4f9ff1d64a4
plan_digest: sha256:47de5fcaa09fdadb4ea501e47f85fc12da5b6ce31b28c2ccad34a25ca9599d0f
checks:
- id: kv-advanced
description: KV svc-authelia has exactly one new version, CAS on the previous one
@ -155,8 +155,8 @@ spec:
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'
- id: eso-synced
description: the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret
ref: tools/service-binds/bind-lane.py
sha256: 5b31b3567af07bba09530c6d3b0ed8b5c947df22bd13ac0442dc33c0d9b420c0
timeout_seconds: 120

View file

@ -43,7 +43,7 @@ spec:
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
commit: 43a78907593a
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
@ -93,7 +93,7 @@ spec:
requires:
mode: OPERATE
scope: openbao/platform/workloads/net-kingdom/directory-binds
plan_digest: sha256:94910771c288437dbaeeba80e32a4f61ef1cdb7d438e7352d21a5e10fe92e77e
plan_digest: sha256:be02f6850b329e2e2262bc720a70f881a146183ff70fb3ac20ae72e3a5ae70e8
checks:
- id: accounts-read-only
description: each account exists in lldap_strict_readonly only and cannot write

View file

@ -46,7 +46,7 @@ spec:
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
commit: 43a78907593a
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
@ -73,7 +73,7 @@ spec:
scope: k8s/sso
checks_after:
- split-shape
plan_digest: sha256:e4867e97a9957d543d5a7e265e56678ce22f3bb87bab64022b381dbee3e2610a
plan_digest: sha256:c07dd58e6581049441d512edb60753bfe7102e3c89ea28d55081141373844d0d
checks:
- id: split-shape
description: every consumer binds as its own svc- account; no service binds as admin

View file

@ -43,7 +43,7 @@ spec:
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
commit: 43a78907593a
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
@ -93,7 +93,7 @@ spec:
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
plan_digest: sha256:c15793ccaad212eaf24de12a1d13709970be84ee917a994463b68880e905dedf
plan_digest: sha256:60578e013b23fd3d68fead65956c939cde1eb9230d9f9fdbe1aaa677e8e0de11
checks:
- id: ld-admin-administers
description: ld-admin signs in and administers; admin keeps lldap_admin membership

View file

@ -0,0 +1,52 @@
# Bounded executor identity in sso (NK-WP-0056-T03). PREPARED IN SOURCE, NOT APPLIED.
#
# The ServiceAccount the executor runs as, with exactly the pilot's Kubernetes authority (T06: rotate the
# svc-authelia bind): force-sync one ExternalSecret and restart one Deployment. No Secret, ConfigMap or Pod
# access, no create/delete, no wildcard, no cluster scope. Its OpenBao and LLDAP credentials are declared in
# tools/service-identity/declarations/bounded-executor.yaml; all of it acts only under a consumed admission
# (canon/standards/bounded-execution-admission_v0.1.md). Packaging and apply belong to rapp-bounded-executor
# (ADR-0015, founder decision 6d1adce9). Tests: sso-mfa/k8s/bounded-executor/test_rbac.py.
apiVersion: v1
kind: ServiceAccount
metadata:
name: nk-bind-executor
namespace: sso
labels:
app.kubernetes.io/name: bounded-executor
app.kubernetes.io/part-of: net-kingdom-sso-mfa
automountServiceAccountToken: false # the executor pod mounts a projected, audience-bound token instead
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: nk-bind-executor
namespace: sso
labels:
app.kubernetes.io/name: bounded-executor
rules:
# restart: kubectl rollout restart patches the pod template annotation; rollout status reads and watches
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["authelia"]
verbs: ["get", "watch", "patch"]
# deliver: force-sync annotation, then read status (Ready, refreshTime) as the check; never the Secret
- apiGroups: ["external-secrets.io"]
resources: ["externalsecrets"]
resourceNames: ["authelia-secrets"]
verbs: ["get", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: nk-bind-executor
namespace: sso
labels:
app.kubernetes.io/name: bounded-executor
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nk-bind-executor
subjects:
- kind: ServiceAccount
name: nk-bind-executor
namespace: sso

View file

@ -0,0 +1,67 @@
"""The bounded executor's Kubernetes authority is exactly the pilot's (NK-WP-0056-T03). Source only, no cluster."""
from __future__ import annotations
import pathlib
import unittest
import yaml
HERE = pathlib.Path(__file__).resolve().parent
ROOT = HERE.parents[2]
DOCS = list(yaml.safe_load_all((HERE / "rbac.yaml").read_text()))
DECLARATION = yaml.safe_load((ROOT / "tools/service-identity/declarations/bounded-executor.yaml").read_text())
PILOT = yaml.safe_load((ROOT / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml").read_text())
KIND_SCOPE = {("apps", "deployments"): "deployment", ("external-secrets.io", "externalsecrets"): "externalsecret"}
FORBIDDEN_RESOURCES = {"secrets", "configmaps", "pods", "pods/exec", "pods/log", "serviceaccounts", "serviceaccounts/token",
"roles", "rolebindings", "clusterroles", "clusterrolebindings", "*"}
def by_kind(kind):
return [d for d in DOCS if d["kind"] == kind]
def role_scopes():
(role,) = by_kind("Role")
return {f"k8s/sso/{KIND_SCOPE[(group, resource)]}/{name}"
for rule in role["rules"] for group in rule["apiGroups"] for resource in rule["resources"]
for name in rule["resourceNames"]}
class RbacTests(unittest.TestCase):
def test_only_namespaced_objects_in_sso(self):
self.assertEqual(sorted(d["kind"] for d in DOCS), ["Role", "RoleBinding", "ServiceAccount"])
self.assertTrue(all(d["metadata"]["namespace"] == "sso" for d in DOCS))
def test_service_account_does_not_automount(self):
(sa,) = by_kind("ServiceAccount")
self.assertEqual(sa["metadata"]["name"], "nk-bind-executor")
self.assertIs(sa["automountServiceAccountToken"], False)
def test_every_rule_is_named_and_narrow(self):
(role,) = by_kind("Role")
for rule in role["rules"]:
with self.subTest(rule=rule["resources"]):
self.assertTrue(rule.get("resourceNames"), "every rule names its objects")
for field in ("apiGroups", "resources", "verbs", "resourceNames"):
self.assertNotIn("*", rule[field])
self.assertLessEqual(set(rule["verbs"]), {"get", "watch", "patch"})
self.assertFalse(set(rule["resources"]) & FORBIDDEN_RESOURCES)
self.assertTrue(set(rule["resources"]) <= {r for _, r in KIND_SCOPE})
def test_binding_binds_only_the_executor(self):
(binding,) = by_kind("RoleBinding")
self.assertEqual(binding["roleRef"], {"apiGroup": "rbac.authorization.k8s.io", "kind": "Role", "name": "nk-bind-executor"})
self.assertEqual(binding["subjects"], [{"kind": "ServiceAccount", "name": "nk-bind-executor", "namespace": "sso"}])
def test_role_equals_declaration_equals_pilot_ceiling(self):
(sa_identity,) = [i for i in DECLARATION["identities"] if i["name"] == "nk-bind-executor"]
declared = {g["scope"] for g in sa_identity["grants"]}
ceiling = {g["scope"] for g in PILOT["spec"]["scope_ceiling"] if g["scope"].startswith("k8s/")}
self.assertEqual(role_scopes(), declared)
self.assertEqual(declared, ceiling)
self.assertEqual(PILOT["spec"]["executor"]["identity"], sa_identity["executor_subject"])
if __name__ == "__main__":
unittest.main()

View file

@ -42,7 +42,7 @@ spec:
plan:
source:
repo: net-kingdom
commit: b8d3dacc4195
commit: 43a78907593a
files:
- path: sso-mfa/k8s/lldap/bind-split.py
sha256: 4decc3bbe65120ff6c4cef5613b9a41a60168ae64ecd4c1380f61a2fcd37b6b3
@ -104,7 +104,7 @@ spec:
requires:
mode: OPERATE
scope: k8s/sso/deployment/lldap
plan_digest: sha256:eb53d0d611a22d43214f7e4afb450e2a1c858110c9d5330b7d562fc98a1c8fe8
plan_digest: sha256:33063248bed00c84b640a37344a4c9e52285e76cacb68b92d08872cd9b7c77db
checks:
- id: ld-admin-administers
description: ld-admin signs in and administers; admin keeps lldap_admin membership

View file

@ -59,7 +59,7 @@ rotation = doc(
[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("deliver", "eso.force-sync", "k8s:sso/externalsecret/authelia-secrets", "authelia", "delegable", True, "OPERATE", "k8s/sso/externalsecret/authelia-secrets", ["eso-synced"], "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"),
@ -67,7 +67,7 @@ rotation = doc(
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("eso-synced", "the ExternalSecret is Ready with a refresh after the step started; the executor never reads the Secret", 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"},

View file

@ -0,0 +1,84 @@
schema_version: "0.1"
service: bounded-executor
owner: "NetKingdom (executor source); runtime package rapp-bounded-executor (NK-WP-0056-T02, founder decision 6d1adce9)"
declared_at: 2026-10-31
attestation:
authored_by: claude-code
basis: [canon/standards/bounded-execution-admission_v0.1.md, examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml, sso-mfa/k8s/bounded-executor/rbac.yaml, workplans/NK-WP-0056-bounded-executor-path.md]
owner_confirmed: false
complete: false
# Every credential the bounded executor holds carries executor_subject service:nk-bind-executor, the identity
# its admissions name. Together they are its declared authority; `validate.py --admissions` checks that an
# admission's scope ceiling lies inside them. All grants are APPROVED: they act only during a consumed
# admission (SI-15). Pilot scope (T06): rotate the svc-authelia bind, one consumer.
identities:
- name: nk-bind-executor
principal_type: WORKLOAD
conventions: [executor]
executor_subject: "service:nk-bind-executor"
purpose: "Kubernetes ServiceAccount sso/nk-bind-executor: force-sync the authelia-secrets ExternalSecret and restart the authelia Deployment; no Secret read"
grants:
- {mode: OPERATE, scope: k8s/sso/deployment/authelia, activation: APPROVED, environments: [PRODUCTION], note: "Role: get, watch, patch on deployments/authelia"}
- {mode: OPERATE, scope: k8s/sso/externalsecret/authelia-secrets, activation: APPROVED, environments: [PRODUCTION], note: "Role: get, patch on externalsecrets/authelia-secrets"}
interactive_login: false
second_factor: not_applicable
custody: [delivered-secret]
consumers: [{name: bounded-executor, kind: service}]
owner: "NetKingdom"
rotation: {path: "Projected ServiceAccount token, short-lived; nothing to rotate by hand", coupling: [bounded-executor]}
status: planned
- name: net-kingdom-bounded-executor
principal_type: WORKLOAD
conventions: [executor]
executor_subject: "service:nk-bind-executor"
purpose: "OpenBao Kubernetes-auth role for sso/nk-bind-executor: write the next version of the svc-authelia bind (CAS) and read it for checks; lane shape pending secrets-engine and railiance-platform answers (T02)"
grants:
- {mode: OPERATE, scope: openbao/platform/workloads/net-kingdom/directory-binds/svc-authelia, activation: APPROVED, environments: [PRODUCTION], note: "KV data read and CAS write; 15 min TTL"}
interactive_login: false
second_factor: not_applicable
custody: [openbao-lease]
consumers: [{name: bounded-executor, kind: service}]
owner: "railiance-platform (OpenBao role) for NetKingdom"
rotation: {path: "OpenBao lease per run; no standing secret", coupling: [bounded-executor]}
status: planned
- name: svc-executor-pw
principal_type: SERVICE
conventions: [executor]
executor_subject: "service:nk-bind-executor"
purpose: "LLDAP account in lldap_password_manager: set svc-authelia's password from KV. LLDAP cannot narrow it to one account (it can reset any non-admin user); each admission narrows it to the accounts it names"
grants:
- {mode: ADMINISTER, scope: lldap/people, activation: APPROVED, environments: [PRODUCTION], note: "lldap_password_manager; admission ceiling narrows to lldap/people/<account>"}
interactive_login: false
second_factor: not_applicable
custody: [openbao-lease]
consumers: [{name: bounded-executor, kind: service}]
owner: "NetKingdom directory and User Engine owners"
rotation: {path: "Its own password held in OpenBao; lane shape pending secrets-engine (T02)", coupling: [bounded-executor]}
status: planned
- name: svc-executor-read
principal_type: SERVICE
conventions: [bind]
executor_subject: "service:nk-bind-executor"
purpose: "Read-only directory bind for plan and verify steps (users, groups, bind identities); replaces the founder signing in as admin for reads"
grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}]
interactive_login: false
second_factor: not_applicable
custody: [openbao-lease]
consumers: [{name: bounded-executor, kind: service}]
owner: "NetKingdom directory and User Engine owners"
rotation: {path: "Bind password in OpenBao, rotated by the executor itself under an admission", coupling: [bounded-executor]}
status: planned
- name: svc-executor-admin
principal_type: SERVICE
conventions: [executor]
executor_subject: "service:nk-bind-executor"
purpose: "lldap_admin, only for root-class steps (create svc-* accounts, change the configured super-admin) under a high-risk admission; how membership is granted per run and removed afterwards is open"
grants:
- {mode: ADMINISTER, scope: lldap, activation: APPROVED, environments: [PRODUCTION], note: "root-class only; no narrower LLDAP group (SI-X6)"}
interactive_login: false
second_factor: not_applicable
custody: [openbao-lease]
consumers: [{name: bounded-executor, kind: service}]
owner: "NetKingdom directory and User Engine owners"
rotation: {path: "Per-run activation to be designed; not part of the T06 pilot", coupling: [bounded-executor]}
status: planned

View file

@ -337,3 +337,53 @@ class CommandLineTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class ExecutorTests(unittest.TestCase):
"""SI-15 for bounded-executor credentials and the admission cross-check (NK-WP-0056-T03)."""
def executor(self, **over):
base = identity(name="nk-exec", principal_type="WORKLOAD", conventions=["executor"],
executor_subject="service:nk-exec",
grants=[grant("OPERATE", "k8s/sso/deployment/x", activation="APPROVED")])
base.update(over)
return base
def test_conformant_executor(self):
broken, unknown = V._violations(self.executor())
self.assertEqual((broken, unknown), (set(), []))
def test_executor_refusals(self):
cases = {
"standing grant": self.executor(grants=[grant("OPERATE", "k8s/sso/deployment/x")]),
"human": self.executor(principal_type="HUMAN"),
"interactive": self.executor(interactive_login=True),
"person consumer": self.executor(consumers=[{"name": "founder", "kind": "person"}]),
"no subject": {k: v for k, v in self.executor().items() if k != "executor_subject"},
"govern": self.executor(grants=[grant("GOVERN", "lldap", activation="APPROVED")]),
}
for label, ident in cases.items():
with self.subTest(label):
self.assertIn("SI-15", V._violations(ident)[0])
def test_checked_in_executor_declaration_has_no_open_rules(self):
document = V.load_declaration(DECLARATIONS / "bounded-executor.yaml")
self.assertEqual(V.structural_errors(document, SCHEMA), [])
report = V.evaluate(document, NOW)
self.assertTrue(all(not i["rules"] for i in report["identities"]), report)
def test_admission_coverage(self):
document = V.load_declaration(DECLARATIONS / "bounded-executor.yaml")
examples = HERE.parents[2] / "examples/bounded-execution-admission"
pilot = V.load_declaration(examples / "bind-rotation-svc-authelia.yaml")
(result,) = V.admission_coverage([document], [pilot])
self.assertTrue(result["covered"], result)
wider = copy.deepcopy(pilot)
wider["spec"]["scope_ceiling"].append({"mode": "OPERATE", "scope": "k8s/sso/deployment/keycape",
"environments": ["PRODUCTION"]})
(result,) = V.admission_coverage([document], [wider])
self.assertEqual(result["outside_declared_grants"], ["spec/scope_ceiling/4"])
stranger = copy.deepcopy(pilot)
stranger["spec"]["executor"]["identity"] = "service:unknown"
(result,) = V.admission_coverage([document], [stranger])
self.assertFalse(result["executor_declared"])

View file

@ -148,6 +148,16 @@ def _violations(identity: dict[str, Any]) -> tuple[set[str], list[str]]:
# SI-7 CONSTRUCT (build mode, development) is never available in production.
if grant["mode"] == "CONSTRUCT" and prod:
broken.add("SI-7")
# SI-15 a bounded executor's credentials act only under an admission (NK-WP-0056): never a person, never
# interactive, every grant APPROVED, never sovereign, rule-changing or build-mode authority.
if "executor" in conventions:
if principal not in ("WORKLOAD", "SERVICE", "AGENT") or identity["interactive_login"] or persons:
broken.add("SI-15")
if not identity.get("executor_subject"):
broken.add("SI-15")
for grant in grants:
if grant.get("activation") != "APPROVED" or grant["mode"] in ("SOVEREIGN", "GOVERN", "CONSTRUCT"):
broken.add("SI-15")
# SI-8 assurance: people and service admins need a second factor; admin roles bind the claim.
if principal == "HUMAN" or "service-admin" in conventions:
if second == "unknown":
@ -264,12 +274,41 @@ def run(paths: list[pathlib.Path], as_of: dt.date) -> tuple[list[dict[str, Any]]
return reports, rejected
def _covers(grant: dict[str, Any], need: dict[str, Any]) -> bool:
return grant["mode"] == need["mode"] and (need["scope"] == grant["scope"]
or need["scope"].startswith(grant["scope"] + "/"))
def admission_coverage(documents: list[dict[str, Any]], admissions: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""SI-15: each admission's scope ceiling lies inside the declared grants of its executor's credentials.
The executor's authority is the union of grants of every declared identity whose executor_subject is the
admission's spec.executor.identity, in the admission's environments. Locations only, never values."""
results = []
for admission in admissions:
spec = admission.get("spec", {})
subject = spec.get("executor", {}).get("identity")
held = [g for d in documents for i in d["identities"] if i.get("executor_subject") == subject
for g in i["grants"]]
outside = []
for index, need in enumerate(spec.get("scope_ceiling", [])):
ok = any(_covers(g, need) and set(need.get("environments", [])) <= set(g.get("environments", []))
for g in held)
if not ok:
outside.append(f"spec/scope_ceiling/{index}")
results.append({"admission": spec.get("id"), "executor_declared": bool(held), "outside_declared_grants": outside,
"covered": bool(held) and not outside})
return results
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("paths", nargs="+", type=pathlib.Path)
parser.add_argument("--as-of", default=dt.date.today().isoformat(), help="evaluation date, YYYY-MM-DD")
parser.add_argument("--json", action="store_true", help="machine-readable output")
parser.add_argument("--fail-on-findings", action="store_true", help="exit 2 when anything is non-conformant or unknown")
parser.add_argument("--admissions", nargs="+", type=pathlib.Path, default=[],
help="bounded-execution admissions whose ceilings must lie inside the declared executor grants (SI-15)")
args = parser.parse_args(argv)
try:
as_of = parse_date(args.as_of)
@ -277,6 +316,18 @@ def main(argv: list[str] | None = None) -> int:
print("service identity: invalid --as-of", file=sys.stderr)
return 1
reports, rejected = run(args.paths, as_of)
coverage = []
if args.admissions:
schema = load_schema()
documents = []
for path in args.paths:
try:
document = load_declaration(path)
except (OSError, ValueError, yaml.YAMLError):
continue
if not structural_errors(document, schema):
documents.append(document)
coverage = admission_coverage(documents, [load_declaration(p) for p in args.admissions])
for line in rejected:
print(f"service identity: {line}", file=sys.stderr)
counts: dict[str, int] = {k: 0 for k in ORDER}
@ -284,7 +335,8 @@ def main(argv: list[str] | None = None) -> int:
for item in report["identities"]:
counts[item["result"]] += 1
if args.json:
print(json.dumps({"as_of": as_of.isoformat(), "counts": counts, "reports": reports, "rejected": len(rejected)}, indent=2))
print(json.dumps({"as_of": as_of.isoformat(), "counts": counts, "reports": reports, "rejected": len(rejected),
**({"admission_coverage": coverage} if args.admissions else {})}, indent=2))
else:
for report in reports:
for item in sorted(report["identities"], key=lambda i: (ORDER[i["result"]], i["identity"])):
@ -294,8 +346,14 @@ def main(argv: list[str] | None = None) -> int:
for note in report["advisories"]:
print(f"{report['service']}: advisory: {note}")
print("summary: " + ", ".join(f"{counts[k]} {k}" for k in ORDER) + f" (as of {as_of.isoformat()})")
for item in coverage:
state = "covered" if item["covered"] else ("no declared executor" if not item["executor_declared"]
else "outside declared grants: " + ", ".join(item["outside_declared_grants"]))
print(f"admission {item['admission']}: {state}")
if rejected:
return 1
if any(not item["covered"] for item in coverage):
return 2
if args.fail_on_findings and (counts["non-conformant"] or counts["unknown"]):
return 2
return 0

View file

@ -117,7 +117,7 @@ railiance-platform, audit-core, glas-harness and railiance-apps
```task
id: NK-WP-0056-T03
status: todo
status: done
priority: high
assignee: claude-code
depends_on: [NK-WP-0056-T02]
@ -132,6 +132,19 @@ it.
**Done when:** the declaration validates and the Role is prepared in source with tests, not applied.
**Done 2026-10-31.** `tools/service-identity/declarations/bounded-executor.yaml` declares the executor's credentials
(all `planned`, all sharing `executor_subject: service:nk-bind-executor`): the Kubernetes ServiceAccount, its OpenBao
role, `svc-executor-pw` (`lldap_password_manager`: LLDAP cannot narrow to one account, the admission narrows it),
the read-only bind `svc-executor-read`, and `svc-executor-admin` (`lldap_admin`, root-class only, per-run activation
open). `tools/service-identity` gained convention `executor` with rule SI-15 (no person, not interactive, every grant
`APPROVED`, no `SOVEREIGN`/`GOVERN`/`CONSTRUCT`) and `--admissions`, which checks that an admission's ceiling lies inside
the executor's declared grants: the pilot (BEA-2026-0001) is covered; the root-class and read-only examples are
correctly reported as needing grants the pilot does not hold. `sso-mfa/k8s/bounded-executor/rbac.yaml` (not applied):
ServiceAccount without automount, a Role with `get/watch/patch` on the authelia Deployment and `get/patch` on the
authelia ExternalSecret only (no read of credential objects: the pilot's delivery check became "ExternalSecret Ready
and refreshed"), and its RoleBinding; `test_rbac.py` ties Role = declaration = pilot ceiling. 44 identity tests,
5 RBAC tests.
## Service credentials into OpenBao
```task