From ca03da52c6525c810db3c002305bb4b51c7c14a5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 24 Oct 2026 22:50:01 +0200 Subject: [PATCH] NK-WP-0054-T02: draft-2 of the Service Identity and Privilege standard after the founder's review Applied the founder's four decisions: OpenBao platform-admin binds aal2 (SI-8 MUST); the human administrator keeps directory admin rights through an admin group (SI-5); declarations start in this repository and move to the services; agents get their own class and rule SI-14. Finding F-1: the coding-agent role presents as a service against IAM Profile v0.3. Schema, validator and tests follow (37 pass). Rehearsals listed, not decided. Not ratified; owner review still open. Co-Authored-By: Claude Sonnet 5.5 Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 328925@bnt-lap001 Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed --- .../schemas/service-identity_v0.1.schema.json | 57 ++++++++++++ .../service-identity-privilege_v0.1.md | 91 ++++++++++++------- docs/service-identity-inventory.md | 8 +- tools/service-identity/README.md | 11 ++- .../declarations/openbao.yaml | 3 +- tools/service-identity/tests/test_validate.py | 37 ++++++++ tools/service-identity/validate.py | 17 +++- .../NK-WP-0054-service-identity-policy.md | 16 +++- 8 files changed, 194 insertions(+), 46 deletions(-) diff --git a/canon/schemas/service-identity_v0.1.schema.json b/canon/schemas/service-identity_v0.1.schema.json index 860609f..fe1c4b8 100644 --- a/canon/schemas/service-identity_v0.1.schema.json +++ b/canon/schemas/service-identity_v0.1.schema.json @@ -140,6 +140,7 @@ "bind", "service-admin", "workload", + "agent", "infrastructure" ] } @@ -285,6 +286,43 @@ "type": "boolean" } } + }, + "agent": { + "type": "object", + "additionalProperties": false, + "required": [ + "mode", + "presents_principal_type", + "max_token_ttl_seconds", + "boundary_policy" + ], + "properties": { + "mode": { + "enum": [ + "autonomous", + "delegated", + "unknown" + ] + }, + "presents_principal_type": { + "enum": [ + "agent", + "service", + "human", + "unknown" + ] + }, + "max_token_ttl_seconds": { + "type": "integer", + "minimum": 1, + "maximum": 86400 + }, + "boundary_policy": { + "type": "string", + "minLength": 3, + "maxLength": 200 + } + } } }, "allOf": [ @@ -304,6 +342,25 @@ "disposition" ] } + }, + { + "if": { + "properties": { + "classes": { + "contains": { + "const": "agent" + } + } + }, + "required": [ + "classes" + ] + }, + "then": { + "required": [ + "agent" + ] + } } ] }, diff --git a/canon/standards/service-identity-privilege_v0.1.md b/canon/standards/service-identity-privilege_v0.1.md index ba73efa..e7d9d3d 100644 --- a/canon/standards/service-identity-privilege_v0.1.md +++ b/canon/standards/service-identity-privilege_v0.1.md @@ -8,7 +8,7 @@ version: "0.1" created: "2026-10-24" updated: "2026-10-24" scope: service-account-governance -revision: "draft-1" +revision: "draft-2" owner: net-kingdom last_reviewed: "2026-10-24" review_interval: 3m @@ -27,10 +27,17 @@ related: ## 0. Status -**Proposed, draft-1.** It binds nothing until the founder ratifies it and each owner adopts it. +**Proposed, draft-2.** It binds nothing until the founder ratifies it and each owner adopts it. It was started on 2026-10-24 from the founder's direction and from the first inventory -(`docs/service-identity-inventory.md`, NK-WP-0054-T01). Its machine-readable declaration -(NK-WP-0054-T03) and conformance check (T04) do not exist yet. +(`docs/service-identity-inventory.md`, NK-WP-0054-T01). + +- **draft-1** (2026-10-24): thirteen rules, naming, five initial exceptions, seven open questions. +- **draft-2** (2026-10-24): the founder's review answered four questions (§10). Added the `agent` class and + SI-14; SI-8 now requires the assurance claim for admin-policy roles; the human administrator keeps + directory admin rights through group membership; declarations start in this repository and move to each + service. Building the offline validator (NK-WP-0054-T03) also made custody a list and broadened SI-X1. + +The offline validator exists (`tools/service-identity`, provisional). The live health check (T04) does not. The finding behind it: one LLDAP account, `uid=admin`, is at the same time the human platform administrator, the directory super-admin and the bind credential of Authelia, KeyCape, the @@ -67,7 +74,8 @@ the credential. Every identity has exactly one **class**. | `break-glass` | Final authority held offline, not routine | `platform-root` (king credential) | | `bind` | A service's own lookup in another service's directory | planned `svc-authelia` | | `service-admin` | The administrative account of one service | `pi-admin`, planned `ld-admin` | -| `workload` | A service or agent that obtains tokens or secrets | KeyCape clients, `secrets-engine-openbao` | +| `workload` | A service that obtains tokens or secrets | KeyCape clients, `secrets-engine-openbao` | +| `agent` | Automation that acts autonomously or under delegated authority; IAM Profile v0.3 agent principal | the coding agent | | `infrastructure` | A role below the application | database role `privacyidea` | ## 3. Rules @@ -94,8 +102,9 @@ automation unless the declaration says so. **SI-5 Attributed administration first.** Where a service supports administrative roles granted to an attributed human identity (for example OpenBao policies bound to a group), day-to-day -administration SHOULD use that route. The local `service-admin` is the sealed fallback and is -listed in the crisis custody list. A service that has only a local administrator (privacyIDEA +administration SHOULD use that route. The human platform administrator holds directory and policy administration rights through membership +of an administrative group on their own account, with their own password and second factor. The local +`service-admin` is the sealed fallback and is listed in the crisis custody list. A service that has only a local administrator (privacyIDEA today) records that as an exception, not as a norm. **SI-6 Break-glass.** There MUST be a break-glass identity, held offline. Using it is an event @@ -106,9 +115,10 @@ test subject of a drill. It authorizes; it does not operate. custody items and MUST NOT hold a production service credential. It builds and tests. **SI-8 Assurance.** Every `human` and `service-admin` identity MUST require a second factor -where the service can enforce one. A role that grants administrative policy SHOULD require the +where the service can enforce one. A role that grants administrative policy MUST require the assurance level itself (for example an `aal2` claim bound in the OpenBao role), not only rely on an upstream service having checked. A role that does not MUST record why in an exception. +Decided 2026-10-24: OpenBao `platform-admin` binds `aal2`. **SI-9 Custody class.** Every credential MUST declare a custody class: `offline-key`, `personal-safe`, `openbao-lease`, `delivered-secret` (delivered into the workload by the @@ -132,6 +142,11 @@ cannot be used while its own service is down MUST NOT be the only recovery path. MUST be run by the production operator identity, with that identity's own token as the test subject, and MUST record which identity ran each stage. +**SI-14 Agents.** An agent identity MUST present as an agent: its tokens carry `principal_type: agent` and +name the agent's mode, autonomous or delegated (IAM Profile v0.3). It MUST NOT use a human's credential, MUST +have short-lived tokens (this draft proposes a ceiling of 3600 seconds) and MUST be bounded by a named +boundary policy. A delegated agent names the delegating actor in its tokens. + ## 4. Naming | Class | Pattern | Notes | @@ -141,6 +156,7 @@ subject, and MUST record which identity ran each stage. | `break-glass` | `platform-root` | The king credential | | `human` | the person's own login | `tegwick`, `admin` | | `workload` | the token subject form already in use | for example `service:secrets-engine`; IAM Profile v0.3 governs the token | +| `agent` | the agent's own subject | for example `service:codex:railiance-platform` today; the tokens must carry `principal_type: agent` (SI-14) | Names are reserved for their class. A `svc-` or `-admin` name MUST NOT be used for a human account. A federated role that holds no credential (custody `none`), such as an OpenBao role that maps a group to a @@ -153,7 +169,7 @@ steps are NK-WP-0054-T05 and T06. | Service | `service-admin` | `bind` and write identities | Human route | | --- | --- | --- | --- | -| LLDAP | `ld-admin` (the built-in super-admin, renamed or replaced after rehearsal) | `svc-authelia`, `svc-keycape`, `svc-privacyidea` read-only; a scoped provisioner identity | `admin` as a normal member of the admin group, no shared password | +| LLDAP | `ld-admin` (the built-in super-admin, renamed or replaced after rehearsal) | `svc-authelia`, `svc-keycape`, `svc-privacyidea` read-only; a scoped provisioner identity | `admin` with directory admin rights through the admin group, own password and OTP | | Authelia | none (LDAP backend only) | uses `svc-authelia` | none | | privacyIDEA | `pi-admin` | resolver uses `svc-privacyidea` | none (local administrator only: exception) | | KeyCape | none | uses `svc-keycape` | none | @@ -162,7 +178,9 @@ steps are NK-WP-0054-T05 and T06. ## 6. Declaration Each service publishes one declaration of every identity it holds, in a machine-readable form -defined by NK-WP-0054-T03. For each identity: name, class, purpose, privileges, custody class, +defined by NK-WP-0054-T03. Decided 2026-10-24: declarations start in this repository +(`tools/service-identity/declarations/`) and move to each service's repository as its owner confirms them. +The owner's confirmation is recorded in the declaration (`owner_confirmed`). For each identity: name, class, purpose, privileges, custody class, consumers, owner, rotation path and coupling, second-factor requirement, status (`active`, `legacy`, `planned`) and any exception id. A declaration holds no credential and no secret-shaped value. @@ -173,8 +191,8 @@ For each identity the result is one of `conformant`, `non-conformant` (with the `excepted` (with the exception id) or `unknown`. Unknown is a finding, not a pass. The offline validator `tools/service-identity/validate.py` (NK-WP-0054-T03, provisional) evaluates the -rules that a declaration alone can decide: SI-1, SI-2, SI-3, SI-6, SI-8, SI-9, SI-11, SI-12 and the naming -rules. SI-4, SI-5, SI-7, SI-10 and SI-13 are advisory or process rules and are not decided offline; SI-4 +rules that a declaration alone can decide: SI-1, SI-2, SI-3, SI-6, SI-8, SI-9, SI-11, SI-12, SI-14 and the +naming rules. SI-4, SI-5, SI-7, SI-10 and SI-13 are advisory or process rules and are not decided offline; SI-4 appears as an advisory. Unknown facts are reported as `unknown` for every class. Dates are explicit: the validator takes `--as-of`. @@ -198,9 +216,13 @@ Initial exceptions at adoption: | SI-X1 | SI-1, SI-2, SI-3, SI-9, NAMING | LLDAP `admin` | One account is the human, the super-admin and the shared bind of four services, and its password is in a person's safe and in a Kubernetes Secret | NK-WP-0054-T05 completes | | SI-X2 | SI-10 | LDAP binds | Static long-lived credential; LDAP has no lease | Tenancy Posture §9 gap review | | SI-X3 | NAMING, SI-11 | `trigger-admin` (privacyIDEA) | Legacy account, not named in the admin form, with uninventoried consumers; its disposition is tracked | NK-WP-0049-T22 disposition | -| SI-X4 | SI-8 | OpenBao `netkingdom-platform-admin` | Role binds on the group claim only in the repository file | Decision on §10 question 1 | +| SI-X4 | SI-8 | OpenBao `netkingdom-platform-admin` | Role binds on the group claim only in the repository file; decided 2026-10-24 to bind `aal2`, change owned by railiance-platform | The role binds `aal2` | | SI-X5 | SI-5 | privacyIDEA | Only a local administrator is supported | Upstream capability | +**Finding at adoption, not excepted:** F-1. The coding-agent role in OpenBao binds `principal_type: service`, so +the agent presents as a service against IAM Profile v0.3 (SI-14). The validator reports it as non-conformant +until railiance-platform changes the role or records an exception with a compensating control. + SI-X2 and SI-X5 concern SI-10 and SI-5, which cannot be decided from a declaration. They are recorded here and are not encoded in the declarations; the offline validator would call them stale. The declarations carry the exceptions the validator can evaluate (SI-X1, SI-X3, SI-X4). @@ -208,7 +230,8 @@ declarations carry the exceptions the validator can evaluate (SI-X1, SI-X3, SI-X ## 9. Relationship to other documents - **IAM Profile v0.3** defines the principal types in tokens. This standard governs the - accounts that exist behind them and does not change the token profile. + accounts that exist behind them and does not change the token profile. KeyCape's confidential clients + stay governed by the profile plus a declaration (settled 2026-10-24). - **Credential Management v0.2 §6** lists `LLDAP_LDAP_USER_PASS` as a three-way coupling. On adoption that row is corrected and replaced by the per-service bind accounts. - **Tenancy Posture §9** treats long-lived credentials as a gap; SI-10 applies it to binds. @@ -218,28 +241,32 @@ declarations carry the exceptions the validator can evaluate (SI-X1, SI-X3, SI-X - The registry of `-admin` forms lives with the declarations (T03) and starts with `pi-admin` and the proposed `ld-admin`. -## 10. Open questions +## 10. Decisions and open items -1. **OpenBao assurance.** Should the `platform-admin` role bind `aal2` itself (SI-8) or rely on - KeyCape's `requireForAll`? The other privileged roles already bind it. -2. **Renaming LLDAP's built-in admin.** The directory's super-admin name is configurable, but - renaming it on an existing directory is untested. It needs a rehearsal in isolation before - `ld-admin` is decided (T06). -3. **The human `admin` in LLDAP.** Does the human keep LLDAP admin-UI rights through group - membership, or administer users only through the governed provisioner path? -4. **The provisioner's narrowest scope.** What is the least privilege that still lets it create - users and groups? It must be tested against the pinned LLDAP image. -5. **Workload clients.** KeyCape's confidential clients are governed by IAM Profile v0.3. This - standard only requires that each is declared, owned and not shared. Is that enough? -6. **Where the declarations live.** In each service's repository (preferred, owners are - accountable) or in this repository (simpler to validate). -7. **Roles named like accounts.** The reserved-name rule is scoped to identities that hold a credential, so - OpenBao's `netkingdom-platform-admin` role is not flagged. Is that the intended reading, or should such - roles be renamed too? +**Decided by the founder, 2026-10-24 (review of draft-1):** + +1. OpenBao `platform-admin` binds the `aal2` claim itself (SI-8). The change is railiance-platform's. +2. The human administrator keeps directory admin rights through an administrative group on their own + account (SI-5); the service-admin account is a sealed fallback. +3. Declarations start in this repository and move to each service repository as owners confirm them (§6). +4. Agents get their own class, with SI-14. + +**Settled unless the founder objects:** KeyCape's confidential clients are governed by IAM Profile v0.3 plus a +declaration; the reserved-name rule applies to credential-holding accounts, not to federated roles (§4). + +**To be rehearsed in isolation, not decided:** + +- Renaming LLDAP's built-in admin to `ld-admin` on an existing directory (NK-WP-0054-T06). +- The provisioner's narrowest working privilege against the pinned LLDAP image (T05). +- Whether a read-only bind account can be prevented from signing in interactively. If not, SI-3's + "unable to sign in" becomes "has a long random password that nobody uses and a read-only group". + +**Open:** the ceiling of 3600 seconds in SI-14 is a proposal; the right figure comes from the existing +agent roles (15 minutes today). ## 11. Adoption Structure first, tooling second, as with Tenancy Posture. The order is: ratify the vocabulary and -rules; complete the declarations (T03); run the health check read-only (T04) and publish the +rules; confirm the declarations with their owners (T03); run the health check read-only (T04) and publish the first report; then retire the exceptions one by one, starting with SI-X1 (T05). No exception is closed by writing a declaration. Each is closed by the live change and an evidence run. diff --git a/docs/service-identity-inventory.md b/docs/service-identity-inventory.md index 39c411a..2b8d8fc 100644 --- a/docs/service-identity-inventory.md +++ b/docs/service-identity-inventory.md @@ -84,7 +84,7 @@ Authelia has no local user store or admin UI in this configuration (LDAP backend | OIDC role for credential-broker | human | Group `net-kingdom-admins` **and** `aal2` receive the credential-broker policy | `credential-broker-keycape-client-secret-generate-oidc-role.json` | Established | | OIDC role for the fluid-telegram operators | human | Group `fluid-telegram-operators` and `aal2` | `fluid-telegram-hall-of-helix-oidc-role.json` | Established | | JWT role `secrets-engine-openbao` | workload | Subject `service:secrets-engine`, scope `openbao:login`, policy `secrets-engine-login-self`, five-minute token | `secrets-engine-jwt-role.json` | Established | -| JWT role and AppRole for the coding agent | workload | Subject `service:codex:railiance-platform`; policies `agent-high-risk-boundary`, `workload-kv-read-issue-core-runtime`; fifteen minutes | `coding-agent-jwt-role.json`, `coding-agent-approle.json` | Established | +| JWT role and AppRole for the coding agent | agent (filed as workload in the first draft) | Subject `service:codex:railiance-platform`; policies `agent-high-risk-boundary`, `workload-kv-read-issue-core-runtime`; fifteen minutes | `coding-agent-jwt-role.json`, `coding-agent-approle.json` | Established | | ESO roles, backup publisher and retention policies | workload | One policy per delivery, for example `net-kingdom-backup-publisher-eso` and `net-kingdom-backup-retention-eso` | `openbao/policies/` | Established as files; live roles **unknown** | | Root grant, unseal shares | break-glass | Final authority | `tools/custody-record`, custody docs | Holders **unknown**; see the crisis custody list | @@ -111,7 +111,11 @@ repositories. They are **unknown** here and each owner must supply a declaration credential-broker and fluid-telegram roles also require `aal2`. MFA is enforced earlier by KeyCape (`requireForAll: true`), so this may be intended, but the policy should say whether OpenBao must assert the assurance level itself. This is a question for T02, not a finding of a defect. -6. **The backup contains the admin password** (the `lldap-secrets` Secret). That is by design +6. **The coding agent presents as a service.** The OpenBao role for the coding agent binds `principal_type: + service`, while IAM Profile v0.3 requires agent tokens to carry `principal_type: agent` and to name the + agent's mode. This is finding F-1 in the standard (§8). The role's fifteen-minute token and its boundary + policy already fit the intent of SI-14. +7. **The backup contains the admin password** (the `lldap-secrets` Secret). That is by design for recovery, and it is another reason the human and service uses must be separated. ## What must be confirmed diff --git a/tools/service-identity/README.md b/tools/service-identity/README.md index 0105dbc..101eb08 100644 --- a/tools/service-identity/README.md +++ b/tools/service-identity/README.md @@ -4,7 +4,7 @@ NK-WP-0054-T03. Offline and read-only. Checks declarations against `canon/schemas/service-identity_v0.1.schema.json` and evaluates the rules of the proposed [Service Identity and Privilege standard](../../canon/standards/service-identity-privilege_v0.1.md) that a declaration alone can decide. It was built against draft-1, before the founder's review, -so a rule change in the standard changes this tool. +so a rule change in the standard changes this tool. It now follows draft-2. ```bash make service-identity-test # unit tests @@ -33,15 +33,16 @@ and more than one break-glass identity. ## Rules decided here SI-1, SI-2, SI-3 (including a credential shared by more than one service), SI-6, SI-8 (including an -admin policy without a bound assurance claim), SI-9, SI-11 (overdue legacy disposition), SI-12 and the -naming rules (`NAMING`). SI-4 (one admin per service) is an advisory. SI-5, SI-7, SI-10 and SI-13 are +admin policy without a bound assurance claim), SI-9, SI-11 (overdue legacy disposition), SI-12, SI-14 +(an agent must present as an agent, with a token lifetime at or below the proposed 3600-second ceiling) and +the naming rules (`NAMING`). SI-4 (one admin per service) is an advisory. SI-5, SI-7, SI-10 and SI-13 are not decidable from a declaration. Observed state (what is really configured) is the health check, NK-WP-0054-T04. ## Declarations -`declarations/` holds the first set, authored from `docs/service-identity-inventory.md`. The location is -provisional (standard section 10, question 6). Every one says `owner_confirmed: false`: they describe what +`declarations/` holds the first set, authored from `docs/service-identity-inventory.md`. Decided 2026-10-24: +declarations start here and move to each service's repository as its owner confirms them. Every one says `owner_confirmed: false`: they describe what the repository establishes, not what the owning service has attested. Facts the repository cannot establish are `unknown`, never guessed. diff --git a/tools/service-identity/declarations/openbao.yaml b/tools/service-identity/declarations/openbao.yaml index 901f16e..ae6677d 100644 --- a/tools/service-identity/declarations/openbao.yaml +++ b/tools/service-identity/declarations/openbao.yaml @@ -60,7 +60,8 @@ identities: rotation: {path: "Federated; the KeyCape client secret rotates in the keycape declaration", coupling: [keycape]} status: active - name: coding-agent - classes: [workload] + classes: [agent] + agent: {mode: unknown, presents_principal_type: service, max_token_ttl_seconds: 900, boundary_policy: agent-high-risk-boundary} purpose: "JWT role and AppRole for the coding agent; policies agent-high-risk-boundary and workload-kv-read-issue-core-runtime, fifteen minutes" privilege_level: unknown interactive_login: false diff --git a/tools/service-identity/tests/test_validate.py b/tools/service-identity/tests/test_validate.py index 63a9037..8518493 100644 --- a/tools/service-identity/tests/test_validate.py +++ b/tools/service-identity/tests/test_validate.py @@ -142,6 +142,38 @@ class RuleTests(unittest.TestCase): self.assertNotIn("SI-11", result_of(declaration(legacy))["rules"]) +class AgentTests(unittest.TestCase): + def agent(self, **agent_over): + agent = {"mode": "autonomous", "presents_principal_type": "agent", "max_token_ttl_seconds": 900, + "boundary_policy": "agent-high-risk-boundary"} + agent.update(agent_over) + return identity(name="coding-agent", classes=["agent"], agent=agent, privilege_level="read", + custody=["none"], consumers=[{"name": "agent-host", "kind": "service"}]) + + def test_agent_class_needs_the_agent_object(self): + doc = declaration(identity(name="a", classes=["agent"], custody=["none"])) + self.assertTrue(V.structural_errors(doc, SCHEMA)) + + def test_conformant_agent(self): + self.assertEqual(result_of(declaration(self.agent()))["result"], "conformant") + + def test_agent_presenting_as_a_service_breaks_si14(self): + r = result_of(declaration(self.agent(presents_principal_type="service"))) + self.assertEqual((r["result"], r["rules"]), ("non-conformant", ["SI-14"])) + + def test_agent_token_lifetime_above_the_ceiling_breaks_si14(self): + self.assertIn("SI-14", result_of(declaration(self.agent(max_token_ttl_seconds=7200)))["rules"]) + + def test_unknown_agent_mode_is_unknown_not_a_pass(self): + r = result_of(declaration(self.agent(mode="unknown"))) + self.assertEqual((r["result"], r["unknown"]), ("unknown", ["agent.mode"])) + + def test_agent_must_not_live_in_a_persons_safe_or_serve_a_person(self): + doc = declaration({**self.agent(), "custody": ["personal-safe"], + "consumers": [{"name": "p", "kind": "person"}]}) + self.assertLessEqual({"SI-9", "SI-2"}, set(result_of(doc)["rules"])) + + class ResultTests(unittest.TestCase): def test_unknown_is_a_finding_for_every_class(self): for fact, value in (("privilege_level", "unknown"), ("custody", ["unknown"])): @@ -192,6 +224,11 @@ class CheckedInDeclarations(unittest.TestCase): self.assertEqual(rejected, []) self.assertEqual({r["service"] for r in reports} >= {"lldap", "keycape", "privacyidea", "openbao", "platform"}, True) + def test_the_coding_agent_role_is_a_finding_against_the_iam_profile(self): + reports, _ = V.run(self.paths(), NOW) + agent = [i for r in reports for i in r["identities"] if i["identity"] == "coding-agent"][0] + self.assertEqual((agent["result"], agent["rules"]), ("non-conformant", ["SI-14"])) + def test_the_shared_admin_credential_is_excepted_today_and_fails_after_expiry(self): def admin(as_of): reports, _ = V.run(self.paths(), as_of) diff --git a/tools/service-identity/validate.py b/tools/service-identity/validate.py index 00a6c39..d501ac9 100644 --- a/tools/service-identity/validate.py +++ b/tools/service-identity/validate.py @@ -40,6 +40,7 @@ SECRET_VALUE = re.compile( BIND_NAME = re.compile(r"^svc-[a-z0-9]+(-[a-z0-9]+)*$") ADMIN_NAME = re.compile(r"^[a-z]{2,6}-admin$") +AGENT_MAX_TTL_SECONDS = 3600 # draft-2 proposal (standard section 10); the figure is not settled ORDER = {"non-conformant": 0, "unknown": 1, "excepted": 2, "planned": 3, "conformant": 4} @@ -126,8 +127,20 @@ def _violations(identity: dict[str, Any]) -> tuple[set[str], list[str]]: broken.add("SI-8") if identity.get("grants_admin_policy") and not identity.get("assurance_claim_bound"): broken.add("SI-8") - if ("bind" in classes or "workload" in classes) and "personal-safe" in custody: - broken.add("SI-9") # a person's safe must not be the home of a service credential + if ("bind" in classes or "workload" in classes or "agent" in classes) and "personal-safe" in custody: + broken.add("SI-9") # a person's safe must not be the home of a service or agent credential + if "agent" in classes: + agent = identity["agent"] + if agent["presents_principal_type"] == "unknown": + unknown.append("agent.presents_principal_type") + elif agent["presents_principal_type"] != "agent": + broken.add("SI-14") # IAM Profile v0.3: agent tokens carry principal_type agent + if agent["mode"] == "unknown": + unknown.append("agent.mode") + if agent["max_token_ttl_seconds"] > AGENT_MAX_TTL_SECONDS: + broken.add("SI-14") + if persons: + broken.add("SI-2") if "break-glass" in classes: if "unknown" not in custody and custody != ["offline-key"]: broken.add("SI-6") diff --git a/workplans/NK-WP-0054-service-identity-policy.md b/workplans/NK-WP-0054-service-identity-policy.md index 825de34..38037e0 100644 --- a/workplans/NK-WP-0054-service-identity-policy.md +++ b/workplans/NK-WP-0054-service-identity-policy.md @@ -79,7 +79,7 @@ assignee: claude-code needs_human: true decision_id: "a3582e79-4ece-4902-9bfa-e962aef2b8f2" depends_on: [NK-WP-0054-T01] -blocking_reason: "Draft-1 delivered 2026-10-24. Await the founder review and answers to the six open questions in section 10." +blocking_reason: "Draft-2 reflects the founder review of 2026-10-24 (four decisions). Await ratification and review by the service owners the inventory names." state_hub_task_id: "5830e2eb-334a-52fa-9c55-a8c00d89c09f" ``` @@ -93,7 +93,12 @@ Profile v0.3, Credential Management v0.2 and the Security Zones standard. **Done when:** the draft is reviewed by the founder and the service owners it names. -Draft-1 delivered 2026-10-24: `canon/standards/service-identity-privilege_v0.1.md` (proposed). Thirteen +Draft-2 (2026-10-24, after the founder's review): decisions applied are OpenBao `platform-admin` binds +`aal2`; the human administrator keeps directory admin rights through an admin group; declarations start here +and move to the services; agents get their own class and rule SI-14. Rehearsals still to do (not decisions): +renaming LLDAP's built-in admin, the provisioner's narrowest scope, and whether a bind account can be kept +from signing in interactively. Finding F-1: the coding-agent role in OpenBao presents as a service against +IAM Profile v0.3. Draft-1 was delivered earlier the same day: `canon/standards/service-identity-privilege_v0.1.md` (proposed). Thirteen rules (SI-1 to SI-13), naming, a target shape for today's services, the declaration fields, the conformance result states, five initial exceptions (SI-X1 to SI-X5) and six open questions. It extends IAM Profile v0.3 and Credential Management v0.2 without changing either. Review is the @@ -169,7 +174,9 @@ Create `svc-authelia`, `svc-keycape` and `svc-privacyidea` as read-only director narrowly scoped provisioner account that can create users and groups without being the directory super-admin (the inventory found these four consumers beside the human; the narrowest working privilege for the provisioner must be tested against the pinned LLDAP image -first). Switch the Authelia, KeyCape and privacyIDEA resolver configurations and the +first). Per the founder's decision the human `admin` keeps directory admin rights through an admin group +on its own account; because today's `admin` is also LLDAP's built-in super-admin, the migration must state +which of the two accounts keeps the name `admin` and rehearse it before any change. Switch the Authelia, KeyCape and privacyIDEA resolver configurations and the provisioner to them, make `admin`'s password human-only and rotate it, and remove the shared secret from the service Secrets. Update the backup profile, the restore adapters (`isolated-lldap.py`, `isolated-authelia.py`, @@ -199,7 +206,8 @@ For each service decide whether a dedicated admin account exists and what it is LLDAP (`ld-admin`), privacyIDEA (`pi-admin`, existing), Authelia, KeyCape (client registry), OpenBao (the `platform-admin` role) and others. Record the result in the inventory and the standard. LLDAP's built-in admin name is configurable but renaming it -on an existing directory is untested here, so it needs a rehearsal before any decision. +on an existing directory is untested here, so it needs a rehearsal before any decision. Also rehearse +whether a read-only bind account can be kept from signing in interactively (SI-3). **Done when:** every service has a recorded disposition and a custody location.