net-kingdom/workplans/NK-WP-0054-service-identity-policy.md
tegwick 3a83fed2c6
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0054-T05: rehearsal findings and remaining phases recorded; lint fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 308701@bnt-lap001
Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
2026-10-28 19:01:40 +01:00

19 KiB

id type title domain repo status flavor owner topic_slug created updated related state_hub_workstream_id
NK-WP-0054 workplan Service identity and privilege policy, conformance check and bind-account split infotech net-kingdom active implementation claude netkingdom 2026-10-24 2026-10-24
NK-WP-0049
NK-WP-0053
NK-WP-0031
36f61e43-f1f3-53fc-ad77-d1d797b3d5b1

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

id: NK-WP-0054-T01
status: done
priority: high
assignee: claude-code
state_hub_task_id: "422918a5-c695-5fe3-ba3c-2b370b2ef4dc"

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.

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

Draft the Service Identity and Privilege standard

id: NK-WP-0054-T02
status: done
priority: high
assignee: claude-code
depends_on: [NK-WP-0054-T01]
state_hub_task_id: "5830e2eb-334a-52fa-9c55-a8c00d89c09f"

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.

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.

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.

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.

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, estate identities in canon terms, new SI-15 (accountable automation). Validator migration is T08. 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): 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 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 founder's, then the service owners named in the inventory.

Schema and offline conformance validator

id: NK-WP-0054-T03
status: done
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T02]
state_hub_task_id: "a5c51d69-28ff-5458-83eb-10a35c8c5492"

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.

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.

Migrate declarations and validator to canon grants (draft-3)

id: NK-WP-0054-T08
status: done
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T03]
state_hub_task_id: "d45efcad-fb96-51b2-ab3d-4b183d3f586e"

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.

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.

Live conformance health check

id: NK-WP-0054-T04
status: wait
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T03]
blocking_reason: "Validator delivered (T03, provisional). Await the T02 review and owner agreement on what read-only metadata each service can provide."
state_hub_task_id: "c8db4607-9306-572b-a3cb-c35ca5989ec7"

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

Split the directory bind from the human admin

id: NK-WP-0054-T05
status: progress
priority: high
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
depends_on: [NK-WP-0054-T01]
state_hub_task_id: "c43d339a-fc8a-5526-885a-64693c930005"

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 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 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, 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 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 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). 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.

Disposition of per-service admin accounts

id: NK-WP-0054-T06
status: done
priority: medium
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
depends_on: [NK-WP-0054-T02]
state_hub_task_id: "8072b5cf-4161-58b9-8dfc-62fa102ccd16"

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

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.

Maintain the crisis custody list

id: NK-WP-0054-T07
status: progress
priority: high
assignee: claude-code
needs_human: true
decision_id: "9f0f7a87-7adb-44f6-8360-db6a80e3ceca"
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."
state_hub_task_id: "ae8524ff-12d8-59c2-b46b-88f3bfd44dd8"

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.