diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index d2c0443..ead8037 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -315,7 +315,7 @@ | task | NK-WP-0054-T02 | done | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T03 | done | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T04 | wait | — | workplans/NK-WP-0054-service-identity-policy.md | -| task | NK-WP-0054-T05 | progress | — | workplans/NK-WP-0054-service-identity-policy.md | +| task | NK-WP-0054-T05 | done | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T06 | done | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T07 | progress | — | workplans/NK-WP-0054-service-identity-policy.md | | task | NK-WP-0054-T08 | done | — | workplans/NK-WP-0054-service-identity-policy.md | diff --git a/canon/standards/service-identity-privilege_v0.1.md b/canon/standards/service-identity-privilege_v0.1.md index 1860a63..8e998d9 100644 --- a/canon/standards/service-identity-privilege_v0.1.md +++ b/canon/standards/service-identity-privilege_v0.1.md @@ -288,13 +288,15 @@ DSM-EXC-001 binds violations of a canon MUST; an exception against a rule that i | Id | Rules | Account | Reason | Elimination path | | --- | --- | --- | --- | --- | -| SI-X1 | SI-1, SI-2, SI-3, SI-4, SI-9, naming | LLDAP `admin` | The human, the super-admin and the shared bind of four services in one account; password in a person's safe and a Kubernetes Secret | NK-WP-0054-T05: `svc-*` binds, provisioner account, `ld-admin`, `admin` human-only | | SI-X2 | SI-10 | LDAP bind accounts | Static long-lived credential; LDAP has no lease | Governed residual secret (DSM-TRANS-002) until a federated directory lookup exists | | SI-X3 | naming, SI-11 | `trigger-admin` (privacyIDEA) | Legacy account, consumers uninventoried | NK-WP-0049-T22 deleted the unreferenced Kubernetes copy (2026-10-27); revoking the privacyIDEA account stays under CUST-WP-0073-T05 / NK-WP-0055-T06 | | SI-X5 | SI-4, SI-5 | privacyIDEA | Only a local administrator is supported | Upstream support for attributed administrators | | SI-X6 | SI-3 | LLDAP `svc-provisioner` | LLDAP v0.6.2 has no group narrower than `lldap_admin` that can create users or add members (rehearsal 2026-10-28); own account and Secret, but super-admin rights | A narrower LLDAP role, or a provisioner that no longer creates users (founder decision 2026-10-28) | -**Closed:** SI-X4 (OpenBao `netkingdom-platform-admin` bound the group claim only) closed 2026-10-27: the founder applied the +**Closed:** SI-X1 (LLDAP `admin` as human, super-admin and shared bind of four services) closed 2026-10-31: the +bind split ran live on 2026-10-30 (`verify --old-admin-retired` passed) and the restore drill on the first post-split +generation passed all five stages as `admin` (NK-WP-0054-T05, +[evidence](../../docs/evidence/2026-10-31-identity-restore-drill/drill.json)). SI-X4 (OpenBao `netkingdom-platform-admin` bound the group claim only) closed 2026-10-27: the founder applied the `aal2` binding (RPF-WP-0065-T04; receipts show `bound_claims` = `net-kingdom-admins` plus `/assurance/level: aal2`, and a fresh MFA login verified). @@ -316,7 +318,7 @@ owner accepts that exception or the issuer and role change land. - **IAM Profile v0.3:** governs tokens, including the agent principal. 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; the inventory found five - consumers. That row is corrected when SI-X1 is eliminated. + consumers. SI-X1 was eliminated on 2026-10-31; that row is now out of date and is a follow-up of NK-WP-0054. - **Crisis custody list** (`docs/crisis-custody-list.md`) is the recovery companion to SI-9 and SI-12, and the enumeration of roots that DSM-ROOT-001 asks for. diff --git a/docs/evidence/2026-10-31-identity-restore-drill/attempts/lldap-1-refused.json b/docs/evidence/2026-10-31-identity-restore-drill/attempts/lldap-1-refused.json new file mode 100644 index 0000000..8a4b6df --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/attempts/lldap-1-refused.json @@ -0,0 +1,29 @@ +{ + "schema": "netkingdom.isolated-lldap-recovery.v1", + "status": "failed", + "production_contacted": false, + "secret_values_exported": false, + "container_cleanup_verified": false, + "temporary_plaintext_cleanup_verified": false, + "source_sha256": "35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "error": "isolated_lldap_check_failed_private_diagnostics_suppressed", + "failed_stage": "input_validation", + "finished_at": "2026-10-31T07:11:42.343583+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/attempts/privacyidea-mfa-platform-root.json b/docs/evidence/2026-10-31-identity-restore-drill/attempts/privacyidea-mfa-platform-root.json new file mode 100644 index 0000000..7ce1591 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/attempts/privacyidea-mfa-platform-root.json @@ -0,0 +1,59 @@ +{ + "schema": "netkingdom.isolated-privacyidea-mfa.v1", + "status": "isolated_privacyidea_mfa_verified", + "production_contacted": false, + "secret_values_exported": false, + "existing_token_mfa_proven": true, + "user_to_token_resolver_proven": true, + "container_cleanup_verified": true, + "temporary_plaintext_cleanup_verified": true, + "source_sha256": "d55c5f93c085907be6ae19b0eba6ef360fe863ef8922aa5b6ec205f4837efa16", + "reused_source_sha256": { + "isolated-privacyidea.py": "4df7196678aac1ad594f32abf45c68dc21a9d2760d9e8a536a215a4a3c5d277f", + "isolated-lldap.py": "35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "identity-bundle.py": "524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "verify-identity.py": "039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f", + "retrieve-identity-offline.py": "3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972" + }, + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + }, + { + "reference": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "manifest_file_count": 36, + "all_manifest_hashes_verified": true, + "ldap_resolvers_rewritten_to_loopback": 1, + "container_isolation_verified_before_private_import": true, + "user_resolved_through_restored_resolver_and_directory": true, + "selected_user_sha256": "2868946f999c23cc527736fa53c6e35ca7960cf01d06b33ea792e1e164aabcc1", + "user_token_count": 1, + "user_usable_token_count": 1, + "user_usable_token_types": [ + "totp" + ], + "wrong_key_refused": true, + "wrong_key_refusal": "error_907", + "wrong_code_refused": true, + "wrong_code_refusal": "wrong_value", + "existing_token_native_check_accepted": true, + "only_loopback_interface_verified": true, + "external_routes_absent": true, + "finished_at": "2026-10-31T07:26:57.126681+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/attempts/transfer-1-failed.json b/docs/evidence/2026-10-31-identity-restore-drill/attempts/transfer-1-failed.json new file mode 100644 index 0000000..bf46a11 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/attempts/transfer-1-failed.json @@ -0,0 +1,5 @@ +{ + "status": "failed", + "source_sha256": "ec123d47a07305253570dacab7e103f8c193a23f20c9fc87466ea4a95ae0ec13", + "error": "contained_owner_retrieval_failed" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/authelia-recovery.json b/docs/evidence/2026-10-31-identity-restore-drill/authelia-recovery.json new file mode 100644 index 0000000..bdab7de --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/authelia-recovery.json @@ -0,0 +1,54 @@ +{ + "schema": "netkingdom.isolated-authelia-recovery.v1", + "status": "isolated_authelia_verified", + "production_contacted": false, + "secret_values_exported": false, + "container_cleanup_verified": true, + "temporary_plaintext_cleanup_verified": true, + "source_sha256": "6139294f6ebdcbaad35934f04b11177d8cf89d134506a730f0c0ebbb3a4967be", + "reused_source_sha256": { + "isolated-lldap.py": "35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "identity-bundle.py": "524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "verify-identity.py": "039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f", + "retrieve-identity-offline.py": "3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972" + }, + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + }, + { + "reference": "docker.io/authelia/authelia@sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54", + "image_id": "sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "manifest_file_count": 36, + "all_manifest_hashes_verified": true, + "container_isolation_verified_before_private_import": true, + "original_configuration_validated": true, + "storage_encryption_key_verified": true, + "existing_user_first_factor": true, + "wrong_password_refused": true, + "archived_client_secret_accepted": true, + "wrong_client_secret_refused": true, + "grant_issued": false, + "only_loopback_interface_verified": true, + "external_routes_absent": true, + "issuer_key_matches_archive": true, + "issuer_jwk_thumbprint_sha256": "El5Prcg0wUe1l9mAV1IC55i45-zrLXwcBnl7WamASm8", + "notifications_confined_to_tmpfs_file": true, + "notification_written": false, + "finished_at": "2026-10-31T07:24:21.595569+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/drill.json b/docs/evidence/2026-10-31-identity-restore-drill/drill.json new file mode 100644 index 0000000..d26b128 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/drill.json @@ -0,0 +1,32 @@ +{ + "schema": "netkingdom.identity-restore-drill.v1", + "work": "NK-WP-0054-T05 (post-split drill), NK-WP-0053 cadence", + "date": "2026-10-31", + "operator": "admin (founder)", + "verdict": "pass", + "generations": { + "20261031T034845Z-a13dc92e2c61": "stages 1-5" + }, + "generation_context": "First daily capture after the 2026-10-30 LLDAP bind split (steps 1-10 and verify --old-admin-retired): the archived configuration binds ld-admin, svc-authelia and svc-privacyidea, and admin's password is the human-only one set at step 10.", + "stages": [ + {"stage": 1, "name": "independent retrieval", "result": "pass", "receipt": "transfer.json", + "identity": "admin, OpenBao platform-admin session (auth/netkingdom, aal2) reading the Backup account custody"}, + {"stage": 2, "name": "LLDAP", "result": "pass", "receipt": "lldap-recovery.json", "login_tested": "admin (post-split password)"}, + {"stage": 3, "name": "privacyIDEA", "result": "pass", "receipt": "privacyidea-recovery.json"}, + {"stage": 4, "name": "Authelia", "result": "pass", "receipt": "authelia-recovery.json", "login_tested": "admin (post-split password)", + "bind_note": "Authelia ran on the archived post-split configmap, i.e. the svc-authelia bind; the receipt does not name the bind account."}, + {"stage": 5, "name": "by-user MFA", "result": "pass", "receipt": "privacyidea-mfa-recovery.json", "login_tested": "admin (realm coulomb)", + "bind_note": "user resolved through the restored resolver, which keeps the archived svc-privacyidea bind"} + ], + "attempts_not_counted": [ + {"receipt": "attempts/transfer-1-failed.json", + "cause": "no valid OpenBao platform-admin token in the operator shell; retrieve-identity.py reads the Backup credential from OpenBao and fails silently without one (drill document lacked the login step; added 2026-10-31)"}, + {"receipt": "attempts/lldap-1-refused.json", + "cause": "given the failed transfer receipt; refused at input_validation before any password prompt"}, + {"receipt": "attempts/privacyidea-mfa-platform-root.json", + "cause": "valid pass, but tested platform-root rather than admin; stage 5 rerun with admin"} + ], + "si_x1_retired": true, + "next_due": "2027-01-31", + "secret_values_recorded": false +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/lldap-recovery.json b/docs/evidence/2026-10-31-identity-restore-drill/lldap-recovery.json new file mode 100644 index 0000000..001b6b4 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/lldap-recovery.json @@ -0,0 +1,54 @@ +{ + "schema": "netkingdom.isolated-lldap-recovery.v1", + "status": "isolated_lldap_verified", + "production_contacted": false, + "secret_values_exported": false, + "container_cleanup_verified": true, + "temporary_plaintext_cleanup_verified": true, + "source_sha256": "35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "manifest_file_count": 36, + "all_manifest_hashes_verified": true, + "container_isolation_verified_before_private_import": true, + "existing_user_native_ldap_bind": true, + "wrong_password_refused": true, + "only_loopback_interface_verified": true, + "external_routes_absent": true, + "directory_identity_rows_preserved": true, + "directory_comparison_scope": [ + "group_attribute_schema", + "group_attributes", + "groups", + "memberships", + "user_attribute_schema", + "user_attributes", + "users" + ], + "directory_table_counts": { + "users": 12, + "groups": 17, + "memberships": 20, + "user_attributes": 14, + "group_attributes": 0, + "user_attribute_schema": 3, + "group_attribute_schema": 0 + }, + "finished_at": "2026-10-31T07:20:44.085127+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-mfa-recovery.json b/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-mfa-recovery.json new file mode 100644 index 0000000..8e95ad5 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-mfa-recovery.json @@ -0,0 +1,59 @@ +{ + "schema": "netkingdom.isolated-privacyidea-mfa.v1", + "status": "isolated_privacyidea_mfa_verified", + "production_contacted": false, + "secret_values_exported": false, + "existing_token_mfa_proven": true, + "user_to_token_resolver_proven": true, + "container_cleanup_verified": true, + "temporary_plaintext_cleanup_verified": true, + "source_sha256": "d55c5f93c085907be6ae19b0eba6ef360fe863ef8922aa5b6ec205f4837efa16", + "reused_source_sha256": { + "isolated-privacyidea.py": "4df7196678aac1ad594f32abf45c68dc21a9d2760d9e8a536a215a4a3c5d277f", + "isolated-lldap.py": "35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81", + "identity-bundle.py": "524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6", + "verify-identity.py": "039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f", + "retrieve-identity-offline.py": "3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972" + }, + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + }, + { + "reference": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "manifest_file_count": 36, + "all_manifest_hashes_verified": true, + "ldap_resolvers_rewritten_to_loopback": 1, + "container_isolation_verified_before_private_import": true, + "user_resolved_through_restored_resolver_and_directory": true, + "selected_user_sha256": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", + "user_token_count": 1, + "user_usable_token_count": 1, + "user_usable_token_types": [ + "totp" + ], + "wrong_key_refused": true, + "wrong_key_refusal": "error_907", + "wrong_code_refused": true, + "wrong_code_refusal": "wrong_value", + "existing_token_native_check_accepted": true, + "only_loopback_interface_verified": true, + "external_routes_absent": true, + "finished_at": "2026-10-31T07:28:59.115112+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-recovery.json b/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-recovery.json new file mode 100644 index 0000000..d4995ca --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/privacyidea-recovery.json @@ -0,0 +1,54 @@ +{ + "schema": "netkingdom.isolated-privacyidea-recovery.v1", + "status": "isolated_privacyidea_checked", + "production_contacted": false, + "secret_values_exported": false, + "existing_token_mfa_proven": false, + "audit_signature_continuity_proven": true, + "container_cleanup_verified": true, + "temporary_plaintext_cleanup_verified": true, + "source_sha256": "4df7196678aac1ad594f32abf45c68dc21a9d2760d9e8a536a215a4a3c5d277f", + "target": { + "engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1", + "engine_name": "bnt-lap001", + "images": [ + { + "reference": "docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d", + "image_id": "sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d" + }, + { + "reference": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473", + "image_id": "sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473" + }, + { + "reference": "postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70", + "image_id": "sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70" + } + ], + "production_contacted": false, + "network_mode": "none", + "host_ports_published": false + }, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "manifest_file_count": 36, + "all_manifest_hashes_verified": true, + "container_isolation_verified_before_private_import": true, + "retained_tokens_total": 7, + "retained_tokens_with_native_decryption": 7, + "different_key_cannot_reproduce_material_count": 7, + "retained_token_material_decrypted_inside_process": true, + "historical_audit_rows": 414440, + "historical_signed_rows": 414440, + "historical_unsigned_rows": 0, + "historical_invalid_signatures": 0, + "historical_legacy_signatures": 0, + "all_stored_audit_rows_checked": true, + "legacy_signature_verification_enabled": false, + "event_completeness_claimed": false, + "new_audit_entry_signed_verified": true, + "tampered_entry_refused": true, + "wrong_audit_public_key_refused": true, + "only_loopback_interface_verified": true, + "external_routes_absent": true, + "finished_at": "2026-10-31T07:23:19.171196+00:00" +} diff --git a/docs/evidence/2026-10-31-identity-restore-drill/transfer.json b/docs/evidence/2026-10-31-identity-restore-drill/transfer.json new file mode 100644 index 0000000..336d0b9 --- /dev/null +++ b/docs/evidence/2026-10-31-identity-restore-drill/transfer.json @@ -0,0 +1,14 @@ +{ + "age_private_key_used": false, + "artifact": "net-kingdom-identity-20261031T034845Z-a13dc92e2c61.tar.age", + "ciphertext_bytes": 155133096, + "ciphertext_sha256": "126be7f4c5f4ff73ff8f15f4ab486c23795ac0a4ce51930e6d3ccb859031f2c1", + "downloaded_at": "2026-10-31T07:15:57.213304+00:00", + "etag": "\"d1e7409d9bff877a87a20c06a8853e57\"", + "folder": "Backup/railiance-backups/net-kingdom-identity", + "independent_owner_retrieval_verified": true, + "provider": "Storage Share", + "schema": "netkingdom.identity-offsite.v1", + "secret_values_exported": false, + "status": "readback_verified" +} diff --git a/docs/identity-restore-drill.md b/docs/identity-restore-drill.md index 36bcaf0..ca1243b 100644 --- a/docs/identity-restore-drill.md +++ b/docs/identity-restore-drill.md @@ -38,6 +38,11 @@ effect). - Pick the generation from the daily backup (the newest that passed health). Record its date, not its content. - Stage the pinned images locally. Each launcher refuses if they are missing. +- Open an OpenBao session as `admin` in the operator shell: + `bao login -method=oidc -path=netkingdom role=platform-admin` (MFA). Stage 1 reads + the Backup-account credential from OpenBao custody through that token and never + prompts; without a valid `platform-admin` token it writes a failed receipt + (`contained_owner_retrieval_failed`) and prints nothing. Check `echo $?` after stage 1. - Confirm no outbound notification or provider write is configured. The launchers enforce isolation; confirm their preflight passes. diff --git a/tools/service-identity/declarations/lldap.yaml b/tools/service-identity/declarations/lldap.yaml index 6e4a760..54edddc 100644 --- a/tools/service-identity/declarations/lldap.yaml +++ b/tools/service-identity/declarations/lldap.yaml @@ -10,43 +10,37 @@ complete: false identities: - name: admin principal_type: HUMAN - conventions: [bind, service-admin] - purpose: "The human platform administrator; with the same password also the directory super-admin and the bind of four services" + purpose: "The human platform administrator; directory rights through lldap_admin on its own password and OTP; no service binds as admin (bind split 2026-10-30)" grants: - - {mode: ADMINISTER, scope: directory, activation: STANDING, environments: [PRODUCTION]} - - {mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true, note: "service binds"} + - {mode: ADMINISTER, scope: directory, activation: STANDING, environments: [PRODUCTION], note: "lldap_admin membership"} interactive_login: true second_factor: required - custody: [delivered-secret, personal-safe] + custody: [personal-safe] consumers: - {name: founder, kind: person} - - {name: authelia, kind: service} - - {name: keycape, kind: service} - - {name: privacyidea, kind: service} - - {name: identity-provisioner, kind: service} owner: "NetKingdom directory and User Engine owners" rotation: - path: "LLDAP_LDAP_USER_PASS in lldap-secrets (Credential Management section 6)" - coupling: [authelia, keycape, privacyidea-resolver, identity-provisioner, lldap-web-ui] + path: "Human password change; no service Secret holds it" + coupling: [] status: active recovery: {needed: true, works_with_sso_down: false, in_crisis_list: true} - name: ld-admin principal_type: HUMAN conventions: [service-admin] - purpose: "LLDAP only: the directory's configured super-admin, a sealed fallback; introduced by configuration after a rehearsal" + purpose: "LLDAP only: the directory's configured super-admin (LLDAP_LDAP_USER_DN), a sealed fallback; introduced 2026-10-30" grants: - {mode: ADMINISTER, scope: lldap, activation: BREAK_GLASS, environments: [PRODUCTION]} interactive_login: true second_factor: unknown - custody: [unknown] + custody: [delivered-secret, offline-key] consumers: [] owner: "NetKingdom directory and User Engine owners" - rotation: {path: "To be decided in NK-WP-0054-T06", coupling: []} - status: planned + rotation: {path: "LLDAP_LDAP_USER_PASS in lldap-secrets and the LLDAP custody file (apply-service-secrets.py named rotation)", coupling: [lldap]} + status: active - name: svc-authelia principal_type: SERVICE conventions: [bind] - purpose: "Target: Authelia's read-only directory lookup" + purpose: "Authelia's read-only directory lookup" grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}] interactive_login: false second_factor: not_applicable @@ -54,11 +48,11 @@ identities: consumers: [{name: authelia, kind: service}] owner: "NetKingdom directory and User Engine owners" rotation: {path: "Secret rotation with Authelia only", coupling: [authelia]} - status: planned + status: active - name: svc-keycape principal_type: SERVICE conventions: [bind] - purpose: "Target: KeyCape's read-only directory lookup" + purpose: "KeyCape's read-only directory lookup" grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}] interactive_login: false second_factor: not_applicable @@ -66,11 +60,11 @@ identities: consumers: [{name: keycape, kind: service}] owner: "NetKingdom directory and User Engine owners" rotation: {path: "Secret rotation with KeyCape only", coupling: [keycape]} - status: planned + status: active - name: svc-privacyidea principal_type: SERVICE conventions: [bind] - purpose: "Target: the privacyIDEA resolver's read-only directory lookup" + purpose: "The privacyIDEA resolver's read-only directory lookup" grants: [{mode: USE, scope: directory, activation: STANDING, environments: [PRODUCTION], read_only: true}] interactive_login: false second_factor: not_applicable @@ -78,10 +72,10 @@ identities: consumers: [{name: privacyidea, kind: service}] owner: "NetKingdom directory and User Engine owners" rotation: {path: "Resolver bind password, stored encrypted in the privacyIDEA database", coupling: [privacyidea]} - status: planned + status: active - name: svc-provisioner principal_type: SERVICE - purpose: "Target: identity-provisioner; member of lldap_admin, the narrowest LLDAP group that can create users (rehearsal 2026-10-28)" + purpose: "identity-provisioner; member of lldap_admin, the narrowest LLDAP group that can create users (rehearsal 2026-10-28)" grants: [{mode: ADMINISTER, scope: directory, activation: STANDING, environments: [PRODUCTION], note: "lldap_admin; exception SI-X6"}] interactive_login: false second_factor: not_applicable @@ -89,16 +83,8 @@ identities: consumers: [{name: identity-provisioner, kind: service}] owner: "NetKingdom directory and User Engine owners" rotation: {path: "Secret rotation with the provisioner only", coupling: [identity-provisioner]} - status: planned + status: active exceptions: - - id: SI-X1 - rules: [SI-1, SI-2, SI-3, SI-4, SI-9, NAMING] - identity: admin - reason: "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" - owner: "Founder" - expires: 2027-01-31 - compensating_control: "Web UI restricted by IP allowlist; OTP on the human login" - elimination_path: "NK-WP-0054-T05: svc-* binds, a provisioner account, ld-admin as the configured super-admin, admin human-only" - id: SI-X6 rules: [SI-3] identity: svc-provisioner diff --git a/tools/service-identity/tests/test_validate.py b/tools/service-identity/tests/test_validate.py index e080da3..9fc4b88 100644 --- a/tools/service-identity/tests/test_validate.py +++ b/tools/service-identity/tests/test_validate.py @@ -266,15 +266,15 @@ class CheckedInDeclarations(unittest.TestCase): 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) - return [i for r in reports for i in r["identities"] if r["service"] == "lldap" and i["identity"] == "admin"][0] - today = admin(NOW) - self.assertEqual(today["result"], "excepted") - self.assertEqual(set(today["excepted"]), {"SI-1", "SI-2", "SI-3", "SI-4", "SI-9", "NAMING"}) - after = admin(dt.date(2027, 2, 1)) - self.assertEqual(after["result"], "non-conformant") + def test_the_bind_split_retired_the_shared_admin_exception(self): + # SI-X1 closed 2026-10-31 (NK-WP-0054-T05): admin is human-only, each service binds as its own account. + reports, _ = V.run(self.paths(), NOW) + lldap = [r for r in reports if r["service"] == "lldap"][0] + results = {i["identity"]: i["result"] for i in lldap["identities"]} + self.assertEqual(results["admin"], "conformant") + self.assertNotIn("planned", results.values()) + doc = V.load_declaration(DECLARATIONS / "lldap.yaml") + self.assertNotIn("SI-X1", [e["id"] for e in doc["exceptions"]]) def test_no_declaration_claims_owner_confirmation_yet(self): for path in self.paths(): diff --git a/workplans/NK-WP-0053-identity-restore-drill-cadence.md b/workplans/NK-WP-0053-identity-restore-drill-cadence.md index 8c13b87..6db08e1 100644 --- a/workplans/NK-WP-0053-identity-restore-drill-cadence.md +++ b/workplans/NK-WP-0053-identity-restore-drill-cadence.md @@ -21,6 +21,9 @@ state_hub_workstream_id: "7c80ba09-8bd5-540e-8dd3-feb85e201ef3" > **Finished 2026-10-30.** Drill passed 2026-10-29 (next due 2027-01-29, held in disaster-control's > restore-proof register); G1 decided (`e8935fa6`, Scaleway intake under NK-WP-0055-T07). > +> **2026-10-31:** second drill passed on generation `20261031T034845Z-a13dc92e2c61` (post bind split, all five +> stages as `admin`; [evidence](../docs/evidence/2026-10-31-identity-restore-drill/drill.json)). Next due **2027-01-31**. +> > **2026-10-28** (founder triage): T02, T04 and T03 are finished now, not at the 2027-01-31 > deadline. One attended `admin` run covers T02 and T04; T03 follows from its results. The > workplan then closes. The quarterly repeat lives in disaster-control's restore-proof register diff --git a/workplans/NK-WP-0054-service-identity-policy.md b/workplans/NK-WP-0054-service-identity-policy.md index 57e0068..bab6e6c 100644 --- a/workplans/NK-WP-0054-service-identity-policy.md +++ b/workplans/NK-WP-0054-service-identity-policy.md @@ -229,7 +229,7 @@ The founder's live export commands are in RPF-WP-0067-T03. ```task id: NK-WP-0054-T05 -status: progress +status: done priority: high assignee: claude-code needs_human: true @@ -365,6 +365,17 @@ refused because the custody `AUTHELIA_KEYCAPE_CLIENT_SECRET` no longer matches t the next backup generation (closes this task, retires SI-X1) and reconciling the stale custody client secret. +**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. + ## Disposition of per-service admin accounts ```task