diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index b32304e..2ac899c 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -308,7 +308,7 @@ | task | NK-WP-0053-T02 | wait | — | workplans/NK-WP-0053-identity-restore-drill-cadence.md | | task | NK-WP-0053-T03 | wait | — | workplans/NK-WP-0053-identity-restore-drill-cadence.md | | task | NK-WP-0053-T04 | todo | — | workplans/NK-WP-0053-identity-restore-drill-cadence.md | -| task | NK-WP-0054-T01 | todo | — | workplans/NK-WP-0054-service-identity-policy.md | +| task | NK-WP-0054-T01 | done | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T02 | todo | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T03 | wait | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T04 | wait | — | workplans/NK-WP-0054-service-identity-policy.md | diff --git a/docs/service-identity-inventory.md b/docs/service-identity-inventory.md new file mode 100644 index 0000000..39c411a --- /dev/null +++ b/docs/service-identity-inventory.md @@ -0,0 +1,127 @@ +# Service identity inventory + +NK-WP-0054-T01. First inventory, 2026-10-24. **Read-only.** It is built from repository +manifests, scripts, ConfigMaps, templates and documents, plus the sibling repository +`railiance-platform` read as files. No Kubernetes Secret, live service or archive was +read, and no credential value appears here. Where the repository cannot establish a +fact, the row says **unknown** and names who must confirm it. + +"Established" means the cited file says it. It does not mean the live estate matches; +live state is T04's job. + +## Classes used + +| Class | Meaning | +| --- | --- | +| human | A person's login | +| break-glass | Final authority, kept offline, not routine | +| bind | A service's directory lookup credential | +| service-admin | An administrative account for one service | +| workload | A service or agent identity obtaining tokens or secrets | +| infrastructure | A database or platform role below the application | + +## People + +| Identity | Class | Established facts | Source | Status | +| --- | --- | --- | --- | --- | +| `platform-root` (king credential) | break-glass | Dedicated final-custody credential, independent of day-to-day accounts; held in an independently held password safe or offline packet. Not for production use (founder, 2026-10-24) | `docs/platform-root-custody.md`, Standard §4 | Established | +| `tegwick` | human | Setup operator, Git/Gitea, tooling; "not the desired long-term platform root of trust" | `docs/platform-root-custody.md` | Established | +| `admin` | human **and** bind **and** directory super-admin | The human platform admin is the same account as LLDAP `uid=admin` (founder, 2026-10-24); see LLDAP | founder statement; LLDAP rows | Established | +| Founder admission key, principal `bernd` | break-glass | SSH signing key for attempt admissions; verifier anchor on the workstation | secrets-engine `docs/founder-admission.md` | Which identity owns it is **unknown**; founder confirms | + +## LLDAP (directory) + +| Identity | Class | Privilege | Credential custody | Consumers | Source | Status | +| --- | --- | --- | --- | --- | --- | --- | +| `uid=admin,ou=people,dc=netkingdom,dc=local` | bind + service-admin + human | Full directory administration, web UI at the IP-restricted admin host | `lldap-secrets` key `LLDAP_LDAP_USER_PASS`; password from KeePassXC or custody for the web UI; present in every identity backup bundle | the human; **Authelia** (LDAP bind); **KeyCape** (LDAP lookups); **privacyIDEA** resolver `lldap-coulomb`; **identity-provisioner** (admin API, as `LLDAP_ADMIN_PASSWORD`) | `authelia/configmap.yaml`, `keycape/bootstrap-config.json` (template), `privacyidea/realm_bootstrap.py`, `identity-provisioner/deployment.yaml`, `lldap/OPERATOR-GROUPS.md` | Established from manifests. KeyCape's live bind is in the Secret `keycape-config`, **not read** | +| Other users | human | Seven users existed in the 2026-10-16 backup (count only) | LLDAP database | n/a | restore receipt `actual-isolated-lldap-recovery.json` | **Unknown** names; directory owner confirms | +| Groups | policy | Seventeen groups in the same backup (count). Named in the repository: `net-kingdom-admins`, `net-kingdom-users`, `activity-core-operators`, `fluid-telegram-operators` | `lldap/OPERATOR-GROUPS.md`, OpenBao role files | gate Authelia domain rules and OpenBao roles | as cited | Names beyond these **unknown** | + +## Authelia + +| Identity | Class | Privilege | Custody | Consumers | Source | Status | +| --- | --- | --- | --- | --- | --- | --- | +| LDAP bind as `uid=admin` | bind | Directory super-admin (more than a lookup needs) | `authelia-secrets` `ldap_password` | Authelia | `authelia/configmap.yaml` | Established | +| OIDC client `keycape` | workload | Only registered client; `one_factor`; MFA is done by KeyCape and privacyIDEA | verifier in `authelia-secrets`; plaintext in `keycape-config` | KeyCape | `authelia/configmap.yaml` | Established | +| Notifier credential | workload | SMTP submission as `platform@coulomb.social` | `authelia-smtp` | Authelia | `authelia/configmap.yaml`, NK-WP-0047 | Gated; activation state **unknown** | + +Authelia has no local user store or admin UI in this configuration (LDAP backend only). + +## KeyCape + +| Identity | Class | Privilege | Custody | Consumers | Source | Status | +| --- | --- | --- | --- | --- | --- | --- | +| LDAP bind as `uid=admin` | bind | Directory super-admin | `keycape-config` | KeyCape | `keycape/bootstrap-config.json` (template) | Template only; live value **unknown** | +| Authelia client `keycape` | workload | Upstream sign-in | `keycape-config` | KeyCape | same | Established | +| privacyIDEA factor-reader token | workload | Reads factor state; exact rights **unknown** | `keycape-factor-read`, delivered by railiance-platform (RPF-WP-0040); OpenBao policy `keycape-factor-renewer` | KeyCape | `keycape/deployment.yaml`, `openbao/policies/` | Rights **unknown**; railiance-platform and key-cape confirm | +| Confidential clients with secrets in the backup profile: `secrets-engine-openbao`, `secrets-engine-approval`, `secrets-engine-requester`, `approval-engine-operator`, `informed-decision-sitting-requester`, `rapp-qonto` | workload | Per client scopes and audiences | one Kubernetes Secret each | their consuming services | `identity-backup-profile.json`, `keycape/deployment.yaml` | Names established; scopes **unknown** except `secrets-engine-openbao` | +| Template clients: `demo-app`, `user-engine-portal`, `netkingdom-bootstrap-console`, `openbao-admin`, `rapp-qonto-client` | workload | Unknown | registry in `keycape-config` | various | `keycape/bootstrap-config.json` | Production registry **not read**; key-cape confirms which exist | + +## privacyIDEA + +| Identity | Class | Privilege | Custody | Consumers | Source | Status | +| --- | --- | --- | --- | --- | --- | --- | +| `pi-admin` | service-admin | Local administrator; policy `pi-admin-all-rights` restricted to `adminuser=pi-admin` | founder's personal password safe (founder, 2026-10-24) | the founder, for token repair | `privacyidea/README.md`, `bootstrap-admin.py` | Established. MFA enrolment for `pi-admin` is **unknown** (README requires attended acceptance) | +| `trigger-admin` (legacy) | service-admin | Legacy Keycloak-era account, policy and Secret | Kubernetes Secret | none known | `privacyidea/README.md` | Not deleted; consumers uninventoried; revocation is NK-WP-0049-T22 | +| Resolver `lldap-coulomb` bind as `uid=admin` | bind | Directory super-admin | resolver config in the database, encrypted with the enckey | privacyIDEA | `privacyidea/realm_bootstrap.py` | Established | +| Database role `privacyidea` | infrastructure | Owner of `privacyidea_db` | `net-kingdom-pg-privacyidea-app` | privacyIDEA | `postgresql/cluster.yaml` | Established | +| End-user tokens | human factor | Five tokens in the 2026-10-16 backup (count) | database, encrypted | KeyCape MFA check | restore receipt | `admin@coulomb` token added 2026-10-24, not yet in any backup | + +## identity-provisioner, user-engine, tenant-engine + +| Identity | Class | Privilege | Custody | Consumers | Source | Status | +| --- | --- | --- | --- | --- | --- | --- | +| LLDAP admin password as `LLDAP_ADMIN_PASSWORD` | bind + service-admin | Full admin API, used to create users and groups | `lldap-secrets` | identity-provisioner | `identity-provisioner/deployment.yaml` | Established | +| `identity-provisioner-token` | workload | Service token; only user-engine pods may reach the provisioner | Kubernetes Secret | user-engine | same | Established | +| user-engine ServiceAccount and database role `user-engine-pg-app` | workload, infrastructure | Token automount off; database owner | Secrets | user-engine | `user-engine/runtime.yaml`, `backup.yaml` | Established | +| tenant-engine | workload | Owner pointers only in this repository | n/a | n/a | `tenant-engine/runtime.yaml` | Owner repository holds it; **unknown** here | + +## OpenBao (owner: railiance-platform; read as files) + +| Identity or role | Class | Privilege | Source | Status | +| --- | --- | --- | --- | --- | +| OIDC role `netkingdom-platform-admin` | human | Group `net-kingdom-admins` receives policies `platform-admin` and `operator-custody`, one-hour token | `openbao/auth/netkingdom-platform-admin-role.json` | Established. The role file binds on the group only; it has no `aal2` claim bound | +| 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 | +| 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 | + +## Other consumers + +flex-auth (six consumers enforcing caller authentication, per `SCOPE.md`), the secrets-engine +runtime and the platform `rapp-*` packages hold their own identities in their own +repositories. They are **unknown** here and each owner must supply a declaration (T03). + +## Observations that change the plan + +1. **One credential, five consumers.** The `admin` password is used by the human, Authelia, + KeyCape, the privacyIDEA resolver and the identity-provisioner. The bind split in T05 + therefore needs four service accounts, not two: `svc-authelia`, `svc-keycape` and + `svc-privacyidea` as read-only lookups, and a provisioner account that can create users and + groups but is not the directory super-admin. +2. **The provisioner is the hard case.** It uses the admin API, so a read-only group is not + enough. What the narrowest working privilege is must be tested against the pinned LLDAP + image before T05. +3. **Three bind consumers need only read access** but hold super-admin today. +4. **`trigger-admin` is an uninventoried legacy admin.** It is outside this inventory until its + consumers are established (NK-WP-0049-T22). +5. **OpenBao `platform-admin` binds on a group claim alone** in the repository file, while the + 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 + for recovery, and it is another reason the human and service uses must be separated. + +## What must be confirmed + +| Unknown | Who confirms | +| --- | --- | +| Names of the seven users and seventeen groups; which are in `net-kingdom-admins` | LLDAP and User Engine owners | +| Live KeyCape registry, live LDAP bind, factor-reader rights | key-cape and railiance-platform | +| Scopes of the confidential clients other than `secrets-engine-openbao` | key-cape | +| Whether `trigger-admin` still exists and who uses it | founder (NK-WP-0049-T22) | +| Live OpenBao roles versus these files | railiance-platform | +| flex-auth and `rapp-*` identities | their owners | +| MFA state of `pi-admin` | founder | diff --git a/workplans/NK-WP-0054-service-identity-policy.md b/workplans/NK-WP-0054-service-identity-policy.md index e8d5634..725d733 100644 --- a/workplans/NK-WP-0054-service-identity-policy.md +++ b/workplans/NK-WP-0054-service-identity-policy.md @@ -45,7 +45,7 @@ not contradict, that profile and Credential Management Standard v0.2. ```task id: NK-WP-0054-T01 -status: todo +status: done priority: high assignee: claude-code state_hub_task_id: "422918a5-c695-5fe3-ba3c-2b370b2ef4dc" @@ -60,6 +60,15 @@ 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 ```task @@ -136,8 +145,12 @@ blocking_reason: "Production change across LLDAP, Authelia, privacyIDEA and thei state_hub_task_id: "c43d339a-fc8a-5526-885a-64693c930005" ``` -Create `svc-authelia` and `svc-privacyidea` as read-only directory binds, switch the -Authelia and privacyIDEA resolver configurations to them, make `admin`'s password +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). 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