NK-WP-0056-T03 done: executor identity and authority ceiling, in source
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:
parent
43a7890759
commit
2387409d08
16 changed files with 358 additions and 22 deletions
7
Makefile
7
Makefile
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
52
sso-mfa/k8s/bounded-executor/rbac.yaml
Normal file
52
sso-mfa/k8s/bounded-executor/rbac.yaml
Normal 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
|
||||
67
sso-mfa/k8s/bounded-executor/test_rbac.py
Normal file
67
sso-mfa/k8s/bounded-executor/test_rbac.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
84
tools/service-identity/declarations/bounded-executor.yaml
Normal file
84
tools/service-identity/declarations/bounded-executor.yaml
Normal 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
|
||||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue