2026-10-24 19:30:52 +02:00
---
id: NK-WP-0054
type: workplan
title: "Service identity and privilege policy, conformance check and bind-account split"
domain: infotech
repo: net-kingdom
2026-10-24 20:06:15 +02:00
status: active
2026-10-24 19:30:52 +02:00
flavor: implementation
owner: claude
topic_slug: netkingdom
created: "2026-10-24"
updated: "2026-10-24"
related:
- NK-WP-0049
- NK-WP-0053
- NK-WP-0031
2026-10-24 19:32:22 +02:00
state_hub_workstream_id: "36f61e43-f1f3-53fc-ad77-d1d797b3d5b1"
2026-10-24 19:30:52 +02:00
---
# NK-WP-0054 — Service identity and privilege policy
Intake from the founder's direction on 2026-10-24, during NK-WP-0049 backup validation.
Three identities exist today: `platform-root` (all-mighty build-mode access, not for
production use), `tegwick` (developer) and `admin` (platform administrator, intended
for operations and for backup validation and crisis recovery). Findings that led here:
- The T19 MFA proof ran as `platform-root` , the identity meant to leave production use.
- The human `admin` is LLDAP `uid=admin` , which is also the directory bind account for
Authelia and the privacyIDEA resolver. One account has one password, so the human
password is the service secret held in Kubernetes Secrets and in every backup bundle.
An OTP on `admin` protects KeyCape logins but not the bind path.
- `pi-admin` already exists for privacyIDEA and is separate; its credentials are in the
founder's personal password safe.
- No concrete crisis custody list existed. A first draft is `docs/crisis-custody-list.md` .
Direction: dedicated `svc-<service>` bind accounts, a dedicated admin account per
service (`ld-admin` , `pi-admin` , …), a NetKingdom standard that governs which identities
and privileges may exist for the services that are part of NetKingdom, and a health check
that validates conformance. This plan prepares and proposes; it grants nothing and changes
no live service. IAM Profile v0.3 already distinguishes human, service and agent
principals in tokens; this standard governs the accounts behind them and must extend,
not contradict, that profile and Credential Management Standard v0.2.
## Inventory current service identities and privileges
```task
id: NK-WP-0054-T01
2026-10-24 19:58:23 +02:00
status: done
2026-10-24 19:30:52 +02:00
priority: high
assignee: claude-code
2026-10-24 19:32:22 +02:00
state_hub_task_id: "422918a5-c695-5fe3-ba3c-2b370b2ef4dc"
2026-10-24 19:30:52 +02:00
```
Read-only inventory from repository manifests, ConfigMaps and owner-provided metadata.
No Secret is read. For each NetKingdom service (LLDAP, Authelia, KeyCape, privacyIDEA,
OpenBao, user-engine, tenant-engine, flex-auth and the platform consumers) list every
identity: name, class (human, break-glass, service bind, service admin, workload),
privileges, the credential's custody location, its consumers and its owner.
**Done when:** `docs/service-identity-inventory.md` lists every identity with its source,
and states plainly which entries are unknown and who must confirm them.
2026-10-24 19:58:23 +02:00
Completed 2026-10-24. `docs/service-identity-inventory.md` covers people, LLDAP, Authelia, KeyCape,
privacyIDEA, the identity-provisioner and engines, and OpenBao (railiance-platform files read
read-only). No Secret or live service was read. It ends with a table of what is unknown and who
confirms it. Main finding: the `admin` credential has **five** consumers (the human, Authelia,
KeyCape, the privacyIDEA resolver and the identity-provisioner), so T05 needs four service
accounts, not two. Also recorded: the provisioner needs more than read access, `trigger-admin`
is an uninventoried legacy admin, and OpenBao `platform-admin` binds on a group claim alone
(a question for T02, not a defect).
2026-10-24 19:30:52 +02:00
## Draft the Service Identity and Privilege standard
```task
id: NK-WP-0054-T02
2026-10-25 00:03:24 +02:00
status: done
2026-10-24 19:30:52 +02:00
priority: high
assignee: claude-code
depends_on: [NK-WP-0054-T01]
2026-10-24 19:32:22 +02:00
state_hub_task_id: "5830e2eb-334a-52fa-9c55-a8c00d89c09f"
2026-10-24 19:30:52 +02:00
```
Write `canon/standards/service-identity-privilege_v0.1.md` as a proposed standard:
identity classes; naming (`svc-<service>` for binds, `<service>-admin` for admins);
the rules the founder's findings imply (one identity, one purpose; a bind account is
read-only and never a login; no human uses a service credential; humans and admins
require MFA; every identity has an owner, a custody class and a rotation path; the
break-glass identity is not routine); the exception record; and its relation to IAM
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.
2026-10-24 23:41:55 +02:00
Founder review of draft-3 completed 2026-10-24: profile approach accepted, `platform-root` and `admin` definitions
confirmed, approved in principle. What remains for **done** is the owners' side: railiance-platform and
security-canon replies (the other four hub messages, to state-hub, key-cape, glas-harness and emergent-security, are
recommendations and do not gate ratification). On their replies, set the standard's status to accepted and close T02.
2026-10-24 23:55:44 +02:00
Owner replies received 2026-10-24: security-canon confirmed all five mode readings and corrected the SEC-DSM
citations; railiance-platform prepared F-1 and the aal2 binding in source (RPF-WP-0065, nothing applied); key-cape,
glas-harness and state-hub assessed the agent and attribution gaps (KEY-WP-0043, GLAS-WP-0018, STATE-WP-0100).
Draft-4 of the standard applies all of it without changing a founder decision. What remains for T02 is ratification:
set the status to accepted when the founder decides.
2026-10-25 00:03:24 +02:00
Completed 2026-10-25: the founder ratified the standard as v0.1 (status accepted). Ratification applies no exception's
elimination path and changes no live service; owners adopt on their own schedule and the declarations stay
unconfirmed until they do.
2026-10-24 23:12:37 +02:00
Draft-3 (2026-10-24): realigned as an estate profile of SecurityCanon after a review against the canon
(`history/2026-10-24-security-identity-and-agent-review.md` ): imported vocabulary, SEC-DSM references per rule,
2026-10-24 23:58:08 +02:00
estate identities in canon terms, new SI-15 (accountable automation). Validator migration is T08.
2026-10-24 22:50:01 +02:00
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):
2026-10-24 22:57:23 +02:00
introducing `ld-admin` (LLDAP only) as the configured super-admin and demoting `admin` , the provisioner's narrowest scope, and whether a bind account can be kept
2026-10-24 22:50:01 +02:00
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
NK-WP-0054-T02: draft-1 of the Service Identity and Privilege standard
Thirteen rules (one identity one class, people and services never share a
credential, read-only per-service bind identities, one service-admin per
service, break-glass is not routine, assurance on privileged roles, custody
class, declaration, recovery, validation run by the operator identity), naming
svc-<service> and <short>-admin, five initial exceptions and six open questions.
Proposed; extends IAM Profile v0.3 and Credential Management v0.2. Hub decision
routes the review.
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
2026-10-24 20:02:30 +02:00
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
founder's, then the service owners named in the inventory.
2026-10-24 19:30:52 +02:00
## Schema and offline conformance validator
```task
id: NK-WP-0054-T03
NK-WP-0054-T03: service identity schema, offline validator and first declarations
Schema, validator (30 tests), make targets and eight declarations authored from the
inventory, all unconfirmed by their owners. First run: 0 non-conformant, 12 unknown,
2 excepted, 5 planned, 6 conformant. Provisional: built against draft-1 before the
founder's review. Building it changed the draft: custody is a list, SI-X1 now covers
SI-9 and NAMING, and the reserved-name rule applies only to credential-holding
identities (open question 7). Unknown is a finding for every class.
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
2026-10-24 22:38:23 +02:00
status: done
2026-10-24 19:30:52 +02:00
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T02]
2026-10-24 19:32:22 +02:00
state_hub_task_id: "a5c51d69-28ff-5458-83eb-10a35c8c5492"
2026-10-24 19:30:52 +02:00
```
A declaration schema in `canon/schemas/` and an offline validator in
`tools/service-identity/` , in the style of `tools/tenancy-posture` and
`tools/custody-record` : each service declares its identities, the validator checks the
declaration against the standard, refuses secret-shaped values and reports counts only.
**Done when:** valid and invalid fixtures pass and fail as expected, and the declarations
for the current services are checked in.
NK-WP-0054-T03: service identity schema, offline validator and first declarations
Schema, validator (30 tests), make targets and eight declarations authored from the
inventory, all unconfirmed by their owners. First run: 0 non-conformant, 12 unknown,
2 excepted, 5 planned, 6 conformant. Provisional: built against draft-1 before the
founder's review. Building it changed the draft: custody is a list, SI-X1 now covers
SI-9 and NAMING, and the reserved-name rule applies only to credential-holding
identities (open question 7). Unknown is a finding for every class.
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
2026-10-24 22:38:23 +02:00
Completed 2026-10-24, **provisionally** : built against draft-1 before the founder's review, so a rule
change in the standard changes the tool. `canon/schemas/service-identity_v0.1.schema.json` ;
`tools/service-identity/validate.py` (30 tests); `make service-identity-test` and
`make service-identity-check` ; eight declarations in `tools/service-identity/declarations/` , authored from
the inventory and all marked `owner_confirmed: false` . First run as of 2026-10-24: 0 non-conformant, 12
unknown, 2 excepted, 5 planned, 6 conformant. Unknown outnumbers conformant because the repository
cannot establish privileges for the KeyCape clients and others; that is the honest result, not a pass.
Building it changed the draft in three ways: custody became a list (the `admin` password lives in a
Secret and a person's safe), the SI-X1 exception grew to include SI-9 and NAMING, and the reserved-name
rule was scoped to credential-holding identities (standard section 10, question 7). SI-4, SI-5, SI-7,
SI-10 and SI-13 are not decidable from a declaration.
2026-10-24 23:12:37 +02:00
## Migrate declarations and validator to canon grants (draft-3)
```task
2026-10-24 23:58:08 +02:00
id: NK-WP-0054-T08
2026-10-24 23:35:30 +02:00
status: done
2026-10-24 23:12:37 +02:00
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T03]
2026-10-24 23:14:04 +02:00
state_hub_task_id: "d45efcad-fb96-51b2-ab3d-4b183d3f586e"
2026-10-24 23:12:37 +02:00
```
Draft-3 of the standard is an estate profile of SecurityCanon: accounts carry an imported `principal_type`
and authority grants written `MODE @ SCOPE` with activation and environment, and break-glass is an
activation. The schema, validator and declarations still use draft-2's classes. Migrate them, keeping the
rule ids, the exception ids and the current results comparable (the first run under grants should report the
same non-conformant, excepted and unknown accounts unless a difference is explained).
**Done when:** the schema expresses principal type and grants, the declarations follow §3 of the standard,
the tests pass, and a before/after comparison is recorded.
2026-10-24 23:35:30 +02:00
Completed 2026-10-24. Schema, validator, fixtures, declarations and tests (40 pass) follow draft-3: imported
`principal_type` , grants `MODE @ SCOPE` with activation and environments, estate conventions `bind` and
`service-admin` , `unknown` placeholders, and an `elimination_path` on every exception. SI-4 and SI-7 became
decidable. Before/after, as of 2026-10-24 over the same eight declarations:
| Result | Draft-2 fields | Draft-3 grants |
| --- | --- | --- |
| non-conformant | 1 | 1 (`openbao/coding-agent` , SI-14) |
| unknown | 11 | 10 |
| excepted | 2 | 2 (`lldap/admin` SI-X1, `openbao/netkingdom-platform-admin` SI-X4) |
| planned | 5 | 5 |
| conformant | 6 | 7 |
The only account that moved is KeyCape's `secrets-engine-openbao` client, unknown to conformant: its scope
(`openbao:login` only) is established, so it can be stated as a grant instead of an unknown privilege level.
`lldap/admin` is now also excepted under SI-4, which grants made decidable; SI-X1 lists it.
2026-10-24 19:30:52 +02:00
## Live conformance health check
```task
id: NK-WP-0054-T04
status: wait
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T03]
NK-WP-0054-T03: service identity schema, offline validator and first declarations
Schema, validator (30 tests), make targets and eight declarations authored from the
inventory, all unconfirmed by their owners. First run: 0 non-conformant, 12 unknown,
2 excepted, 5 planned, 6 conformant. Provisional: built against draft-1 before the
founder's review. Building it changed the draft: custody is a list, SI-X1 now covers
SI-9 and NAMING, and the reserved-name rule applies only to credential-holding
identities (open question 7). Unknown is a finding for every class.
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
2026-10-24 22:38:23 +02:00
blocking_reason: "Validator delivered (T03, provisional). Await the T02 review and owner agreement on what read-only metadata each service can provide."
2026-10-24 19:32:22 +02:00
state_hub_task_id: "c8db4607-9306-572b-a3cb-c35ca5989ec7"
2026-10-24 19:30:52 +02:00
```
Compare declared against observed: for example, which DN each consumer's configuration
binds as, which group the bind accounts belong to, and which policies the admin roles
carry. Observed state comes from owner-provided read-only metadata and repository
manifests, never from reading Secrets. Findings feed the existing proposal-only posture
feedback (`tools/posture-feedback` ) with an evidence-freshness bound. It changes nothing.
**Done when:** a dated run reports conformant, non-conformant and unknown per identity,
with each owner named for every finding.
2026-10-28 09:53:35 +01:00
**2026-10-28 (KeyCape):** key-cape `a1ab8ff` (KEY-WP-0045) adds `keycape identity-facts` , a value-safe
JSON export of the client registry, directory bind, factor service and admin surfaces. Four of
the five KeyCape unknowns are now declared from source (scopes limited to registered scopes,
`token.go:355-363` ); `rapp-qonto` stays `unknown` until the live view (reference config names it
`rapp-qonto-client` , `qonto:read` ). Findings handed on: KeyCape's LLDAP bind is the directory
admin (scope of T05, KEY-WP-0045-T03); `approval-engine-operator` is withdrawn but registered
(SI-11, KEY-WP-0013-T02). The founder's live view streams the deployed config through the
exporter without writing it to disk; the exact command is in key-cape KEY-WP-0045-T02.
2026-10-28 09:54:14 +01:00
**2026-10-28 (OpenBao, PostgreSQL):** railiance-platform `bd8ca57` (RPF-WP-0067-T02) adds a
value-safe metadata exporter (`scripts/service_identity_metadata_export.py` , 10 tests) for
OpenBao auth roles/policies with effective rules and PostgreSQL role flags and memberships.
Declared from source: `fluid-telegram-operators` OPERATE on four exact KV entries;
`coding-agent` OBSERVE only (its boundary policy denies the KV path its second policy grants,
which is therefore inert). `coding-agent` `agent.mode` stays unknown (KeyCape KEY-WP-0043).
The founder's live export commands are in RPF-WP-0067-T03.
2026-10-24 19:30:52 +02:00
## Split the directory bind from the human admin
```task
id: NK-WP-0054-T05
2026-10-31 08:31:17 +01:00
status: done
2026-10-24 19:30:52 +02:00
priority: high
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
depends_on: [NK-WP-0054-T01]
2026-10-24 19:32:22 +02:00
state_hub_task_id: "c43d339a-fc8a-5526-885a-64693c930005"
2026-10-24 19:30:52 +02:00
```
2026-10-24 19:58:23 +02:00
Create `svc-authelia` , `svc-keycape` and `svc-privacyidea` as read-only directory binds and a
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
2026-10-24 22:57:23 +02:00
first). Per the founder's decisions the human `admin` keeps its name and its administrative rights through an admin
group on its own password and OTP, and `ld-admin` is LLDAP's configured super-admin only. The migration
therefore introduces `ld-admin` by configuration on the existing directory and demotes `admin` ; this is
rehearsed in isolation before any change. Switch the Authelia, KeyCape and privacyIDEA resolver configurations and the
2026-10-24 19:58:23 +02:00
provisioner to them, make `admin` 's password
2026-10-24 19:30:52 +02:00
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` ,
`isolated-privacyidea-mfa.py` , which read the directory password from `lldap-secrets` and
rewrite the resolver address) and the restore packet. Prepare guarded helpers and
filled-in commands; the founder runs the live steps.
**Done when:** both services bind with their own read-only accounts, the human `admin`
password is not present in any service Secret, a fresh backup restores and passes the T17
and T19 drills under the new accounts, and the old shared credential is retired.
2026-10-28 18:53:10 +01:00
**2026-10-28 founder decision `9f0f7a87` : go, including the live run this week.** Order: guarded
helpers and filled-in commands; an isolated rehearsal on a restored copy of the directory covering
the `ld-admin` introduction, demotion of `admin` to an admin group, the narrowest provisioner
privilege on the pinned LLDAP image, and SI-3 (can a `svc-*` bind be kept from interactive login);
then the founder runs the live steps as soon as the rehearsal passes. KeyCape's bind is confirmed
as the directory admin from source (key-cape `a1ab8ff` , KEY-WP-0045-T03). The rotation lane is
`lldap-directory-bind` / `lldap-service-binds` in `tools/rotation-lanes/lanes.yaml` .
2026-10-28 19:01:40 +01:00
**2026-10-28 synthetic rehearsal (phase 1 of 4):** `sso-mfa/k8s/lldap/bind-split-rehearsal.py` (agent-run,
pinned LLDAP v0.6.2, generated passwords, 127.0.0.1 only, removed afterwards;
[receipt ](../docs/evidence/2026-10-28-nk54-bind-split-rehearsal/synthetic-rehearsal.json )). Proven:
`ld-admin` can be introduced into a directory whose super-admin is `admin` and made the configured
super-admin by restart on the same data; `admin` keeps its rights through `lldap_admin` with its
password unchanged; `lldap_strict_readonly` binds read all users and groups and cannot write.
Findings: (1) `lldap_password_manager` cannot create users or add members, so the provisioner's
narrowest working group is `lldap_admin` (own account, but super-admin rights: needs an exception or
a provisioner redesign); (2) SI-3: every `svc-*` account can sign in to the LLDAP web UI; the UI is
IP-restricted, and Authelia's user filter must require `net-kingdom-users` membership; (3) production
`deployment.yaml` runs the unpinned `lldap/lldap:stable` , while the rehearsal and the restore tools pin
v0.6.2: pin production before the live run.
Remaining phases, each its own budgeted task: ** (2)** consumer switch in source (Authelia configmap user
and users filter, KeyCape `create-secrets.py` bind, privacyIDEA `realm_bootstrap.py` resolver,
identity-provisioner deployment, LLDAP image pin) with a synthetic Authelia check of the filter;
**(3)** the guarded live helper (plan/apply/verify, value-free receipts, passwords only by hidden
prompt or fd) plus the backup profile and the restore adapters `isolated-lldap.py` ,
`isolated-authelia.py` , `isolated-privacyidea-mfa.py` ; ** (4)** the founder runbook with filled-in
commands, a founder-run rehearsal on a restored backup, then the live run.
2026-10-28 19:25:30 +01:00
2026-10-28 founder decision: `svc-provisioner` gets its own account in `lldap_admin` , recorded as
exception SI-X6 (SI-3, expires 2027-04-30) in the standard and `declarations/lldap.yaml` .
2026-10-28 20:33:04 +01:00
**2026-10-28 phase 2 (source switch), parts a and b:** `identity-provisioner` logs in as
`LLDAP_ADMIN_USERNAME` (default `admin` ) and refuses chosen usernames starting `svc-` or ending
`-admin` (`2ebc4f0` ; its image needs a rebuild through the owner pipeline). Custody: the LLDAP
custody file now carries `LLDAP_LDAP_USER_PASS` as the `ld-admin` password plus
`LLDAP_SVC_{AUTHELIA,KEYCAPE,PRIVACYIDEA,PROVISIONER}_PASS` ; the human `admin` password leaves
custody. Consumers: Authelia binds as `svc-authelia` (`ldap_password` from its own key), KeyCape as
`svc-keycape` (`create-secrets.py` , `bootstrap-config.json` ), the privacyIDEA resolver as
`svc-privacyidea` (`realm_bootstrap.py` ), the provisioner from the new Secret
`identity-provisioner-ldap` (`create-secrets.sh` , deployment, `identity-provisioner-reconcile.py` ).
LLDAP's deployment sets `LLDAP_LDAP_USER_DN=ld-admin` . The image is **not** pinned yet: production
runs `lldap/lldap:stable` , and pinning to v0.6.2 could downgrade the database; the runbook reads the
running digest first. Service-account emails use `@service.netkingdom.invalid` . Left: 2c (Authelia
users filter requiring `net-kingdom-users` , synthetic check), then phases 3 and 4.
2026-10-29 21:52:52 +01:00
**2026-10-29 phase 2c:** Authelia's `users_filter` now excludes `svc-*` and `ld-admin` instead of
requiring `net-kingdom-users` , because tenant users are provisioned only into `<tenant>:users`
and an inclusion filter could lock them out. `bind-split-rehearsal.py --stage authelia` runs the
pinned Authelia 4.39.28 with the repository's own `configmap.yaml` , bound as `svc-authelia`
against a synthetic LLDAP ([receipt ](../docs/evidence/2026-10-28-nk54-bind-split-rehearsal/synthetic-authelia-filter.json )):
human login 200, wrong password 401, `svc-keycape` /`svc-privacyidea` /`ld-admin` 401; under the previous
filter all three were 200. The read-only `svc-authelia` bind is sufficient for Authelia's lookups.
Phase 2 is complete; phases 3 and 4 remain.
2026-10-29 22:00:49 +01:00
**2026-10-29 phase 3a (live helper):** `sso-mfa/k8s/lldap/bind-split.py` , founder-run, changes the
directory only. `prepare-custody` adds `LLDAP_SVC_*_PASS` and a new `ld-admin` `LLDAP_LDAP_USER_PASS`
to the private custody copy (40 alphanumerics, the shape `apply-service-secrets.literal()` accepts)
and refuses a second run; `plan` is read-only; `accounts [--apply]` creates `ld-admin` and the `svc-*`
accounts with `@service.netkingdom.invalid` emails, sets exactly their groups and their passwords
(pinned `lldap_set_password` , password in the environment only) and refuses an existing account with
a foreign email or extra groups; `verify [--old-admin-retired]` proves every account signs in with
its custody password, `admin` keeps `lldap_admin` , and the old shared password is refused. The
human `admin` signs in at a hidden prompt; receipts are value-free. `make bind-split-test` runs it
end to end on a throwaway pinned LLDAP (3 tests). `apply-service-secrets.py` gains the named
rotations `lldap LLDAP_LDAP_USER_PASS` and `authelia LLDAP_SVC_AUTHELIA_PASS -> ldap_password` , so the
existing Secrets are switched only by explicit, conditional rotation (97 tests). Left: 3b (backup
profile and the three restore launchers), then phase 4.
2026-10-29 22:11:19 +01:00
**2026-10-29 phase 3b (restore path):** no launcher change is needed, which corrects this task's
earlier assumption. `isolated-lldap.py` takes the configured super-admin name from the archived
deployment's `LLDAP_LDAP_USER_DN` and its password from the archived `lldap-secrets` , and prompts for
the password of the user under test; `isolated-authelia.py` binds with the archived configmap user and
`authelia-secrets` ; `isolated-privacyidea-mfa.py` rewrites only the resolver `LDAPURI` and keeps the
archived bind DN and password. After the split all three therefore follow `ld-admin` , `svc-authelia`
and `svc-privacyidea` from the backup itself. Regression test: the configured admin follows the archived
deployment (`tests/test_isolated_lldap_recovery.py` ). `bind-split-rehearsal.py --stage startup`
([receipt ](../docs/evidence/2026-10-28-nk54-bind-split-rehearsal/synthetic-startup.json )) proves on
v0.6.2 that once `lldap_admin` has a member, LLDAP ignores the configured admin password and does not
create a missing configured admin: a backup taken at any point of the switch restores, and
**`bind-split.py accounts` must run before the LLDAP deployment switch**. The backup profile is
unchanged: its components carry their own Secrets with the new values, and identity-provisioner is not
an identity-restore component (its password is re-derivable from custody). Phase 3 is complete;
phase 4 (runbook, founder rehearsal on a restored backup, live run) remains.
2026-10-29 22:20:09 +01:00
**2026-10-29 phase 4 (runbook and remaining consumer tools):** `sso-mfa/k8s/lldap/BIND-SPLIT-RUNBOOK.md`
gives the founder-run order with checks and rollbacks: preconditions (restore drill on the current
backup, LLDAP pinned to the running digest only if it is v0.6.2, rebuilt provisioner image), prepare
custody, plan, accounts, verify, then one consumer at a time (provisioner, Authelia, KeyCape, privacyIDEA
resolver, LLDAP), then the human `admin` password change and `verify --old-admin-retired` , then the
restore drill on the next generation. New tools: `sso-mfa/k8s/keycape/directory-bind-rollout.py`
(`028a7a9` ; edits only `lldap.bindDN` /`bindPW` in `sso/keycape-config` , inspect/dry-run/attended
apply with uid and resourceVersion test, `--operation revert` ; 8 tests) and
`realm_bootstrap.py --switch-bind` (`e556af8` ; only from the legacy `uid=admin` bind, only
`BINDDN` /`BINDPW` ; 3 tests). Since phase 2b, `realm_bootstrap.py` refuses the live resolver until
`--switch-bind` runs. The founder's restore-drill rehearsal is the NK-WP-0053 run on the current
backup (precondition), and the post-split drill closes this task. Agent preparation is complete.
2026-10-30 19:54:33 +01:00
**2026-10-30 live run: passed** ([evidence ](../docs/evidence/2026-10-30-nk54-bind-split/README.md )).
The founder ran steps 1-10; `verify --old-admin-retired` passed: `ld-admin` is the configured
super-admin, Authelia, KeyCape, the privacyIDEA resolver and identity-provisioner bind with their own
accounts, `admin` 's password is human-only and the old shared password is refused. Deviations: the
custody copy had to be decrypted first and `KUBECONFIG` exported (runbook setup corrected); step 6 was
refused because the custody `AUTHELIA_KEYCAPE_CLIENT_SECRET` no longer matches the live hash, and
`ldap_password` was rotated alone through the same guarded `publish()` path. Open: the restore drill on
the next backup generation (closes this task, retires SI-X1) and reconciling the stale custody client
secret.
2026-10-31 08:31:17 +01:00
**Done 2026-10-31: post-split restore drill passed** ([evidence ](../docs/evidence/2026-10-31-identity-restore-drill/drill.json )).
The founder, operating as `admin` , ran all five NK-WP-0053 stages on generation `20261031T034845Z-a13dc92e2c61` ,
the first daily capture after the live split: independent retrieval, LLDAP (`admin` with its new human-only
password), privacyIDEA (7/7 tokens, 414,440 signed audit rows), Authelia on the archived `svc-authelia`
configuration (T17), and `admin` 's OTP through the restored `svc-privacyidea` resolver (T19). Every receipt
reports cleanup verified and the transfer digest. Not counted: a stage 1 run without an OpenBao `platform-admin`
token (the tool fails silently; the drill document now has the login step), the stage 2 run it fed, and an
MFA run that tested `platform-root` . SI-X1 is retired in the standard and `tools/service-identity/declarations/lldap.yaml`
(`admin` and the four `svc-*` binds conformant). Follow-up: Credential Management v0.2 section 6 still lists
`LLDAP_LDAP_USER_PASS` as the three-way shared credential.
2026-10-24 19:30:52 +02:00
## Disposition of per-service admin accounts
```task
id: NK-WP-0054-T06
2026-10-28 18:53:10 +01:00
status: done
2026-10-24 19:30:52 +02:00
priority: medium
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
depends_on: [NK-WP-0054-T02]
2026-10-24 19:32:22 +02:00
state_hub_task_id: "8072b5cf-4161-58b9-8dfc-62fa102ccd16"
2026-10-24 19:30:52 +02:00
```
For each service decide whether a dedicated admin account exists and what it is called:
LLDAP (`ld-admin` ), privacyIDEA (`pi-admin` , existing), Authelia, KeyCape (client
registry), OpenBao (the `platform-admin` role) and others. Record the result in the
2026-10-24 22:57:23 +02:00
inventory and the standard. `ld-admin` is for LLDAP only; `admin` stays the human's name on every service. Introducing `ld-admin` as the
configured super-admin on an existing directory is untested here, so it needs a rehearsal first. Also rehearse
2026-10-24 22:50:01 +02:00
whether a read-only bind account can be kept from signing in interactively (SI-3).
2026-10-24 19:30:52 +02:00
**Done when:** every service has a recorded disposition and a custody location.
2026-10-28 18:53:10 +01:00
**Done 2026-10-28, founder decision `9f0f7a87` :** dispositions accepted as proposed and recorded in
the standard (section "Per-service admin disposition"): LLDAP `ld-admin` ; privacyIDEA `pi-admin` ;
Authelia and KeyCape none (configuration-only, no local admin plane); OpenBao federated
`platform-admin` with root only via `generate-root` ; PostgreSQL the CNPG `postgres` superuser through
railiance-platform. The `ld-admin` introduction and the SI-3 check are rehearsed under T05.
2026-10-24 19:30:52 +02:00
## Maintain the crisis custody list
```task
id: NK-WP-0054-T07
2026-10-28 18:53:10 +01:00
status: progress
2026-10-24 19:30:52 +02:00
priority: high
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
2026-10-28 18:53:10 +01:00
blocking_reason: "No TO CONFIRM row remains (2026-10-28). Open: the machine-readable freshness check and the first admin-operated drill (NK-WP-0053) verifying the list."
2026-10-24 19:32:22 +02:00
state_hub_task_id: "ae8524ff-12d8-59c2-b46b-88f3bfd44dd8"
2026-10-24 19:30:52 +02:00
```
Keep `docs/crisis-custody-list.md` current, and propose a non-secret machine-readable
record with a freshness check, modelled on `tools/custody-record` , so a drill can verify
that each row has a holder, a location and a last-proven date.
**Done when:** no row is marked `TO CONFIRM` , the freshness check exists and the first
`admin` -operated drill (NK-WP-0053) has verified the list.
2026-10-31 13:29:30 +01:00
## Assess the post-split password-reset boundary
```task
id: NK-WP-0054-T09
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0054-T05]
2026-10-31 13:30:58 +01:00
state_hub_task_id: "5c7b015a-67e6-5ec1-b28a-b4d1835606ef"
2026-10-31 13:29:30 +01:00
```
Completed 2026-10-31 as the consumer-return assessment for CSOC-WP-0015-T06.
T05's completed bind migration and post-split restore drill prove their stated
login/MFA scope. Neither proves a password change through Authelia. The founder
receipt at 2026-10-30T18:53:16Z records svc-authelia in lldap_strict_readonly.
Read-only native metadata at 2026-10-31T12:27:58Z finds the svc-authelia ConfigMap
and Ready Authelia generation 31 / LLDAP generation 9, at the exact image digests
used by Coulomb's isolated native reset proof. That proof shows first factor and
reset-link verification succeed, but password change returns HTTP 200/KO, leaving
the old password valid. The ordinary-user password-manager fixture succeeds;
this is not approval for that role or evidence of every privilege boundary.
Evidence: docs/evidence/2026-10-31-reset-authority/consumer-assessment.json, linked
founder receipts and Coulomb native proofs. No production reset, email, directory
membership read, credential access or runtime change occurred. Current membership
since the dated owner receipt is unknown; no live failed reset is claimed.
T05 remains done for its original scope. T10 owns the newly explicit reset return.
## Restore and verify supported post-split password reset
```task
id: NK-WP-0054-T10
2026-10-31 13:40:07 +01:00
status: progress
2026-10-31 13:29:30 +01:00
priority: high
assignee: codex
depends_on: [NK-WP-0054-T09]
2026-10-31 13:30:58 +01:00
state_hub_task_id: "6e4e5070-68d9-5b50-bb99-6844656b03e5"
2026-10-31 13:29:30 +01:00
```
Residual of the completed bind split, required by CSOC-WP-0015-T06 and the existing
personal password-reset behavior. Review a supported, bounded reset authority or
recovery path without restoring the shared human-admin credential. Preparation can
start now; a runtime grant/change requires a separate reviewed founder decision.
The earlier split approval is not approval to widen svc-authelia's privileges.
First verify current effective membership through an owner-approved value-free
observation. Rehearse the proposed authority on the pinned native images, including
ordinary-user password change, privileged-account limits, service interactive-login
exclusion, rejected unverified/expired/replayed requests, and unrelated directory
write denial. Compare all rights, not only HTTP status. Prepare exact target-bound
execution/rollback, custody/declaration updates and the post-change backup/restore
check under the existing bind/custody ownership. Do not change the canonical
read-only split helper or declaration to imply an unapproved live grant.
Done when the reviewed path has an attended native return, a fresh reset actually
changes the eligible user's password and permits app login, old-password and replay
checks refuse, MFA/TLS/directory boundaries are preserved, and the current archived
configuration restores the same supported behavior. Retain secret-free evidence
and explicit productive verdict; hand the result to CSOC-WP-0015-T06. Existing
personal reset acceptance remains historical evidence, not a post-split return.
2026-10-31 13:40:07 +01:00
2026-10-31 T10 preparation checkpoint: the matrix in T11 establishes the proposed
role's actual scope on the pinned binaries. Password-manager refuses LLDAP admins
but can change non-LDAP-admin service and application-admin passwords. Therefore
no one-line role promotion is an execution-ready remedy. Next: value-free current
membership/subject classification, owner review of the service write grant and
2026-10-31 13:54:51 +01:00
a supported directory-admin recovery path if the accepted subject needs it, then the guarded packet.
2026-10-31 13:40:07 +01:00
No existing identity grant, declaration, helper or runtime was changed.
## Rehearse native password-reset authority and unrelated write limits
```task
id: NK-WP-0054-T11
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0054-T09]
2026-10-31 13:42:37 +01:00
state_hub_task_id: "3c95bbb2-7680-5e2b-bd3a-3dabac820891"
2026-10-31 13:40:07 +01:00
```
Completed 2026-10-31. Ten direct password-change cases and eight native Authelia
reset flows cover strict-readonly/password-manager against ordinary users, an
LLDAP admin, an application-admin outside that group, and another service account
(the service case uses the direct directory API). Password-manager changes all
non-LLDAP-admin targets; the LLDAP admin refuses. Strict-readonly refuses all.
Actual old/new authentication verifies each result; first factor, unverified
reset and consumed-link refusal remain checked. No production account is mapped
to a synthetic class by this result.
Both roles refuse seven unrelated writes with seven successful admin controls:
user create/delete/other-profile edit, group create/delete, self-promotion and
removal of another admin's membership. User/group metadata remains unchanged.
Authelia excludes interactive service login; direct LLDAP access and password
API scope remain distinct. This is a bounded operation matrix, not an exhaustive
rights audit or productive acceptance. All containers use disposable loopback
confinement with no host mounts, ports, external routes or provider SMTP.
Evidence: docs/evidence/2026-10-31-reset-authority/native-matrix.json, produced by
coulomb-social/scripts/rehearse-reset-authority.py with source manifest retained in
that repository's docs/release/reset-authority-matrix-2026-10-31.sources.txt.
Consumer validation binds the full gate (546 passed, no failures/errors/skips),
both original native reset cases and three CLI refusal checks. T10 retains current
membership observation, complete grant/recovery design, admission, supervised
execution and native backup/restore acceptance; none is closed by this rehearsal.
2026-10-31 13:54:51 +01:00
2026-10-31 subject-binding correction: the retained October6 acceptance identifies
`tegwick` . Treating a new recovery route for every directory administrator as a
Coulomb prerequisite before observing that subject would expand the original
contract. Technical decision8770d4ea-8538-4f88-a9f8-8bcfb0542603 binds the check to
tegwick; directory-admin recovery blocks this consumer only if that subject is
in lldap_admin. The denial remains an explicit product/owner boundary and is not
silently waived. Broad password-manager authority still needs reviewed admission.
T12 is the completed read-only observer preparation; T13 owns the actual result.
## Prepare the fixed-subject reset-role observer
```task
id: NK-WP-0054-T12
status: done
priority: high
assignee: codex
2026-10-31 14:00:35 +01:00
state_hub_task_id: "95675a19-b406-5425-aaf4-e5313d12aea4"
2026-10-31 13:54:51 +01:00
```
Completed 2026-10-31. `sso-mfa/k8s/lldap/reset-authority-observe.py` reuses the
existing origin-restricted, TLS-verified, no-redirect transport. It queries only
admin, svc-authelia and the accepted personal account tegwick; no mutation or
credential-discovery operation exists. The human admin password is prompted only
in an attended terminal. Receipt creation requires a private owned directory,
refuses an existing file/symlink/repository location and writes mode0600. Receipts
contain role booleans/counts and source/subject hashes, not passwords, bearer
tokens, raw usernames or arbitrary group names. Provider failures are redacted.
92 unit/transport checks pass, including the reused transport's redirect/origin
controls. All four native bind-split tests pass; the final observer node separately
passes against the pinned LLDAP image, proving ordinary/admin classification,
unknown-user refusal and unchanged directory metadata. Its fixture container and
volume are removed. Ruff passes. Evidence: docs/evidence/2026-10-31-reset-authority/
observer-preparation.json. The runtime observation remains T13; fixture success
neither grants password-write authority nor proves reset or application access.
## Obtain the attended current reset-role observation
```task
id: NK-WP-0054-T13
2026-10-31 14:00:35 +01:00
status: done
2026-10-31 13:54:51 +01:00
priority: high
assignee: codex
2026-10-31 14:00:35 +01:00
needs_human: false
state_hub_task_id: "278d5ba8-1c2a-58d9-a24e-2233af2c1aef"
2026-10-31 13:54:51 +01:00
```
T12 is complete. The next result is `netkingdom.reset-role-observation.v1` with
status `current_reset_roles_observed` , source hashes, svc-authelia role flags and
tegwick role flags. Validate the exact target/source binding, current membership
and any multiple-built-in-role ambiguity before constructing the runtime proposal.
A refusal or missing user is a diagnostic outcome, not permission to replace the
accepted subject or widen access. Do not collect the password in chat or retrieve
it from custody. This human step supplies private authentication, not a new policy
approval. The directory-admin route is conditional on the accepted subject's
actual classification; every runtime grant/change remains separately reviewed.
2026-10-31 14:00:35 +01:00
Completed 2026-10-31 at12:57:44Z: founder ran the observer after correcting a
line-wrapped receipt path (the first attempt refused before authentication).
Independent private-file readback verifies mode0600/owner/single link, exact
endpoint and tegwick subject hashes, all four prepared source hashes and no
unexpected fields. Current roles: svc-authelia is strict-readonly with no other
groups; tegwick is in none of the three built-in privilege groups and has one
other group. No multiple-role ambiguity. Evidence: docs/evidence/2026-10-31-
reset-authority/current-roles.json and consumer reset-role-return-verification-
2026-10-31.json. T13 is done; no grant or reset was performed. T10 now prepares
the reviewed password-manager transition for this consumer, retaining explicit
non-admin service/application-account reach and directory-admin exclusion. The
accepted tegwick flow needs no new directory-admin recovery route on this evidence.
2026-10-31 14:26:17 +01:00
## Prepare and rehearse the guarded reset-role transition
```task
id: NK-WP-0054-T14
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0054-T11, NK-WP-0054-T13]
2026-10-31 14:34:44 +01:00
state_hub_task_id: "9d9570d9-7ea4-558c-8573-c5428cafb788"
2026-10-31 14:26:17 +01:00
```
Owner: Codex. Prepare the exact svc-authelia role replacement and read-only
recovery; validate the journal, crash recovery and pinned native transport.
Measure role refresh in a single running Authelia process and disclose retained
HTTP-token rights after membership rollback. Completion requires source-bound
validation and an executable reviewed proposal, not a live grant. This extracts
packet preparation from T10; its original operational acceptance remains open.
Completed 2026-10-31: controller and proposal prepared; 25 unit and four native
checks pass, zero failures/errors/skips, Ruff passes. Real process death after
either LDAP effect recovers read-only without grant replay. Same-process Authelia
rehearsal proves refusal/success/refusal across grant/rollback, with old/new
passwords and consumed challenges. Native token test proves the explicit rollback
limit: old HTTP token rights survive membership removal; fresh tokens lose them.
Evidence: docs/evidence/2026-10-31-reset-authority/transition-validation.json and
role-refresh.json; source and review in coulomb-social/docs/release/native-password-
reset-review-2026-10-30.md. Proposal digest:
sha256:753f50f139a0d090a8b64ba15174f2a7e8b0e639eb0dd01d17783476b5a3759f.
No production membership, password, declaration or runtime was changed.
## Admit and execute the reviewed reset-role transition
```task
id: NK-WP-0054-T15
status: wait
priority: high
assignee: codex
needs_human: true
depends_on: [NK-WP-0054-T14]
blocked_on: human
blocking_reason: Founder admission of the complete non-admin password-write scope and attended terminal execution are required.
2026-10-31 14:34:44 +01:00
state_hub_task_id: "5d60f800-3838-5831-952d-9a9d6b9a6b75"
2026-10-31 14:26:17 +01:00
```
Owner: founder for admission and private authentication; Codex verifies the
returned durable receipt. The earlier read-only observation authorizes no grant.
Review coulomb-social/docs/release/native-password-reset-review-2026-10-30.md.
Completion requires the exact proposal digest, native role postflight and private
receipt readback. A refused or interrupted operation stays open and follows the
reviewed recovery. Membership rollback does not invalidate pre-existing HTTP
tokens or undo productive password changes. No live token-revocation operation
is approved by this packet; its limitation is part of the admission question.
## Align the reset authority sources and verify productive recovery
```task
id: NK-WP-0054-T16
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0054-T15]
blocked_on: dependency
blocking_reason: Requires the reviewed T15 grant and its verified native receipt.
2026-10-31 14:34:44 +01:00
state_hub_task_id: "c9b4253e-956f-584b-82ae-d9b450dd9309"
2026-10-31 14:26:17 +01:00
```
Owner: Codex prepares/verifies, founder supplies productive verdict and private
inputs. After T15's verified result, align the service identity declaration and
existing bind verification/repair expectations with the admitted role. Keep the
single Authelia consumer and current credential custody; never infer broader
human-admin authority. Verify the accepted tegwick reset, new/old password result,
consumed-link refusal, app access and MFA, then a fresh identity archive and
isolated restore of the same supported behavior. Retain only value-free receipts.
A proposed/source-only grant or fixture pass cannot close this task. It supplies
the remaining acceptance of T10 and therefore CSOC-WP-0015-T06; no completed T05
or historical October6 acceptance is reopened or counted twice.