NK-WP-0054-T02: draft-2 of the Service Identity and Privilege standard after the founder's review
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 328925@bnt-lap001
Assistant-Session: 14b795fd-c632-4c01-8d28-e7e490c7b0ed
This commit is contained in:
tegwick 2026-10-24 22:50:01 +02:00
parent 48385816fd
commit ca03da52c6
8 changed files with 194 additions and 46 deletions

View file

@ -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"
]
}
}
]
},

View file

@ -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 `<short>-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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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")

View file

@ -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.