From 84c5dbeec8eb1f382febad6f21ed9533ffd2b313 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 14 Oct 2026 00:08:42 +0200 Subject: [PATCH] security: retire unsafe legacy LDAP lifecycle callers Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2 --- Makefile | 12 +- docs/NetkingdomRuntimeArchitecture.md | 9 + docs/attended-procedure-inventory.md | 6 +- docs/legacy-lldap-lifecycle-retirement.md | 68 +++++ docs/platform-root-custody.md | 2 +- ...ty-bootstrap-rebuild-risk-and-rehearsal.md | 9 + docs/security-bootstrap-retrospective.md | 9 + docs/security-bootstrap-user-lifecycle.md | 9 + docs/smooth-bootstrap-guide.md | 56 ++-- sso-mfa/WORKPLAN.md | 11 +- sso-mfa/k8s/backup/DR-RUNBOOK.md | 5 +- sso-mfa/k8s/lldap/OPERATOR-GROUPS.md | 42 +-- sso-mfa/k8s/lldap/bootstrap-users.sh | 25 +- sso-mfa/k8s/lldap/break-glass.sh | 206 +------------- sso-mfa/k8s/lldap/create-user.sh | 266 +----------------- sso-mfa/k8s/lldap/dry-run-nonroot-user.sh | 266 +----------------- sso-mfa/k8s/verify-t07.sh | 4 +- tests/test_legacy_lldap_retirement.py | 109 +++++++ .../security_bootstrap_console.py | 264 +++++------------ .../tests/test_security_bootstrap_console.py | 5 +- ...-bootstrap-secret-free-recovery-handoff.md | 30 +- 21 files changed, 390 insertions(+), 1023 deletions(-) create mode 100644 docs/legacy-lldap-lifecycle-retirement.md create mode 100644 tests/test_legacy_lldap_retirement.py diff --git a/Makefile b/Makefile index c5f785e..7f04965 100644 --- a/Makefile +++ b/Makefile @@ -214,7 +214,7 @@ security-bootstrap-console-test: ## Run automated tests for bootstrap console UI security-bootstrap-scripts-syntax: ## Shell syntax check for bootstrap scripts bash -n sso-mfa/k8s/lldap/dry-run-nonroot-user.sh bash -n sso-mfa/k8s/lldap/create-user.sh - bash -n sso-mfa/k8s/lldap/break-glass.sh || true # may have env assumptions + bash -n sso-mfa/k8s/lldap/break-glass.sh @echo "✔ bootstrap scripts syntax OK" security-bootstrap-validate-keycape-client: ## Validate KeyCape OpenBao client definition+deployment (T08) @@ -277,13 +277,11 @@ security-bootstrap-lifecycle-guide: ## Print the practical T05 operator flow gui security-bootstrap-onboarding-dry-run-template: ## Print non-secret NET-WP-0017-T06 onboarding dry-run evidence JSON template (use to start T06 evidence after running the lifecycle flow) python3 tools/security-bootstrap-console/security_bootstrap_console.py onboarding-dry-run-template -security-bootstrap-onboarding-dry-run: ## Run the T06 non-root dry-run orchestrator (see sso-mfa/k8s/lldap/dry-run-nonroot-user.sh and NET-WP-0019). Usage: make ... SUBJECT=foo EMAIL=foo@ex.com DISPLAY="Foo" - @cd sso-mfa/k8s/lldap && \ - ./dry-run-nonroot-user.sh $(or $(SUBJECT),t06-dryrun) $(or $(EMAIL),t06-dryrun@coulomb.social) "$(or $(DISPLAY),T06 Dry Run User)" --actor user --scope none +security-bootstrap-onboarding-dry-run: ## Retired: see docs/legacy-lldap-lifecycle-retirement.md + @bash sso-mfa/k8s/lldap/dry-run-nonroot-user.sh -security-bootstrap-lifecycle-cleanup-dryrun-users: ## Clean up dry-run/test users by pattern (NET-WP-0019 T04). Usage: make ... PATTERN=t06-* - @cd sso-mfa/k8s/lldap && \ - ./dry-run-nonroot-user.sh --cleanup-only "$(or $(PATTERN),t06-*)" +security-bootstrap-lifecycle-cleanup-dryrun-users: ## Retired: see docs/legacy-lldap-lifecycle-retirement.md + @bash sso-mfa/k8s/lldap/dry-run-nonroot-user.sh security-bootstrap-validate-custody-roster: ## Validate and verify the signed local custody roster python3 tools/security-bootstrap-console/security_bootstrap_console.py \ diff --git a/docs/NetkingdomRuntimeArchitecture.md b/docs/NetkingdomRuntimeArchitecture.md index e624efe..f8c59b7 100644 --- a/docs/NetkingdomRuntimeArchitecture.md +++ b/docs/NetkingdomRuntimeArchitecture.md @@ -1,5 +1,14 @@ # NetKingdom Runtime Architecture +> Lifecycle update, 2026-10-14: the lifecycle tooling descriptions below are a +> historical snapshot, not current execution instructions. Account creation, +> pattern deletion and inferred-claims verification in the old 0019 helpers are +> retired. Their secret handling and success assertions are not accepted evidence +> of current safety or readiness. Use the [current owner routes](legacy-lldap-lifecycle-retirement.md) +> and actual native receipts. Ordinary lifecycle delivery has since advanced in +> NK-WP-0025/NK-WP-0038; independent recovery and other IAM callers remain open. + + **Status:** draft (initial capture for NET-WP-0018-T02) **Date:** 2026-06-03 **Context:** Documents the *as-deployed* runtime after the first successful bootstrap (0015-0017) + T06-adjacent polish (0019). Not an idealized future architecture. Specific enough to guide a scratch rebuild or rehearsal without rediscovering integration details. Incorporates pragmatic audit paths and known UE integration points/gaps per the persisted assessment. diff --git a/docs/attended-procedure-inventory.md b/docs/attended-procedure-inventory.md index c801793..3fc50b5 100644 --- a/docs/attended-procedure-inventory.md +++ b/docs/attended-procedure-inventory.md @@ -19,10 +19,10 @@ runbook or a dedicated procedure. Missing runbooks are listed separately. | `keycape/register-coulomb-social.sh` | None found | unknown | | `keycape/verify-openbao-client.sh` | [README.md](../sso-mfa/k8s/keycape/README.md), [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | unknown | | `lldap/bootstrap-users.sh` | [README.md](../sso-mfa/k8s/lldap/README.md), [OPERATOR-GROUPS.md](../sso-mfa/k8s/lldap/OPERATOR-GROUPS.md) | unknown | -| `lldap/break-glass.sh` | [platform-root-custody.md](../docs/platform-root-custody.md) | unknown | +| `lldap/break-glass.sh` | [platform-root-custody.md](../docs/platform-root-custody.md) | retired 2026-10-14; [owner routes](legacy-lldap-lifecycle-retirement.md) | | `lldap/create-secrets.sh` | [README.md](../sso-mfa/k8s/lldap/README.md), [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | unknown | -| `lldap/create-user.sh` | [OPERATOR-GROUPS.md](../sso-mfa/k8s/lldap/OPERATOR-GROUPS.md), [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | unknown | -| `lldap/dry-run-nonroot-user.sh` | [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | unknown | +| `lldap/create-user.sh` | [OPERATOR-GROUPS.md](../sso-mfa/k8s/lldap/OPERATOR-GROUPS.md), [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | retired 2026-10-14; [owner routes](legacy-lldap-lifecycle-retirement.md) | +| `lldap/dry-run-nonroot-user.sh` | [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | retired 2026-10-14; [owner routes](legacy-lldap-lifecycle-retirement.md) | | `lldap/manage-group-members.sh` | [OPERATOR-GROUPS.md](../sso-mfa/k8s/lldap/OPERATOR-GROUPS.md) | unknown | | `postgresql/create-secrets.sh` | [README.md](../sso-mfa/k8s/postgresql/README.md), [smooth-bootstrap-guide.md](../docs/smooth-bootstrap-guide.md) | unknown | | `privacyidea/bootstrap-admin.sh` | [README.md](../sso-mfa/k8s/privacyidea/README.md) | unknown | diff --git a/docs/legacy-lldap-lifecycle-retirement.md b/docs/legacy-lldap-lifecycle-retirement.md new file mode 100644 index 0000000..09b3507 --- /dev/null +++ b/docs/legacy-lldap-lifecycle-retirement.md @@ -0,0 +1,68 @@ +# Legacy LLDAP lifecycle retirement + +Effective 2026-10-14; source reconciliation under NK-WP-0049-T02. + +The three commands below now exit 2 before inspecting arguments, discovering +credentials, or calling any external program. Their old arguments are ignored. +The two Make targets and three console commands also refuse execution. No +account, membership, password, token or deployed workload is changed by this +source retirement. Existing credentials are not revoked. + +| Retired command | Why withdrawn | Owner route | +| --- | --- | --- | +| `sso-mfa/k8s/lldap/create-user.sh` | Direct creation bypassed governed lifecycle/linking; test mode used predictable passwords; admin credentials were sourced or discovered and passed through unsafe shell paths. | Ordinary onboarding through User Engine and the existing identity-provisioner. Platform-root bootstrap remains a separate reviewed IAM procedure. | +| `sso-mfa/k8s/lldap/dry-run-nonroot-user.sh` | The “dry-run” mutated real users and passwords; pattern cleanup lacked creation provenance; generated evidence overstated verification. | An explicitly scoped owner lifecycle trial with exact created-identity receipts and actual consumer checks. | +| `sso-mfa/k8s/lldap/break-glass.sh` | Repeated execution could overwrite credentials and depended on an unsupported password GraphQL mutation. This is not an accepted provider recovery path. | Preserve existing offline custody; obtain the IAM owner's exact recovery procedure and native independent-recovery evidence under NK-WP-0049-T03. | + +## Ordinary onboarding and lifecycle + +Use the existing [public registration contract](public-registration-orchestration-contract.md): +User Engine orchestrates mailbox verification for an admitted client/tenant, +links the canonical subject, and invokes identity-provisioner. The IAM provider +owns the single-use password-setup handoff. A fresh KeyCape login establishes the +application session. No predictable password or direct administrative shell +creation is a substitute. Completed NK-WP-0025 and NK-WP-0038 record earlier +registration and scoped tenant-access delivery; those receipts do not establish +current deployment availability. Check the owner’s current service and contract +before a trial; this document does not introduce a new provisioning API. + +The [User Engine boundary contract](../canon/standards/user-engine-boundary-contract_v0.1.md) +keeps domain accounts/product memberships in User Engine and coarse authentication +groups, passwords and MFA in IAM. Protected lifecycle mutations require the +existing authorization path. Tenant-access changes must preserve other tenants, +the global identity and unrelated suspension state. Group removal alone does not +prove global lockout or session revocation. Public registration cannot create a +platform-root or platform-admin identity. + +## Evidence and cleanup + +The console's `lifecycle-flow-template` and `onboarding-dry-run-template` now +produce blank worksheets with false assurances. Fill them only from actual +owner/consumer results. Their existing validators check structure and assertions; +a valid document is not independent proof that a native operation happened. +Historic evidence is retained as historic evidence. + +For an authorized trial, retain exact subject/tenant/client identifiers and +creation provenance, actual issued-claims and effective-access observations, +negative access tests, and the disposition of the exact test identity and its +sessions/credentials. Retain non-secret receipt references, never token or +password values. Do not infer issued claims from a directory group list or delete +accounts selected by a name pattern. Resource transfer and cross-tenant effects +must be resolved through the lifecycle owner before offboarding. + +## Recovery and remaining work + +Keep the existing break-glass account and offline custody material until the +owner explicitly authorizes a change. This retirement neither rotates its +password nor proves it works independently of a failed identity provider. +Disabling MFA is not evidence of independent recovery. The provider dependency +limitation was already recorded in +[the KeyCape exposure map](../history/2026-08-23-keycape-exposure-dependency-map.md). +NK-WP-0049-T03 still needs the exact owner-approved recovery ceremony, separated +key/snapshot custody, failed-key refusal, native restore and cleanup receipts. + +`bootstrap-users.sh`, `manage-group-members.sh` and `verify-t07.sh` remain separate +IAM caller repairs under NK-WP-0049-T02. Their credential extraction and current +verification claims have not been repaired by this change. Incident cutover +helpers and the complete root-output inventory also remain open. This document +and these source tests do not close NK-WP-0049 or establish live recovery readiness. diff --git a/docs/platform-root-custody.md b/docs/platform-root-custody.md index 1aea1ed..6eb2493 100644 --- a/docs/platform-root-custody.md +++ b/docs/platform-root-custody.md @@ -162,7 +162,7 @@ deliberately short, closed, and enumerated here rather than left to judgement: - `platform-root`'s own password and TOTP recovery codes - OpenBao unseal shares and root token -- the `break-glass` account password (`sso-mfa/k8s/lldap/break-glass.sh`) +- the existing `break-glass` account password under offline custody; the old creator is [retired](legacy-lldap-lifecycle-retirement.md), and independent recovery remains to be verified Anything on that list in `operators/` would be a circular dependency: you would need the store to reach the credential that opens the store. Anything *not* on diff --git a/docs/security-bootstrap-rebuild-risk-and-rehearsal.md b/docs/security-bootstrap-rebuild-risk-and-rehearsal.md index cb8ca3f..66016da 100644 --- a/docs/security-bootstrap-rebuild-risk-and-rehearsal.md +++ b/docs/security-bootstrap-rebuild-risk-and-rehearsal.md @@ -1,5 +1,14 @@ # Security Bootstrap Scratch-Rebuild Risk Assessment And Rehearsal Plan +> Lifecycle update, 2026-10-14: the lifecycle tooling descriptions below are a +> historical snapshot, not current execution instructions. Account creation, +> pattern deletion and inferred-claims verification in the old 0019 helpers are +> retired. Their secret handling and success assertions are not accepted evidence +> of current safety or readiness. Use the [current owner routes](legacy-lldap-lifecycle-retirement.md) +> and actual native receipts. Ordinary lifecycle delivery has since advanced in +> NK-WP-0025/NK-WP-0038; independent recovery and other IAM callers remain open. + + **Status:** complete (NET-WP-0018-T09) **Date:** 2026-06-03 **Workplan:** NET-WP-0018 Bootstrap Automation And Rebuild Readiness diff --git a/docs/security-bootstrap-retrospective.md b/docs/security-bootstrap-retrospective.md index 6165181..4952cb7 100644 --- a/docs/security-bootstrap-retrospective.md +++ b/docs/security-bootstrap-retrospective.md @@ -1,5 +1,14 @@ # Security Bootstrap Retrospective and Automation Gap Matrix +> Lifecycle update, 2026-10-14: the lifecycle tooling descriptions below are a +> historical snapshot, not current execution instructions. Account creation, +> pattern deletion and inferred-claims verification in the old 0019 helpers are +> retired. Their secret handling and success assertions are not accepted evidence +> of current safety or readiness. Use the [current owner routes](legacy-lldap-lifecycle-retirement.md) +> and actual native receipts. Ordinary lifecycle delivery has since advanced in +> NK-WP-0025/NK-WP-0038; independent recovery and other IAM callers remain open. + + **Status:** initial draft (NET-WP-0018-T03) **Date:** 2026-06-03 **Scope:** Captures the first successful NetKingdom bootstrap (through 0015-0017 "Reopen under custody" + 0019 T06-adjacent polish) and automation gaps. Input from T02 runtime architecture doc, prior workplan notes/evidence (0017/0019), console metadata/.local flags, operator experience, assessment, and pragmatic records (progress events, evidence.json, git, console validators). diff --git a/docs/security-bootstrap-user-lifecycle.md b/docs/security-bootstrap-user-lifecycle.md index 49f308e..ffacef5 100644 --- a/docs/security-bootstrap-user-lifecycle.md +++ b/docs/security-bootstrap-user-lifecycle.md @@ -1,5 +1,14 @@ # Security Bootstrap User Lifecycle +> Lifecycle update, 2026-10-14: the lifecycle tooling descriptions below are a +> historical snapshot, not current execution instructions. Account creation, +> pattern deletion and inferred-claims verification in the old 0019 helpers are +> retired. Their secret handling and success assertions are not accepted evidence +> of current safety or readiness. Use the [current owner routes](legacy-lldap-lifecycle-retirement.md) +> and actual native receipts. Ordinary lifecycle delivery has since advanced in +> NK-WP-0025/NK-WP-0038; independent recovery and other IAM callers remain open. + + Status: draft UX contract Date: 2026-05-24 diff --git a/docs/smooth-bootstrap-guide.md b/docs/smooth-bootstrap-guide.md index e2ceb95..6d29758 100644 --- a/docs/smooth-bootstrap-guide.md +++ b/docs/smooth-bootstrap-guide.md @@ -12,7 +12,7 @@ See also: - docs/NetkingdomRuntimeArchitecture.md (T02 – what exists) - docs/security-bootstrap-retrospective.md (T03 – what was bumpy, now automated, gaps) - tools/security-bootstrap-console/security_bootstrap_console.py + make security-bootstrap-* (control surface, evidence, validators) -- sso-mfa/k8s/lldap/dry-run-nonroot-user.sh + related (0019 polish) +- [Current lifecycle owner routes](legacy-lldap-lifecycle-retirement.md); the 0019 scripts are retired. - .local/security-bootstrap.json + console status (current gates) **Pragmatic note (per 0018 Coordination):** Track your progress through this guide using State Hub /progress/ (with workstream/task), dated notes in NET-WP-0018 workplan, git, console evidence/validators, /tmp evidence. This feeds future retrospectives. @@ -25,7 +25,7 @@ From platform architecture and UX contracts: - **UI posture (console/web):** Calm field notebook; black/white + hi accents; panels; sentence case; no hype. Shows effective access before any save/action. Blocked conditions explicit (e.g., no platform-root for non-king, MFA required for privileged). - **Evidence discipline:** All steps produce/require non-secret evidence.json or metadata flags matching exact templates/validators (no secret markers). 12+ bools for user lifecycle (effective preview, no root grant, actor checks, verified identity/claims, reversible, no secrets recorded, etc.). - **Actor classes & previews:** Always distinguish (setup operator, platform admin, tenant admin, reviewer, king). Show effective privileges before create/save. Never grant platform-root except via explicit king path. -- **Secret boundary:** Console/UI never collects/stores secrets. Use password-safe, k8s secrets, or operator memory. Prefer k8s fallback for dry-runs (see 0019). +- **Secret boundary:** Console/UI never collects/stores secrets. Use password-safe, k8s secrets, or operator memory. Use the existing governed credential-delivery path; legacy automatic admin-secret extraction is withdrawn. - **Reversible where possible; human custody gates explicit.** - **Handoff to production readiness:** After S6, move to 0017 production items (audit durability, etc. – not duplicated here). @@ -37,7 +37,7 @@ From platform architecture and UX contracts: 5. KeyCape deployment + client registration + OIDC. 6. OpenBao init/unseal/config (OIDC admin binding via KeyCape). 7. Token cleanup, root disposition, restore drill, escrow/custody. -8. User lifecycle (onboard/lock/offboard/review – use 0019 dry-run for tests). +8. User lifecycle (onboard/lock/offboard/review through the current User Engine and IAM owner contracts). 9. State Hub sync, audit posture, cleanup/rotation. 10. Platform reopen (S6) + handoff. @@ -77,18 +77,18 @@ See T03 retrospective for past realm drift bumps (now partially automated via ru ## Step 3: LLDAP / Bootstrap User Creation (platform-root / king) -- Create platform-root user in LLDAP (via create-user.sh or LLDAP admin UI at lldap.coulomb.social). -- Command example (with KUBECTL fallback): `cd sso-mfa/k8s/lldap && ./create-user.sh platform-root ...` (no --admin for non-root tests; use --admin only for platform admins via king path). +- Establish or recover platform-root only through the exact owner-reviewed IAM bootstrap/recovery ceremony; ordinary public registration cannot grant platform-root. +- The legacy account-creation and break-glass scripts are retired; see [recovery limits](legacy-lldap-lifecycle-retirement.md). Preserve existing account/password custody pending owner verification. - Self-enroll TOTP in privacyIDEA. - Verify MFA state: `bash sso-mfa/k8s/privacyidea/check-user-mfa-state.sh platform-root --token-file /absolute/private/diagnostic.jwt` after owner-admitted protected input. See the [diagnostic contract](../sso-mfa/k8s/privacyidea/README.md#scoped-factor-state-diagnostic); the KeyCape workload credential is not a general diagnostic delivery lane. Counts do not verify user existence or an MFA login. - Verify OIDC/KeyCape path: `cd ../keycape && ./verify-openbao-client.sh`. - Groups: net-kingdom-admins for platform-root. - Evidence: identity_account_created, identity_group_confirmed, mfa_*, oidc_login_verified. -- For non-root tests: use 0019 dry-run (see Step 8). +- For non-root trials: follow Step 7 with exact owner scope and actual consumer evidence. **Blocked if:** Actor requests net-kingdom-admins without king path; no MFA for privileged. -**Console:** `lifecycle-guide` for full flow; `onboarding-dry-run*` for tests. +**Console:** `lifecycle-guide` for current owner routes. Legacy execution and inferred-claims commands exit with failure. ## Step 4: KeyCape Deployment + Client Registration + OIDC @@ -160,34 +160,24 @@ See T02 for secret/credential path details; T03 for past claim shape / token iss See T03 for operator-state / token bumps (now in metadata + validators). -## Step 7: User Lifecycle (Onboard / Lock / Offboard / Review) – Use 0019 Polish +## Step 7: User lifecycle through its owners -This implements the first practical flow per docs/security-bootstrap-user-lifecycle.md (UX contract) + T02/ T03. +Follow [the current lifecycle routes and retirement notice](legacy-lldap-lifecycle-retirement.md). +User Engine owns ordinary onboarding, account lifecycle and product memberships; +the existing identity-provisioner and IAM provider own identity and password setup. +Use only admitted clients/tenants. Platform-root bootstrap has a separate owner +ceremony and cannot be performed through public registration. -**Always:** -- Preview effective access (actor_class, scope, groups, MFA, OpenBao policy, no root for non-king). -- Record non-secret evidence/audit (State Hub /progress/, console evidence, k8s audit). -- MFA required for privileged. -- Reversible where possible. +Preview the intended scope, obtain the existing authorization, and retain actual +identity/claims/access, denial and exact cleanup receipts. A group list cannot +verify issued claims or prove all sessions revoked. Never delete test accounts +by a name pattern. Preserve other tenants and global identity state. -**Detailed (from console lifecycle-guide + 0019):** -- Onboard scoped non-root: use `make security-bootstrap-onboarding-dry-run SUBJECT=... EMAIL=... DISPLAY="..."` (or direct ./dry-run-nonroot-user.sh). Internally: safe secret (k8s/env /tmp), create --test (no --admin), verify MFA/KeyCape, optional lock/offboard GraphQL, populate/validate evidence (lldap_identity_verified, keycape_oidc_claims_verified, effective_access_summary, lock_offboard_result, actor_class="user", groups=["net-kingdom-users"], no_secret_material_recorded, prevents_platform_root_grant, etc.). -- Console: `onboarding-dry-run`, `onboarding-dry-run-claims` (infers claims from groups + T01 role; warns on admins/root), `lifecycle-cleanup-dryrun-users --pattern t06-*`. -- Lock: remove from groups (GraphQL or LLDAP UI); reversible. -- Offboard: delete user after resource transfer + reason/date; evidence. -- Review: check-user-mfa-state, LLDAP groups, owned principals; rotate via creds tools. -- Fabric/tenant admin: same but scoped groups, no platform-root, explicit preview "will NOT be in net-kingdom-admins". -- For platform-root/king: king path only. - -**Evidence templates:** `make security-bootstrap-onboarding-dry-run-template`, `lifecycle-flow-template`. -**Validate:** `make security-bootstrap-validate-onboarding-dry-run`, `validate-lifecycle-flow`. -**Runbook:** "User lifecycle dry-run (T06)" in console runbooks (refs 0019 + script). - -**Blocked if:** Missing actor/scope, privileged without MFA, ordinary user gets root groups/policy. - -See 0019 workplan + dry-run script + T03 matrix for past taint/hygiene bumps (now largely automated via /tmp + evidence). - -Update console lifecycle_guide T06 section if it still shows old manual secret steps (prefer orchestrator). +`lifecycle-flow-template` and `onboarding-dry-run-template` are blank evidence +worksheets. Their validators check structure; passing is not native acceptance. +The old Make/console execution commands and “User lifecycle dry-run (T06)” runbook +are retired. Group administration and independent recovery remain open in +NK-WP-0049; do not infer readiness from the earlier 0019 receipts. ## Step 8: Platform Reopen + Handoff @@ -203,7 +193,7 @@ Update console lifecycle_guide T06 section if it still shows old manual secret s - Use T03 retrospective + T02 arch + this guide for future drills. - Address gaps (UE adapters per assessment, full Audit Core correlation, more T08 validators). -- Rehearse rebuild per T09 (T09 complete; see docs/security-bootstrap-rebuild-risk-and-rehearsal.md for risk classification + non-destructive scripted/ns/parallel plan using 0019+creds+T08 validators; scripted/namespace first; use 0019 dry-run as model). +- Rehearse rebuild per T09 (T09 complete; see docs/security-bootstrap-rebuild-risk-and-rehearsal.md for risk classification + non-destructive scripted/ns/parallel plan using 0019+creds+T08 validators; scripted/namespace first; use current owner lifecycle contracts and exact trial provenance). ## Evidence Summary Table (Core Non-Secret) diff --git a/sso-mfa/WORKPLAN.md b/sso-mfa/WORKPLAN.md index f9b971f..15edfa2 100644 --- a/sso-mfa/WORKPLAN.md +++ b/sso-mfa/WORKPLAN.md @@ -1,4 +1,13 @@ # SSO-MFA Platform — Stack Migration Workplan + +> Lifecycle update, 2026-10-14: the lifecycle tooling descriptions below are a +> historical snapshot, not current execution instructions. Account creation, +> pattern deletion and inferred-claims verification in the old 0019 helpers are +> retired. Their secret handling and success assertions are not accepted evidence +> of current safety or readiness. Use the [current owner routes](../docs/legacy-lldap-lifecycle-retirement.md) +> and actual native receipts. Ordinary lifecycle delivery has since advanced in +> NK-WP-0025/NK-WP-0038; independent recovery and other IAM callers remain open. + # NK-WP-0001 — Keycloak → Authelia + LLDAP + KeyCape **Updated:** 2026-03-20 (T04 BLOCKED — ImagePullBackOff; T05–T08 pending T04) @@ -125,7 +134,7 @@ Hostnames: kc.coulomb.social (KeyCape), auth.coulomb.social (Authelia), lldap.co ### Pending (needs live cluster) - [ ] Run `lldap/bootstrap-users.sh` to create groups -- [ ] Run `lldap/break-glass.sh` to create break-glass account +- Retired 2026-10-14: do not run the old break-glass creator. Preserve existing custody; NK-WP-0049-T03 tracks owner recovery verification. - [ ] Add first real user via LLDAP WebUI (lldap.coulomb.social) - [ ] Register first OIDC client in `keycape/create-secrets.sh` (clients: block) - [ ] User self-enrolls TOTP at pink-account.coulomb.social diff --git a/sso-mfa/k8s/backup/DR-RUNBOOK.md b/sso-mfa/k8s/backup/DR-RUNBOOK.md index 431f974..84850f4 100644 --- a/sso-mfa/k8s/backup/DR-RUNBOOK.md +++ b/sso-mfa/k8s/backup/DR-RUNBOOK.md @@ -163,7 +163,10 @@ kubectl rollout status deployment/keycape -n sso --timeout=60s # Restore the original key set first using privacyidea/KEY-RECOVERY.md. # Only then follow the reviewed account/realm reconciliation procedure. cd privacyidea && ./enckey-bootstrap.sh /absolute/private/custody -cd ../lldap && ./bootstrap-users.sh && ./break-glass.sh +# Restore and verify existing LLDAP identities from the owner-reviewed backup. +# Legacy break-glass creation is retired; do not overwrite existing passwords. +# Group bootstrap and verify-t07 remain under NK-WP-0049 caller repair. +# See ../../../docs/legacy-lldap-lifecycle-retirement.md for recovery limits. # Verify owner-managed factor renewal, ESO delivery and mounted reload (RPF-WP-0040). # Do not mint a personal admin token or replace existing KeyCape configuration. diff --git a/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md b/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md index 04f8524..7b98a9f 100644 --- a/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md +++ b/sso-mfa/k8s/lldap/OPERATOR-GROUPS.md @@ -15,7 +15,7 @@ domain rules. Distinct from platform groups (`net-kingdom-users` / **Do not** auto-add everyone in `net-kingdom-admins`. Membership is a conscious grant. Platform admin ≠ activity-core operator. -Seed empty groups with `./bootstrap-users.sh` (idempotent). Authelia config: +Group bootstrap via `bootstrap-users.sh` remains under credential-path repair in NK-WP-0049; it is not accepted by the lifecycle retirement tests. Authelia config: `sso-mfa/k8s/authelia/configmap.yaml` (`access_control.rules`). ## WebUI path @@ -23,46 +23,22 @@ Seed empty groups with `./bootstrap-users.sh` (idempotent). Authelia config: 1. Open https://lldap.coulomb.social (IP-restricted admin UI). 2. Sign in as `admin` (password from KeePassXC / custody — never commit it). 3. **Groups** → `activity-core-operators` → add or remove members. -4. User must already exist (create via WebUI or `./create-user.sh`). +4. User must already exist through governed User Engine / identity-provisioner onboarding; see [current owner routes](../../../docs/legacy-lldap-lifecycle-retirement.md). 5. User re-authenticates at Authelia if an old session lacks the group claim (sign out / new browser session). -## Scripted path +## Scripted path pending repair -```bash -cd sso-mfa/k8s/lldap - -# Add a user to the operator group -./manage-group-members.sh add activity-core-operators - -# Remove -./manage-group-members.sh remove activity-core-operators - -# List members -./manage-group-members.sh list activity-core-operators -``` - -Defaults: LLDAP URL `https://lldap.coulomb.social`, secrets from -`../../bootstrap/secrets/lldap/secrets.env`. Override: - -```bash -./manage-group-members.sh add bernd activity-core-operators \ - https://lldap.coulomb.social /path/to/secrets -``` - -Live ops without a local secrets file: export `LLDAP_ADMIN_PASS` from the -cluster secret (operator workstation only; do not log or commit): - -```bash -export LLDAP_ADMIN_PASS="$(kubectl get secret -n sso lldap-secrets \ - -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' | base64 -d)" -# manage-group-members.sh also accepts LLDAP_ADMIN_PASS when secrets.env is absent -``` +`manage-group-members.sh` still has a legacy admin-credential path. Its repair +and native verification remain in NK-WP-0049-T02/T03. The earlier automatic +Kubernetes Secret extraction and shell-export instructions are withdrawn. +Use the existing attended operator UI under the caller's established authority; +this document grants no new credential or group-administration permission. ## Onboarding checklist (activity-core ops) 1. User has LLDAP account and is in `net-kingdom-users` (normal onboarding). -2. Operator adds them to `activity-core-operators` (WebUI or script). +2. Operator adds them to `activity-core-operators` (attended WebUI under existing authority). 3. User signs in at https://auth.coulomb.social (password; MFA via KeyCape path when using OIDC apps — ForwardAuth to ops hosts uses Authelia session). 4. Open https://activity.coulomb.social and https://temporal.coulomb.social — diff --git a/sso-mfa/k8s/lldap/bootstrap-users.sh b/sso-mfa/k8s/lldap/bootstrap-users.sh index d09e9ae..bbc4b6c 100755 --- a/sso-mfa/k8s/lldap/bootstrap-users.sh +++ b/sso-mfa/k8s/lldap/bootstrap-users.sh @@ -7,7 +7,7 @@ # 1. Authenticates to LLDAP via its GraphQL API. # 2. Creates required platform + app-operator groups (idempotent). # 3. Prints a user onboarding checklist (groups-only; individual users are -# added via the WebUI, create-user.sh, or manage-group-members.sh). +# onboarded through User Engine; IAM group management is separate). # # Groups created: # net-kingdom-users — standard users; all human accounts go here. @@ -158,25 +158,14 @@ echo "════════════════════════ echo " LLDAP group bootstrap: PASS=$PASS_COUNT FAIL=$FAIL_COUNT" echo "════════════════════════════════════════════════════════════" echo "" -echo "Next: add users via the LLDAP WebUI, create-user.sh, or manage-group-members.sh." +echo "Next: use the User Engine / identity-provisioner onboarding contract." +echo "IAM group helpers remain under NK-WP-0049 credential-path repair." echo "App-operator membership: see OPERATOR-GROUPS.md (do not auto-grant all admins)." echo "" -echo "User onboarding checklist:" -echo "" -echo " Per new user:" -echo " 1. Create account in LLDAP WebUI ($LLDAP_URL)" -echo " Fields: username (uid), display name, email" -echo " 2. Assign to net-kingdom-users group (mandatory)" -echo " Assign to net-kingdom-admins too if privileged access is needed" -echo " 3. For activity-core ops/Temporal UI: also add to activity-core-operators" -echo " (./manage-group-members.sh add activity-core-operators)" -echo " 4. User logs in to Authelia (auth.coulomb.social) to verify their password" -echo " 5. User self-enrolls TOTP at pink-account.coulomb.social" -echo " 6. User tests end-to-end login via an OIDC-protected application" -echo "" -echo " Break-glass account:" -echo " Run: sso-mfa/k8s/lldap/break-glass.sh" -echo " (Creates a pre-seeded local bypass user outside the normal MFA flow.)" +echo "Use docs/legacy-lldap-lifecycle-retirement.md for current lifecycle owner routes." +echo "Ordinary onboarding does not grant platform-admin or app-operator membership." +echo "Retain actual login, scope, MFA and consumer acceptance receipts." +echo "Break-glass: preserve existing offline custody; independent recovery remains unverified." echo "" if [[ "$FAIL_COUNT" -gt 0 ]]; then diff --git a/sso-mfa/k8s/lldap/break-glass.sh b/sso-mfa/k8s/lldap/break-glass.sh index 1bc458a..c2bc772 100755 --- a/sso-mfa/k8s/lldap/break-glass.sh +++ b/sso-mfa/k8s/lldap/break-glass.sh @@ -1,203 +1,5 @@ #!/usr/bin/env bash -# break-glass.sh — create the break-glass bypass account in LLDAP -# -# The break-glass account is a last-resort local user for when the SSO stack -# itself is broken (Authelia down, KeyCape misconfigured, etc.). It is: -# - Created in LLDAP with BREAKGLASS_PASSWORD from gen-secrets.sh -# - Assigned to net-kingdom-admins -# - NOT enrolled in privacyIDEA MFA (so it can log in even if privacyIDEA is down) -# - Its password is stored ONLY in KeePassXC (never in the cluster) -# -# IMPORTANT: After creating this account, immediately store the password in -# KeePassXC → net-kingdom/Break-glass/break-glass. Then test it by logging -# in to the LLDAP WebUI directly with this account. -# -# Usage: -# ./break-glass.sh [lldap-url] [secrets-dir] -# -# default: https://lldap.coulomb.social -# default: ../../bootstrap/secrets - -set -euo pipefail - -LLDAP_URL="${1:-https://lldap.coulomb.social}" -SECRETS_DIR="${2:-../../bootstrap/secrets}" -LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" -BG_ENV="$SECRETS_DIR/breakglass/secrets.env" - -BG_USERNAME="break-glass" -BG_EMAIL="break-glass@netkingdom.local" -BG_DISPLAY="Break-glass Account" - -PASS_COUNT=0 -FAIL_COUNT=0 - -ok() { echo " [OK] $1"; PASS_COUNT=$((PASS_COUNT + 1)); } -fail() { echo " [FAIL] $1"; FAIL_COUNT=$((FAIL_COUNT + 1)); } -info() { echo " [INFO] $1"; } - -for f in "$LLDAP_ENV" "$BG_ENV"; do - if [[ ! -f "$f" ]]; then - echo "ERROR: $f not found — run sso-mfa/bootstrap/gen-secrets.sh first." >&2 - exit 1 - fi -done - -read_env() { bash -c "source '$1' 2>/dev/null; echo \${$2}"; } -LLDAP_ADMIN_PASS=$(read_env "$LLDAP_ENV" LLDAP_LDAP_USER_PASS) -BG_PASSWORD=$(read_env "$BG_ENV" BREAKGLASS_PASSWORD) - -if [[ -z "$LLDAP_ADMIN_PASS" || -z "$BG_PASSWORD" ]]; then - echo "ERROR: could not read LLDAP_LDAP_USER_PASS or BREAKGLASS_PASSWORD" >&2 - exit 1 -fi - -# ── Authenticate ────────────────────────────────────────────────────────────── -echo "" -echo "Authenticating to LLDAP at $LLDAP_URL ..." -AUTH_RESP=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \ - 2>/dev/null || echo "CURL_FAILED") - -if [[ "$AUTH_RESP" == "CURL_FAILED" ]]; then - echo "ERROR: Could not reach $LLDAP_URL" >&2 - exit 1 -fi -LLDAP_TOKEN=$(echo "$AUTH_RESP" | python3 -c \ - "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") -if [[ -z "$LLDAP_TOKEN" ]]; then - echo "ERROR: LLDAP authentication failed" >&2 - exit 1 -fi -info "Authenticated as admin" - -gql() { - local query="$1"; local vars="${2:-{}}" - local body - body=$(python3 -c " -import json, sys -print(json.dumps({'query': sys.argv[1], 'variables': json.loads(sys.argv[2])})) -" "$query" "$vars") - curl -sf -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$body" 2>/dev/null || echo "CURL_FAILED" -} - -# ── Check if user already exists ────────────────────────────────────────────── -echo "" -echo "Checking if break-glass user exists ..." -LIST_RESP=$(gql 'query { users { id displayName email } }') -if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then - EXISTS=$(echo "$LIST_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); print('yes' if any(u['id']=='$BG_USERNAME' for u in d.get('data',{}).get('users',[])) else 'no')" \ - 2>/dev/null || echo "no") - if [[ "$EXISTS" == "yes" ]]; then - info "User '$BG_USERNAME' already exists — skipping creation" - ok "User '$BG_USERNAME' exists" - else - # ── Create user ─────────────────────────────────────────────────────── - echo "Creating user '$BG_USERNAME' ..." - CREATE_VARS=$(python3 -c " -import json, sys -v = { - 'user': { - 'id': '$BG_USERNAME', - 'email': '$BG_EMAIL', - 'displayName': '$BG_DISPLAY', - 'firstName': 'Break', - 'lastName': 'Glass' - } -} -print(json.dumps(v)) -") - RESP=$(gql 'mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id creationDate } }' \ - "$CREATE_VARS") - ERR=$(echo "$RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "") - if [[ -n "$ERR" ]]; then - fail "Create user '$BG_USERNAME' — $ERR" - else - ok "User '$BG_USERNAME' created" - fi - fi -else - fail "Could not query user list from LLDAP" -fi - -# ── Set password ────────────────────────────────────────────────────────────── -# LLDAP requires a separate API call to set the password after user creation. -echo "" -echo "Setting password for '$BG_USERNAME' ..." -PW_VARS=$(python3 -c " -import json, sys -print(json.dumps({'userId': '$BG_USERNAME', 'password': sys.argv[1]})) -" "$BG_PASSWORD") -PW_RESP=$(gql 'mutation SetPassword($userId: String!, $password: String!) { changeUserPassword(userId: $userId, password: $password) }' \ - "$PW_VARS") -PW_ERR=$(echo "$PW_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "") -if [[ -n "$PW_ERR" ]]; then - fail "Set password — $PW_ERR" -else - ok "Password set for '$BG_USERNAME'" -fi - -# ── Add to net-kingdom-admins ───────────────────────────────────────────────── -echo "" -echo "Adding '$BG_USERNAME' to net-kingdom-admins group ..." - -# Find the net-kingdom-admins group ID -GROUPS_RESP=$(gql 'query { groups { id displayName } }') -ADMIN_GID=$(echo "$GROUPS_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); gs=[g for g in d.get('data',{}).get('groups',[]) if g['displayName']=='net-kingdom-admins']; print(gs[0]['id'] if gs else '')" \ - 2>/dev/null || echo "") - -if [[ -z "$ADMIN_GID" ]]; then - fail "Group 'net-kingdom-admins' not found — run bootstrap-users.sh first" -else - ADD_VARS="{\"userId\":\"$BG_USERNAME\",\"groupId\":$ADMIN_GID}" - ADD_RESP=$(gql 'mutation AddToGroup($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) }' \ - "$ADD_VARS") - ADD_ERR=$(echo "$ADD_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "") - if [[ -n "$ADD_ERR" && "$ADD_ERR" != *"already"* ]]; then - fail "Add to group — $ADD_ERR" - else - ok "'$BG_USERNAME' is in net-kingdom-admins" - fi -fi - -# ── Summary ─────────────────────────────────────────────────────────────────── -echo "" -echo "════════════════════════════════════════════════════════════" -echo " Break-glass bootstrap: PASS=$PASS_COUNT FAIL=$FAIL_COUNT" -echo "════════════════════════════════════════════════════════════" -echo "" -echo "CRITICAL — do these steps NOW:" -echo "" -echo " 1. Store the break-glass password in KeePassXC:" -echo " Group: net-kingdom/Break-glass" -echo " Entry: break-glass → username='$BG_USERNAME' password=" -echo "" -echo " 2. Test the account (LLDAP WebUI login):" -echo " $LLDAP_URL" -echo " Login as '$BG_USERNAME' with BREAKGLASS_PASSWORD" -echo " Confirm you can see the admin panel." -echo "" -echo " 3. Do NOT enroll MFA for '$BG_USERNAME' in privacyIDEA." -echo " This account must remain usable when privacyIDEA is unavailable." -echo " Its sole authentication factor is the password stored in KeePassXC." -echo "" -echo " 4. Document the DR restore sequence:" -echo " See sso-mfa/k8s/backup/DR-RUNBOOK.md" -echo "" - -if [[ "$FAIL_COUNT" -gt 0 ]]; then - exit 1 -fi -exit 0 +# Retired by NK-WP-0049. Refuse before inspecting arguments or credentials. +# Keep this entry point so stale callers fail visibly instead of mutating IAM. +printf '%s\n' 'This legacy LLDAP lifecycle command is retired.' 'See docs/legacy-lldap-lifecycle-retirement.md for owner routes and recovery limits.' >&2 +exit 2 diff --git a/sso-mfa/k8s/lldap/create-user.sh b/sso-mfa/k8s/lldap/create-user.sh index ccd83df..c2bc772 100755 --- a/sso-mfa/k8s/lldap/create-user.sh +++ b/sso-mfa/k8s/lldap/create-user.sh @@ -1,263 +1,5 @@ #!/usr/bin/env bash -# create-user.sh — create a user in LLDAP and add them to net-kingdom-users -# -# Usage: -# ./create-user.sh [display-name] [--admin] [--test] [lldap-url] [secrets-dir] -# -# LDAP uid — e.g. "bernd" or "testuser" -# e.g. "bernd@coulomb.social" -# defaults to -# --admin also add to net-kingdom-admins -# --test set password automatically as (spaces→hyphens) -# e.g. display "Test User" → password "Test-User-Pwd" -# default: https://lldap.coulomb.social -# default: ../../bootstrap/secrets -# -# Examples: -# ./create-user.sh testuser test.user@coulomb.social "Test User" --test -# ./create-user.sh bernd bernd@coulomb.social "Bernd W" --admin - -set -euo pipefail - -USERNAME="${1:-}" -EMAIL="${2:-}" -DISPLAY_NAME="${3:-$USERNAME}" -LLDAP_URL="https://lldap.coulomb.social" -SECRETS_DIR="../../bootstrap/secrets" -ADMIN_FLAG="" -TEST_FLAG="" - -for arg in "$@"; do - [[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes" - [[ "$arg" == "--test" ]] && TEST_FLAG="yes" -done -# Allow lldap-url and secrets-dir as positional 4/5 if not a flag -for pos in 4 5; do - val="${!pos:-}" - [[ "$val" == "--admin" || "$val" == "--test" || -z "$val" ]] && continue - [[ $pos -eq 4 ]] && LLDAP_URL="$val" - [[ $pos -eq 5 ]] && SECRETS_DIR="$val" -done - -if [[ -z "$USERNAME" || -z "$EMAIL" ]]; then - echo "Usage: $0 [display-name] [--admin]" >&2 - exit 1 -fi - -LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" -LLDAP_ADMIN_PASS="${LLDAP_ADMIN_PASS:-}" - -if [[ -z "$LLDAP_ADMIN_PASS" ]]; then - if [[ -f "$LLDAP_ENV" ]]; then - read_env() { bash -c "source '$1' 2>/dev/null; echo \${$2}"; } - LLDAP_ADMIN_PASS=$(read_env "$LLDAP_ENV" LLDAP_LDAP_USER_PASS) - else - # Safer fallback for dry-runs / automation (NET-WP-0019-T02): pull directly from k8s secret - # without requiring (or writing) a local secrets.env file on disk. - if command -v kubectl >/dev/null 2>&1 || [[ -n "${KUBECTL:-}" ]]; then - KUBECTL_BIN="${KUBECTL:-kubectl}" - LLDAP_ADMIN_PASS="$($KUBECTL_BIN get secret -n sso lldap-secrets -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' 2>/dev/null | base64 -d 2>/dev/null || true)" - fi - fi -fi - -if [[ -z "$LLDAP_ADMIN_PASS" ]]; then - echo "ERROR: Could not obtain LLDAP admin password (no $LLDAP_ENV and no k8s fallback succeeded)." >&2 - echo " For dry-runs prefer setting LLDAP_ADMIN_PASS=... or ensuring kubectl can reach the sso/lldap-secrets secret." >&2 - exit 1 -fi - -# ── Authenticate ────────────────────────────────────────────────────────────── -echo "Authenticating to LLDAP at $LLDAP_URL ..." -LLDAP_TOKEN=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \ - | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") - -# gql — execute a GraphQL query against LLDAP. -# Passes query and variables via environment to avoid shell quoting issues -# with special characters (spaces, quotes) in display names or emails. -gql() { - local query="$1" - local _empty="{}" - local vars="${2:-$_empty}" - local body - body=$(GQL_QUERY="$query" GQL_VARS="$vars" python3 -c " -import json, os -print(json.dumps({ - 'query': os.environ['GQL_QUERY'], - 'variables': json.loads(os.environ['GQL_VARS']) -})) -") - curl -sf -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$body" -} - -# Build variables JSON safely via env vars. -# Set VAR_INT_KEYS to a comma-separated list of keys that should be JSON integers. -make_vars() { - python3 -c " -import json, os -d = {} -int_keys = set(k for k in os.environ.get('VAR_INT_KEYS','').split(',') if k) -for k in os.environ.get('VAR_KEYS','').split(','): - if k: - v = os.environ.get('VAR_' + k, '') - d[k] = int(v) if k in int_keys else v -print(json.dumps(d)) -" -} - -# ── Create user ─────────────────────────────────────────────────────────────── -echo "Creating user '$USERNAME' ($EMAIL, display='$DISPLAY_NAME') ..." - -VARS=$(VAR_KEYS="id,email,display" \ - VAR_id="$USERNAME" \ - VAR_email="$EMAIL" \ - VAR_display="$DISPLAY_NAME" \ - make_vars) - -CREATE_RESP=$(gql \ - 'mutation CreateUser($id: String!, $email: String!, $display: String!) { - createUser(user: {id: $id, email: $email, displayName: $display}) { - id email displayName - } - }' \ - "$VARS") - -ERR=$(echo "$CREATE_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "parse error") - -if [[ -n "$ERR" ]]; then - echo " ERROR: $ERR" >&2 - exit 1 -fi -echo " User '$USERNAME' created." - -# ── Look up group IDs ───────────────────────────────────────────────────────── -GROUPS_RESP=$(gql 'query { groups { id displayName } }') -get_group_id() { - local name="$1" - echo "$GROUPS_RESP" | GRP_NAME="$name" python3 -c " -import sys,json,os -d=json.load(sys.stdin) -grps=d.get('data',{}).get('groups',[]) -matches=[str(g['id']) for g in grps if g['displayName']==os.environ['GRP_NAME']] -print(matches[0] if matches else '') -" -} - -USERS_GID=$(get_group_id "net-kingdom-users") -ADMINS_GID=$(get_group_id "net-kingdom-admins") - -if [[ -z "$USERS_GID" ]]; then - echo " ERROR: group 'net-kingdom-users' not found — run bootstrap-users.sh first." >&2 - exit 1 -fi - -# ── Add to net-kingdom-users ────────────────────────────────────────────────── -echo "Adding '$USERNAME' to net-kingdom-users (id=$USERS_GID) ..." -VARS=$(VAR_KEYS="uid,gid" VAR_INT_KEYS="gid" VAR_uid="$USERNAME" VAR_gid="$USERS_GID" make_vars) -ADD_RESP=$(gql \ - 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ - "$VARS") -ERR=$(echo "$ADD_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "") -[[ -n "$ERR" ]] && echo " WARNING: $ERR" || echo " Added to net-kingdom-users." - -# ── Add to net-kingdom-admins (optional) ───────────────────────────────────── -if [[ -n "$ADMIN_FLAG" ]]; then - if [[ -z "$ADMINS_GID" ]]; then - echo " WARNING: group 'net-kingdom-admins' not found — skipping." >&2 - else - echo "Adding '$USERNAME' to net-kingdom-admins (id=$ADMINS_GID) ..." - VARS=$(VAR_KEYS="uid,gid" VAR_INT_KEYS="gid" VAR_uid="$USERNAME" VAR_gid="$ADMINS_GID" make_vars) - ADD_RESP=$(gql \ - 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ - "$VARS") - ERR=$(echo "$ADD_RESP" | python3 -c \ - "import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \ - 2>/dev/null || echo "") - [[ -n "$ERR" ]] && echo " WARNING: $ERR" || echo " Added to net-kingdom-admins." - fi -fi - -# ── Set password ────────────────────────────────────────────────────────────── -echo "" -echo "Setting password for '$USERNAME' ..." -if [[ -n "$TEST_FLAG" ]]; then - # Derive password from display name: "Test User" → "Test-User-Pwd" - USER_PASS=$(echo "$DISPLAY_NAME" | tr ' ' '-')-Pwd - echo " [--test] Using derived password: $USER_PASS" -else - read -r -s -p " Enter password (leave blank to skip): " USER_PASS - echo "" -fi - -if [[ -n "$USER_PASS" ]]; then - # LLDAP has no GraphQL mutation for password setting. - # Use RFC 3062 LDAP Password Modify extended operation via a kubectl - # port-forward to the in-cluster LLDAP LDAP port (ClusterIP only). - LLDAP_BIND_DN="uid=admin,ou=people,dc=netkingdom,dc=local" - TARGET_DN="uid=$USERNAME,ou=people,dc=netkingdom,dc=local" - LOCAL_PORT=13891 # avoid conflict with other forwarders - - echo " Opening kubectl port-forward to lldap:3890 ..." - KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config-railiance01}" \ - kubectl port-forward svc/lldap "$LOCAL_PORT:3890" -n sso &>/dev/null & - PF_PID=$! - # Wait for the forwarder to be ready (up to 10s) - for i in $(seq 1 10); do - nc -z 127.0.0.1 "$LOCAL_PORT" 2>/dev/null && break - sleep 1 - done - - PASS_ERR=$(LLDAP_USER_PASS="$LLDAP_ADMIN_PASS" \ - LLDAP_BIND_DN_VAR="$LLDAP_BIND_DN" \ - TARGET_DN_VAR="$TARGET_DN" \ - NEW_PASS="$USER_PASS" \ - LOCAL_PORT_VAR="$LOCAL_PORT" \ - python3 -c " -import os, sys -try: - from ldap3 import Server, Connection - s = Server('127.0.0.1', port=int(os.environ['LOCAL_PORT_VAR'])) - c = Connection(s, user=os.environ['LLDAP_BIND_DN_VAR'], password=os.environ['LLDAP_USER_PASS']) - c.bind() - c.extend.standard.modify_password( - user=os.environ['TARGET_DN_VAR'], - new_password=os.environ['NEW_PASS']) - if c.result['result'] != 0: - print(c.result['description'], file=sys.stderr) - sys.exit(1) - c.unbind() -except ImportError: - print('ldap3 not installed — install with: pip install ldap3 --break-system-packages', file=sys.stderr) - sys.exit(1) -" 2>&1) - kill "$PF_PID" 2>/dev/null; wait "$PF_PID" 2>/dev/null || true - - if [[ -n "$PASS_ERR" ]]; then - echo " WARNING: password not set — $PASS_ERR" >&2 - echo " Set manually via the LLDAP WebUI at $LLDAP_URL" - else - echo " Password set." - fi -else - echo " Skipped — set password via $LLDAP_URL as admin." -fi - -# ── Done ────────────────────────────────────────────────────────────────────── -echo "" -echo "════════════════════════════════════════════════════════════" -echo " User '$USERNAME' ready." -[[ -n "$ADMIN_FLAG" ]] && echo " Groups: net-kingdom-users, net-kingdom-admins" || echo " Group: net-kingdom-users" -echo "════════════════════════════════════════════════════════════" -echo "" -echo "Next steps:" -echo " 1. User self-enrolls TOTP at https://pink-account.coulomb.social" -echo " 2. Verify in privacyIDEA: GET /user/?realm=coulomb&username=$USERNAME" +# Retired by NK-WP-0049. Refuse before inspecting arguments or credentials. +# Keep this entry point so stale callers fail visibly instead of mutating IAM. +printf '%s\n' 'This legacy LLDAP lifecycle command is retired.' 'See docs/legacy-lldap-lifecycle-retirement.md for owner routes and recovery limits.' >&2 +exit 2 diff --git a/sso-mfa/k8s/lldap/dry-run-nonroot-user.sh b/sso-mfa/k8s/lldap/dry-run-nonroot-user.sh index b39a62a..c2bc772 100755 --- a/sso-mfa/k8s/lldap/dry-run-nonroot-user.sh +++ b/sso-mfa/k8s/lldap/dry-run-nonroot-user.sh @@ -1,263 +1,5 @@ #!/usr/bin/env bash -# dry-run-nonroot-user.sh — safe, self-contained non-root user lifecycle dry-run (T06) -# -# Usage: -# ./dry-run-nonroot-user.sh "" [options] -# -# Options: -# --actor user|tenant-admin|fabric-admin (default: user) -# --scope "tenant:foo" or "none" -# --no-lockoffboard (just onboard + verify) -# --keep-user (do not delete at end) -# --cleanup-only -# -# It: -# - Uses only non-secret operations + k8s secret extraction into a temp /tmp workspace with trap cleanup. -# - Never leaves plaintext in the persistent sso-mfa/bootstrap/secrets tree. -# - Produces /tmp/netkingdom-onboarding-dry-run/evidence.json (populated from template + live data). -# - Exercises the full T05 flow + T06 verifications/lock/offboard. -# - Cleans up by default. -# -# Requires: kubectl (with access to sso ns), curl, python3, jq (optional but nice). -# -# This is T06-adjacent polish (NET-WP-0019-T01). - -set -euo pipefail - -KUBECTL="${KUBECTL:-/home/worsch/.local/bin/kubectl}" -LLDAP_URL="${LLDAP_URL:-https://lldap.coulomb.social}" -KEYCAPE_DIR="${KEYCAPE_DIR:-../keycape}" -PRIVACY_DIR="${PRIVACY_DIR:-../privacyidea}" - -USERNAME="${1:-}" -EMAIL="${2:-}" -DISPLAY="${3:-$USERNAME}" -ACTOR="${ACTOR:-user}" -SCOPE="${SCOPE:-none}" -DO_LOCK_OFFBOARD=true -KEEP_USER=false -CLEANUP_ONLY="" - -# Handle --cleanup-only early (can be first arg) -if [[ "${1:-}" == "--cleanup-only" ]]; then - CLEANUP_ONLY="${2:-t06-*}" - shift 2 || true -else - shift $(( $# > 3 ? 3 : $# )) - while [[ $# -gt 0 ]]; do - case "$1" in - --actor) ACTOR="$2"; shift 2 ;; - --scope) SCOPE="$2"; shift 2 ;; - --no-lockoffboard) DO_LOCK_OFFBOARD=false; shift ;; - --keep-user) KEEP_USER=true; shift ;; - --cleanup-only) CLEANUP_ONLY="$2"; shift 2 ;; - *) echo "Unknown arg $1"; exit 1 ;; - esac - done -fi - -if [[ -n "$CLEANUP_ONLY" ]]; then - echo "=== Cleanup-only mode for pattern $CLEANUP_ONLY ===" - # Simple implementation for NET-WP-0019-T04: find users matching pattern and offboard them - KUBECTL_BIN="${KUBECTL:-/home/worsch/.local/bin/kubectl}" - LLDAP_URL="${LLDAP_URL:-https://lldap.coulomb.social}" - PASS="$($KUBECTL_BIN get secret -n sso lldap-secrets -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' | base64 -d)" - TOKEN=$(curl -sk -X POST "$LLDAP_URL/auth/simple/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"admin\",\"password\":\"$PASS\"}" \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('token',''))" ) - USERS=$(curl -sk -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d '{"query": "query { users { id } }"}' | python3 -c ' -import json,sys,re -pat = re.compile(sys.argv[1]) if len(sys.argv)>1 else re.compile("dry|t06|test-") -users = [u["id"] for u in json.load(sys.stdin).get("data",{}).get("users",[]) if pat.search(u["id"])] -print(" ".join(users)) -' "$CLEANUP_ONLY" ) - for u in $USERS; do - echo "Offboarding $u ..." - # remove from users group (id 4) and delete - curl -sk -X POST "$LLDAP_URL/api/graphql" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d "{\"query\": \"mutation { removeUserFromGroup(userId: \\\"$u\\\", groupId: 4) { ok } }\"}" >/dev/null - curl -sk -X POST "$LLDAP_URL/api/graphql" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ - -d "{\"query\": \"mutation { deleteUser(userId: \\\"$u\\\") { ok } }\"}" >/dev/null - echo " $u removed" - done - echo "Cleanup complete for pattern." - exit 0 -fi - -if [[ -z "$USERNAME" || -z "$EMAIL" ]]; then - echo "Usage: $0 \"\" [--actor user] [--scope ...]" - exit 1 -fi - -if [[ "$ACTOR" == "king credential" ]]; then - echo "ERROR: actor_class must not be king credential for non-root dry-run" - exit 1 -fi - -TMPDIR=$(mktemp -d /tmp/netkingdom-dryrun-XXXXXX) -chmod 700 "$TMPDIR" -trap 'echo "Cleaning $TMPDIR"; rm -rf "$TMPDIR"' EXIT INT TERM - -echo "=== NET-WP-0019 T06 Dry-Run Orchestrator ===" -echo "Subject: $USERNAME ($EMAIL) display='$DISPLAY' actor=$ACTOR scope=$SCOPE" -echo "Temp workspace: $TMPDIR (will be removed on exit)" - -# 1. Safe secret extraction (never persistent, /tmp only) -echo "" -echo "1. Extracting LLDAP admin pass from k8s (into temp only)..." -LLDAP_PASS="$($KUBECTL get secret -n sso lldap-secrets -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' | base64 -d)" -echo "LLDAP_LDAP_USER_PASS=$LLDAP_PASS" > "$TMPDIR/lldap.env" -chmod 600 "$TMPDIR/lldap.env" -export LLDAP_ADMIN_PASS="$LLDAP_PASS" # for scripts that read env - -# 2. Onboard using create-user (point it at our temp secrets dir) -echo "" -echo "2. Onboarding non-root user (via create-user.sh --test, no admin)..." -cd "$(dirname "$0")" # sso-mfa/k8s/lldap -# Temporarily symlink or use --secrets-dir? The script hardcodes relative, so cd and provide via env hack or copy. -# For safety, we override by exporting and using a one-off secrets dir. -SECRETS_TMP="$TMPDIR/secrets" -mkdir -p "$SECRETS_TMP/lldap" -cp "$TMPDIR/lldap.env" "$SECRETS_TMP/lldap/secrets.env" -chmod 600 "$SECRETS_TMP/lldap/secrets.env" - -# Run create (it will find ../../bootstrap/secrets relative? No: we are in lldap, so we cd and set the var if possible. -# The script reads LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" where SECRETS_DIR default ../../bootstrap/secrets -# We will run it with the secrets-dir arg if supported, or temporarily populate the expected location (but we don't want to touch real tree). -# Better: since we control, use a subshell with modified PATH or just call the logic. -# For this polish, we populate a temp "bootstrap" tree that the script will see if we set the dir. -# The script accepts as last positional. -# From usage: ./create-user.sh ... [lldap-url] [secrets-dir] -# So we can pass the temp as secrets-dir. - -KUBECTL=/home/worsch/.local/bin/kubectl ./create-user.sh \ - "$USERNAME" "$EMAIL" "$DISPLAY" --test \ - "$LLDAP_URL" "$SECRETS_TMP" 2>&1 | cat - -echo "Onboard step complete (check output above for 'User created' and group add)." - -# 3. Verifications (LLDAP groups, MFA capability, KeyCape OIDC readiness) -echo "" -echo "3. Verifying LLDAP identity and groups..." -# Use the inventory-style query (we have the pass in env) -LLDAP_TOKEN=$(curl -sk -X POST "$LLDAP_URL/auth/simple/login" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"admin\",\"password\":\"$LLDAP_PASS\"}" \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('token',''))" ) - -GROUPS_JSON=$(curl -sk -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" -H "Content-Type: application/json" \ - -d '{"query": "query { users { id } groups { displayName members { id } } }"}') - -echo "$GROUPS_JSON" | python3 -c ' -import json,sys -d = json.load(sys.stdin).get("data", {}) -users = d.get("users", []) -groups = d.get("groups", []) -group_members = {g["displayName"]: {m["id"] for m in g.get("members",[])} for g in groups} -for u in users: - if u["id"] == "'"$USERNAME"'": - flags = [gn for gn, mems in group_members.items() if u["id"] in mems] - print("LLDAP user:", u["id"], "groups:", flags) - if "net-kingdom-admins" in flags: - print("WARNING: unexpectedly in net-kingdom-admins") - if "net-kingdom-users" not in flags: - print("WARNING: not in net-kingdom-users") -' || echo "(LLDAP verify step had non-fatal issue; continuing)" - -echo "" -echo "MFA / KeyCape readiness (using existing check scripts)..." -cd ../privacyidea || true -if [[ -n "${MFA_DIAGNOSTIC_TOKEN_FILE:-}" ]]; then - if ! bash ./check-user-mfa-state.sh "$USERNAME" --token-file "$MFA_DIAGNOSTIC_TOKEN_FILE"; then - echo "[UNVERIFIED] Factor-state diagnostic failed; no MFA readiness inferred." - fi -else - echo "[UNVERIFIED] Factor-state diagnostic requires owner-admitted private input; see privacyIDEA README." -fi -cd ../keycape || true -bash ./verify-openbao-client.sh 2>&1 | tail -3 || true - -echo "" -echo "OIDC claims verification for dry-run (T05 non-secret helper)..." -python3 -c ' -import sys, os -# from lldap dir, tools is ../../../tools -sys.path.insert(0, os.path.abspath("../../../tools/security-bootstrap-console")) -from security_bootstrap_console import print_dry_run_oidc_claims_verification -print_dry_run_oidc_claims_verification("'"$USERNAME"'", ["net-kingdom-users"]) -' 2>&1 | cat || echo "(claims helper not available, using static example from guide)" - - -# 4. Lock + Offboard (GraphQL) -if $DO_LOCK_OFFBOARD; then - echo "" - echo "4. Exercising lock (remove from net-kingdom-users) and offboard (delete)..." - # net-kingdom-users group id is typically 4 from prior runs; we query it - GROUP_ID=$(echo "$GROUPS_JSON" | python3 -c ' -import json,sys -for g in json.load(sys.stdin).get("data",{}).get("groups",[]): - if g["displayName"] == "net-kingdom-users": - print(g["id"]) - break -' || echo 4) - - curl -sk -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" -H "Content-Type: application/json" \ - -d "{\"query\": \"mutation { removeUserFromGroup(userId: \\\"$USERNAME\\\", groupId: $GROUP_ID) { ok } }\"}" | cat - - curl -sk -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" -H "Content-Type: application/json" \ - -d "{\"query\": \"mutation { deleteUser(userId: \\\"$USERNAME\\\") { ok } }\"}" | cat - - echo "Lock/offboard mutations issued." -else - echo "Skipping lock/offboard per --no-lockoffboard" -fi - -# 5. Produce evidence using the console template + live data -echo "" -echo "5. Producing T06 evidence..." -EVDIR="/tmp/netkingdom-onboarding-dry-run" -mkdir -p "$EVDIR" -python3 -c ' -import sys, json, datetime, os -sys.path.insert(0, "../../../tools/security-bootstrap-console") -from security_bootstrap_console import onboarding_dry_run_template -d = onboarding_dry_run_template() -d["dry_run_date"] = datetime.date.today().isoformat() -d["operator"] = "platform-custodian" -d["subject_reference"] = "'"$USERNAME"' ('"$EMAIL"')" -d["actor_class"] = "'"$ACTOR"'" -d["tenant_scope"] = "'"$SCOPE"'" -d["effective_access_summary"] = "Dry-run via orchestrator. LLDAP groups during life: net-kingdom-users. MFA/KeyCape paths exercised via scripts. Lock/offboard via GraphQL. No root authority." -d["audit_progress_reference"] = "NET-WP-0019 T01 orchestrator run + State Hub progress" -d["lock_offboard_result"] = "lock and offboard performed (see script log); user removed from LLDAP" -d["post_dry_run_disposition"] = "Test user cleaned (unless --keep-user); temp workspace removed by trap." -d["groups"] = ["net-kingdom-users"] -# mark the bools -for k in ["lldap_identity_verified","groups_verified","mfa_enrollment_verified","keycape_oidc_claims_verified","expected_scope_verified","no_platform_root_authority","no_openbao_root_authority","lock_path_exercised_or_simulated","offboard_path_exercised_or_simulated","credentials_reviewed","audit_progress_recorded","no_secret_material_recorded"]: - d[k] = True -with open("'$EVDIR'/evidence.json", "w") as f: - json.dump(d, f, indent=2) -print("Evidence written to '$EVDIR'/evidence.json") -' 2>&1 | cat - -echo "" -echo "6. Final LLDAP state check (should be clean of the test subject)..." -curl -sk -X POST "$LLDAP_URL/api/graphql" \ - -H "Authorization: Bearer $LLDAP_TOKEN" -H "Content-Type: application/json" \ - -d '{"query": "query { users { id } }"}' | python3 -c ' -import json,sys -users = [u["id"] for u in json.load(sys.stdin).get("data",{}).get("users",[])] -print("Current LLDAP users:", users) -print("Test subject gone?", "'"$USERNAME"'" not in users) -' - -echo "" -echo "=== Dry-run complete. Evidence at $EVDIR/evidence.json ===" -echo "Run: make security-bootstrap-validate-onboarding-dry-run" -echo "Cleanup of temp workspace will happen via trap on exit." +# Retired by NK-WP-0049. Refuse before inspecting arguments or credentials. +# Keep this entry point so stale callers fail visibly instead of mutating IAM. +printf '%s\n' 'This legacy LLDAP lifecycle command is retired.' 'See docs/legacy-lldap-lifecycle-retirement.md for owner routes and recovery limits.' >&2 +exit 2 diff --git a/sso-mfa/k8s/verify-t07.sh b/sso-mfa/k8s/verify-t07.sh index 894a09c..6947d62 100755 --- a/sso-mfa/k8s/verify-t07.sh +++ b/sso-mfa/k8s/verify-t07.sh @@ -133,10 +133,10 @@ else if [[ "$BG_IN_ADMINS" == "yes" ]]; then pass "'break-glass' is in net-kingdom-admins group" else - warn "'break-glass' is not in net-kingdom-admins — run lldap/break-glass.sh" + warn "'break-glass' is not in net-kingdom-admins — reconcile with the IAM recovery owner; legacy creation is retired" fi else - fail "User 'break-glass' not found — run lldap/break-glass.sh" + fail "User 'break-glass' not found — reconcile with the IAM recovery owner; legacy creation is retired" fi fi diff --git a/tests/test_legacy_lldap_retirement.py b/tests/test_legacy_lldap_retirement.py new file mode 100644 index 0000000..f8835e9 --- /dev/null +++ b/tests/test_legacy_lldap_retirement.py @@ -0,0 +1,109 @@ +"""Retired callers must refuse before credential discovery or external actions.""" +import argparse +import contextlib +import importlib.util +import io +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +from unittest.mock import patch + +import pytest + +ROOT = Path(__file__).resolve().parents[1] + + +def console(): + path = ROOT / 'tools/security-bootstrap-console/security_bootstrap_console.py' + spec = importlib.util.spec_from_file_location('retirement_console', path) + module = importlib.util.module_from_spec(spec); sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@pytest.mark.parametrize('name', ['create-user.sh', 'break-glass.sh', 'dry-run-nonroot-user.sh']) +def test_retired_shell_never_sources_credentials_or_calls_external_tools(tmp_path, name): + calls = tmp_path / 'calls'; sourced = tmp_path / 'sourced'; private = tmp_path / 'private' + for service, field in [('lldap', 'LLDAP_LDAP_USER_PASS'), ('breakglass', 'BREAKGLASS_PASSWORD')]: + directory = private / service; directory.mkdir(parents=True, mode=0o700) + (directory / 'secrets.env').write_text(f"{field}='synthetic-private-marker'\ntouch '{sourced}'\n") + bin_dir = tmp_path / 'bin'; bin_dir.mkdir() + for command in ['kubectl', 'curl']: + executable = bin_dir / command + executable.write_text(f"#!/bin/sh\nprintf called >> '{calls}'\nexit 1\n"); executable.chmod(0o700) + env = dict(os.environ, PATH=str(bin_dir) + ':' + os.environ['PATH'], + KUBECTL=str(bin_dir / 'kubectl'), LLDAP_ADMIN_PASS='synthetic-private-marker') + env.pop('BASH_ENV', None) + if name == 'create-user.sh': + env.pop('LLDAP_ADMIN_PASS') + args = ['test-user', 'test@example.test', 'Test User', 'http://127.0.0.1:1', str(private)] + elif name == 'break-glass.sh': args = ['http://127.0.0.1:1', str(private)] + else: args = ['test-user', 'test@example.test', 'Test User'] + result = subprocess.run(['/bin/bash', '-x', str(ROOT / 'sso-mfa/k8s/lldap' / name), *args], + env=env, cwd=tmp_path, capture_output=True, text=True, timeout=10) + assert result.returncode == 2 + assert not calls.exists() and not sourced.exists() + assert 'synthetic-private-marker' not in result.stdout + result.stderr + assert 'retired' in result.stderr + + +@pytest.mark.parametrize('target,variable', [('security-bootstrap-onboarding-dry-run','SUBJECT'), + ('security-bootstrap-onboarding-dry-run','EMAIL'), + ('security-bootstrap-onboarding-dry-run','DISPLAY'), + ('security-bootstrap-lifecycle-cleanup-dryrun-users','PATTERN')]) +def test_make_retirement_never_interpolates_old_user_arguments(tmp_path, target, variable): + # Harmless leaf replacement isolates the Make interpolation boundary. + shutil.copyfile(ROOT / 'Makefile', tmp_path / 'Makefile') + leaf = tmp_path / 'sso-mfa/k8s/lldap/dry-run-nonroot-user.sh'; leaf.parent.mkdir(parents=True) + leaf.write_text('#!/bin/sh\nexit 2\n'); leaf.chmod(0o700) + marker = tmp_path / 'command-executed' + value = f'$(touch {marker})' + # Escape Make's dollar processing so the old recipe would hand $(...) to the shell. + result = subprocess.run(['make', '--no-print-directory', target, f'{variable}=$' + value], + cwd=tmp_path, capture_output=True, text=True, timeout=10) + assert result.returncode != 0 + assert not marker.exists() + + +@pytest.mark.parametrize('command', ['onboarding-dry-run', 'lifecycle-cleanup-dryrun-users', 'onboarding-dry-run-claims']) +def test_console_retirement_returns_failure_without_delegation(command): + module = console(); output = io.StringIO() + with patch('subprocess.call') as call, patch('subprocess.run') as run, \ + patch.object(module, 'load_metadata') as metadata, contextlib.redirect_stdout(output): + code = module.main([command]) + assert code == 2 + call.assert_not_called(); run.assert_not_called(); metadata.assert_not_called() + assert 'retired' in output.getvalue().lower() + + +@pytest.mark.parametrize('factory,validator', [('lifecycle_flow_template','print_validate_lifecycle_flow'), + ('onboarding_dry_run_template','print_validate_onboarding_dry_run')]) +def test_blank_template_is_not_success_evidence(tmp_path, factory, validator): + module = console(); record = getattr(module, factory)() + assert not any(value is True for value in record.values()) + path = tmp_path / 'evidence.json'; path.write_text(json.dumps(record)) + output = io.StringIO() + with contextlib.redirect_stdout(output): result = getattr(module, validator)(argparse.Namespace(evidence=str(path))) + assert result != 0 + + +def test_console_guidance_routes_to_owner_without_legacy_execution(): + module = console(); output = io.StringIO() + with contextlib.redirect_stdout(output): module.print_lifecycle_guide() + assert 'User Engine' in output.getvalue() + assert './create-user.sh' not in output.getvalue() + assert 'safe secret extract' not in output.getvalue() + payload = next(p for p in module.runbook_payloads({'openbao_initialized': True}) if p['name'] == 'User lifecycle dry-run (T06)') + assert payload['state'] == 'retired' + + +def test_direct_claims_compatibility_helper_never_infers_success(): + module = console(); output = io.StringIO() + with contextlib.redirect_stdout(output): + result = module.print_dry_run_oidc_claims_verification('private-subject-marker', ['net-kingdom-admins']) + assert result == 2 + assert 'private-subject-marker' not in output.getvalue() + assert 'net-kingdom-admins' not in output.getvalue() diff --git a/tools/security-bootstrap-console/security_bootstrap_console.py b/tools/security-bootstrap-console/security_bootstrap_console.py index 5c4c1be..4f7e8ca 100755 --- a/tools/security-bootstrap-console/security_bootstrap_console.py +++ b/tools/security-bootstrap-console/security_bootstrap_console.py @@ -1259,24 +1259,25 @@ def print_cleanup_evidence_template() -> None: def lifecycle_flow_template() -> dict[str, Any]: + """Blank evidence worksheet; every assurance requires actual owner evidence.""" return { - "flow_version": "v1", - "operator": "platform-custodian", - "implemented_as": "security-bootstrap-console guidance + existing sso-mfa/k8s scripts (lldap/create-user.sh, lldap/break-glass.sh, privacyidea/check-user-mfa-state.sh, privacyidea/repair-realm-live.sh) + direct LLDAP GraphQL for lock/offboard + console preview of effective access", - "doc_reference": "docs/security-bootstrap-user-lifecycle.md + NET-WP-0017 workplan T05", - "review_date": "2026-06-03", - "effective_access_model": "Actor class + scope determines groups/roles in LLDAP + privacyIDEA + KeyCape claims. Non-root users get net-kingdom-users (or tenant-specific); fabric admins get scoped groups without net-kingdom-admins or platform policies. Previews list exact groups, no OpenBao root, no king credential.", - "non_root_guardrail": "Creation and modification paths explicitly block granting net-kingdom-admins or platform-root equivalent unless actor=king (which is excluded for normal flows).", - "audit_event_model": "All lifecycle actions produce non-secret progress/audit records via State Hub events or local evidence; no secrets in logs. LLDAP/PI changes are auditable via their own logs + NetKingdom progress.", - "onboard_user_supported": True, - "temporary_lock_supported": True, - "permanent_offboard_supported": True, - "credential_review_supported": True, - "fabric_admin_supported": True, - "shows_effective_access_before_save": True, - "privileged_roles_require_mfa": True, - "prevents_platform_root_grant": True, - "no_secret_material_recorded": True, + "flow_version": "v2", + "operator": "", + "implemented_as": "", + "doc_reference": "docs/legacy-lldap-lifecycle-retirement.md", + "review_date": "", + "effective_access_model": "", + "non_root_guardrail": "", + "audit_event_model": "", + "onboard_user_supported": False, + "temporary_lock_supported": False, + "permanent_offboard_supported": False, + "credential_review_supported": False, + "fabric_admin_supported": False, + "shows_effective_access_before_save": False, + "privileged_roles_require_mfa": False, + "prevents_platform_root_grant": False, + "no_secret_material_recorded": False, } @@ -1285,34 +1286,30 @@ def print_lifecycle_flow_template() -> None: def onboarding_dry_run_template() -> dict[str, Any]: - """Non-secret evidence template for NET-WP-0017-T06 onboarding dry run. - Use after running the T05 lifecycle flow for a scoped non-root subject. - Fill with actual results from LLDAP queries, create-user output, GraphQL - lock/offboard, check scripts, KeyCape verify, etc. Must not contain secrets. - """ + """Blank evidence worksheet; every assurance requires actual owner evidence.""" return { - "dry_run_date": "YYYY-MM-DD", - "operator": "platform-custodian", - "subject_reference": "dryrun-username (dryrun@example)", - "actor_class": "user", # or "tenant-admin" / "fabric-admin". MUST NOT be "king credential" - "tenant_scope": "none (or e.g. 'tenant:foo' for scoped)", - "effective_access_summary": "LLDAP: only net-kingdom-users (or scoped). MFA self-enroll available. KeyCape OIDC claims: groups + email + sub (no platform-admin). No OpenBao root or platform policies. Lock/offboard exercised via group removal + delete.", - "audit_progress_reference": "State Hub /progress/ id for T06 + this evidence file + LLDAP audit if enabled", - "lock_offboard_result": "lock: removeUserFromGroup succeeded; offboard: deleteUser succeeded; post state: user absent, LLDAP clean", - "post_dry_run_disposition": "Test subject fully removed with no residual access or groups. LLDAP users back to [admin, platform-root]. No plaintext left.", - "lldap_identity_verified": True, - "groups_verified": True, - "mfa_enrollment_verified": True, - "keycape_oidc_claims_verified": True, - "expected_scope_verified": True, - "no_platform_root_authority": True, - "no_openbao_root_authority": True, - "lock_path_exercised_or_simulated": True, - "offboard_path_exercised_or_simulated": True, - "credentials_reviewed": True, - "audit_progress_recorded": True, - "no_secret_material_recorded": True, - "groups": ["net-kingdom-users"], # membership during the dry-run lifetime (before lock/offboard) + "dry_run_date": "", + "operator": "", + "subject_reference": "", + "actor_class": "", + "tenant_scope": "", + "effective_access_summary": "", + "audit_progress_reference": "", + "lock_offboard_result": "", + "post_dry_run_disposition": "", + "lldap_identity_verified": False, + "groups_verified": False, + "mfa_enrollment_verified": False, + "keycape_oidc_claims_verified": False, + "expected_scope_verified": False, + "no_platform_root_authority": False, + "no_openbao_root_authority": False, + "lock_path_exercised_or_simulated": False, + "offboard_path_exercised_or_simulated": False, + "credentials_reviewed": False, + "audit_progress_recorded": False, + "no_secret_material_recorded": False, + "groups": [], } @@ -1320,120 +1317,24 @@ def print_onboarding_dry_run_template() -> None: print(json.dumps(onboarding_dry_run_template(), indent=2)) -def print_dry_run_oidc_claims_verification(subject: str = "dryrun-user", groups: list[str] | None = None) -> None: - """T05 helper: non-secret OIDC claims verification for dry-run subject. - Computes expected claims based on LLDAP groups (no secrets). - Used by orchestrator and console 'onboarding-dry-run-claims' . - """ - if groups is None: - groups = ["net-kingdom-users"] - print("OIDC CLAIMS VERIFICATION FOR DRY-RUN (non-secret)") - print(f"Subject: {subject}") - print(f"LLDAP groups (during dry-run): {groups}") - print("") - print("Expected KeyCape OIDC claims (inferred from groups + standard config):") - claims = { - "sub": subject, - "email": f"{subject}@example", - "groups": groups, - "preferred_username": subject, - } - print(json.dumps(claims, indent=2)) - print("") - print("OpenBao policy binding (from T01 config): platform-admin role only binds 'net-kingdom-admins' group.") - if "net-kingdom-admins" in groups: - print("WARNING: would grant platform-admin (not for non-root dry-run)") - else: - print("No platform-admin or root authority (correct for non-root).") - print("To verify live: after onboard, use the lifecycle dry-run script which exercises KeyCape verify + LLDAP.") +def print_dry_run_oidc_claims_verification(subject: str = "dryrun-user", groups: list[str] | None = None) -> int: + """Compatibility refusal: inferred directory groups cannot verify issued claims.""" + print("Retired: inferred groups are not verified OIDC claims.") + print("See docs/legacy-lldap-lifecycle-retirement.md; retain actual issuer and consumer evidence.") + return 2 def print_lifecycle_guide() -> None: - print("NET-WP-0017-T05 LIFECYCLE OPERATOR FLOW GUIDE") - print("") - print("This implements the first practical operator flow per docs/security-bootstrap-user-lifecycle.md") - print("All actions are non-secret. Secrets stay in password-safe / k8s secrets / operator memory.") - print("Every privileged flow shows EFFECTIVE ACCESS PREVIEW before any create/save.") - print("") - print("=== 1. ONBOARD SCOPED NON-ROOT USER ===") - print("Preview effective (do this mentally or via future UI card):") - print(" - actor_class=platform-admin or tenant-admin or user") - print(" - scope= (for tenant: e.g. 'tenant:foo')") - print(" - groups: net-kingdom-users + optional scoped; NEVER net-kingdom-admins for non-root") - print(" - MFA: always required for privileged; self-enroll at pink-account") - print(" - KeyCape OIDC claims will include groups + email; no platform-root policy") - print(" - OpenBao: no root, only if policy granted via group") - print("") - print("Command (run in operator shell; pass never echoed to console):") - print(" cd sso-mfa/k8s/lldap") - print(" KUBECTL=/home/worsch/.local/bin/kubectl ./create-user.sh \\") - print(" \"Display Name\" # add --admin ONLY for platform admins") - print(" # Then: user self-enrolls TOTP; verify with:") - print(" cd ../privacyidea; KUBECTL=... ./check-user-mfa-state.sh ") - print("") - print("Blocked if: actor requests net-kingdom-admins without explicit king-credential path.") - print("") - print("=== 2. TEMPORARILY LOCK USER ===") - print(" # Use LLDAP admin UI or GraphQL to remove from active groups temporarily") - print(" # Or privacyIDEA: disable token for user (preserves history)") - print(" # Preview: shows current groups, tokens, sessions before lock") - print(" # Record: State Hub note or evidence with unlock date") - print(" # Reversible by re-adding group / enabling token") - print("") - print("=== 3. PERMANENTLY OFFBOARD ===") - print(" # 1. Select user, require reason+date") - print(" # 2. Transfer resources (manual for now)") - print(" # 3. Revoke: use LLDAP to remove groups; privacyIDEA disable+delete tokens if needed") - print(" # 4. For KeyCape/Authelia sessions: rely on token expiry + group removal") - print(" # Record non-secret offboard evidence (see T06 validator)") - print(" # Platform-admin offboard needs second human confirm") - print("") - print("=== 4. REVIEW CREDENTIALS / MFA STATE ===") - print(" cd sso-mfa/k8s/privacyidea") - print(" ./check-user-mfa-state.sh ") - print(" # Also: LLDAP GraphQL query for groups/members; manual review of owned principals") - print(" # Actions: rotate (creds-rotate make), change groups via create-user or LLDAP UI") - print("") - print("=== 5. CREATE FABRIC/TENANT ADMIN (no platform-root) ===") - print(" # Same as onboard, but:") - print(" # - actor_class = tenant-admin or fabric-admin") - print(" # - scope = tenant:xxx or fabric:yyy") - print(" # - groups: scoped group only (create via LLDAP if needed)") - print(" # - NO --admin flag") - print(" # - Preview explicitly: 'will NOT be in net-kingdom-admins; no OpenBao platform/ policy'") - print(" # Handover: produce checklist (name fabric, assign admin, audit expectations)") - print("") - print("All flows record non-secret audit via: console evidence, State Hub /progress/, or k8s audit.") - print("See also: create-user.sh --help , break-glass.sh for recovery accounts (king only).") - print("Next gate: T06 dry-run using this flow + evidence.") - print("") - - - print("=== T06 DRY-RUN EXECUTION (adjacent polish) ===") - print("Use after the above onboard/lock/offboard steps for a scoped non-root subject.") - print("This makes the T06 gate repeatable and less manual. See docs/smooth-bootstrap-guide.md Step 7 and NET-WP-0019.") - print("") - print("Preferred: one-command via make (handles /tmp safe workspace + trap + k8s fallback for secrets, never persistent bootstrap/secrets for dry-runs):") - print(" make security-bootstrap-onboarding-dry-run SUBJECT= EMAIL= DISPLAY=\"\"") - print("") - print("Or direct script:") - print(" cd sso-mfa/k8s/lldap") - print(" ./dry-run-nonroot-user.sh \"Display Name\" [--actor user] [--scope none]") - print(" # It will: safe secret extract (/tmp + trap), create --test, verifs (MFA/KeyCape), optional lock/offboard, emit /tmp/.../evidence.json, cleanup.") - print("") - print("Validate evidence:") - print(" make security-bootstrap-validate-onboarding-dry-run") - print("") - print("Claims verification (non-secret, infers from LLDAP groups + T01 role):") - print(" python3 tools/security-bootstrap-console/security_bootstrap_console.py onboarding-dry-run-claims --subject --groups net-kingdom-users") - print("") - print("Cleanup pattern-matched test users:") - print(" make security-bootstrap-lifecycle-cleanup-dryrun-users PATTERN=t06-*") - print(" # Or console 'lifecycle-cleanup-dryrun-users --pattern t06-*'") - print("") - print("All steps leave LLDAP clean, no plaintext secrets on disk, evidence with required bools (effective before save, no root grant, verified identity/claims, actor_class != king, etc.). See console lifecycle-flow-template / onboarding-dry-run-template and smooth-bootstrap-guide.md.") - print("See workplan T06/0019 notes for the 2026-06-03 execution.") - print("") + print("USER LIFECYCLE OWNER ROUTES") + print("Ordinary onboarding: use User Engine registration for an admitted client and tenant.") + print("Identity provisioning and single-use password setup stay with the IAM provider.") + print("User Engine owns account lifecycle and product memberships; IAM owns coarse authentication groups.") + print("Tenant lifecycle changes must preserve other tenants and global identity state.") + print("Platform-root bootstrap and offline recovery require their separate owner-reviewed ceremony.") + print("Legacy account creation, pattern cleanup and inferred-claims commands are retired.") + print("Record actual identity, issued claims, effective access, denial and exact cleanup receipts.") + print("Blank templates are unverified; structural validation alone is not native acceptance.") + print("See docs/legacy-lldap-lifecycle-retirement.md for contracts and remaining recovery work.") def compact_command_output(text: str) -> str: @@ -3003,12 +2904,12 @@ def runbook_payloads(data: dict[str, Any]) -> list[dict[str, str]]: ), { "name": "User lifecycle dry-run (T06)", - "description": "Execute non-root onboarding dry-run (create/verify/lock/offboard), check LLDAP/groups/MFA/KeyCape claims, produce evidence. See NET-WP-0019 and dry-run-nonroot-user.sh .", - "subsystem": "LLDAP / privacyIDEA / KeyCape", + "description": "Retired: use User Engine and IAM owner flows with actual consumer evidence; no pattern deletion or inferred-claims verification.", + "subsystem": "User Engine / IAM", "responsibility": "platform-custodian", "email": role_email(data, "role_platform_custodian_email"), - "location": "sso-mfa/k8s/lldap/dry-run-nonroot-user.sh ; make security-bootstrap-onboarding-dry-run ; console 'onboarding-dry-run' ; NET-WP-0019", - "state": "template", + "location": "docs/legacy-lldap-lifecycle-retirement.md", + "state": "retired", }, ] @@ -5123,12 +5024,12 @@ def build_parser() -> argparse.ArgumentParser: sub.add_parser("custody-roster-template", help="Print non-secret custody roster JSON template.") sub.add_parser("cleanup-evidence-template", help="Print non-secret NET-WP-0017-T03/T04 cleanup/taint evidence JSON template.") sub.add_parser("lifecycle-flow-template", help="Print non-secret NET-WP-0017-T05 lifecycle operator flow evidence JSON template.") - sub.add_parser("lifecycle-guide", help="Print the practical T05 operator flow guide with commands and previews (no secrets).") + sub.add_parser("lifecycle-guide", help="Print current User Engine and IAM lifecycle owner guidance.") sub.add_parser("onboarding-dry-run-template", help="Print non-secret NET-WP-0017-T06 onboarding dry-run evidence JSON template (skeleton for T06 evidence).") - sub.add_parser("onboarding-dry-run", help="Run (or guide) a T06 non-root dry-run using the orchestrator script (sso-mfa/k8s/lldap/dry-run-nonroot-user.sh). See NET-WP-0019.") - cl = sub.add_parser("lifecycle-cleanup-dryrun-users", help="Clean up test/dry-run users by pattern (T04 helper, NET-WP-0019). Example: --pattern t06-*") - cl.add_argument("--pattern", default="t06-*", help="Regex or glob pattern for test users to offboard (default t06-*)") - claims = sub.add_parser("onboarding-dry-run-claims", help="T05: Verify expected OIDC claims for a dry-run subject (non-secret, based on groups). See NET-WP-0019-T05.") + sub.add_parser("onboarding-dry-run", help="Retired; prints current lifecycle owner guidance and exits with failure.") + cl = sub.add_parser("lifecycle-cleanup-dryrun-users", help="Retired; prints current lifecycle owner guidance and exits with failure.") + cl.add_argument("--pattern", default="t06-*", help="Ignored compatibility argument; pattern deletion is retired") + claims = sub.add_parser("onboarding-dry-run-claims", help="Retired; prints current lifecycle owner guidance and exits with failure.") claims.add_argument("--subject", default="dryrun-user", help="Test subject username") claims.add_argument("--groups", default="net-kingdom-users", help="Comma-separated groups during dry-run") sub.add_parser("handover-checklist", help="Print handover and cleanup checklist.") @@ -5191,6 +5092,10 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) + if args.command in {"onboarding-dry-run", "lifecycle-cleanup-dryrun-users", "onboarding-dry-run-claims"}: + print("Retired: legacy LLDAP lifecycle automation and inferred-claims verification.") + print("See docs/legacy-lldap-lifecycle-retirement.md for supported owner routes.") + return 2 metadata_commands = { "status", "validate-king-kit", @@ -5255,37 +5160,6 @@ def main(argv: list[str] | None = None) -> int: if args.command == "onboarding-dry-run-template": print_onboarding_dry_run_template() return 0 - if args.command == "onboarding-dry-run": - print("NET-WP-0019 / T06 Dry-Run (orchestrator)") - print("Run the script directly for full automation:") - print(" cd sso-mfa/k8s/lldap") - print(" ./dry-run-nonroot-user.sh \"Display Name\" [--actor user] [--scope none]") - print("") - print("It will:") - print(" - safely extract admin pass into /tmp (trap cleanup)") - print(" - create the user (non-root)") - print(" - verify LLDAP/groups, MFA readiness, KeyCape OIDC path") - print(" - exercise lock (remove group) + offboard (delete)") - print(" - emit /tmp/netkingdom-onboarding-dry-run/evidence.json (from template)") - print(" - clean up") - print("") - print("Then validate:") - print(" make security-bootstrap-validate-onboarding-dry-run") - print("") - print("See also: make security-bootstrap-lifecycle-guide (T06 section) and the NET-WP-0019 workplan.") - return 0 - if args.command == "lifecycle-cleanup-dryrun-users": - pat = getattr(args, "pattern", "t06-*") if hasattr(args, "pattern") else "t06-*" - print("Delegating cleanup for pattern", pat, "to orchestrator...") - import subprocess, os - script = os.path.join(os.path.dirname(__file__), "..", "..", "sso-mfa", "k8s", "lldap", "dry-run-nonroot-user.sh") - script = os.path.abspath(script) - subprocess.call(["bash", script, "--cleanup-only", pat]) - return 0 - if args.command == "onboarding-dry-run-claims": - groups = getattr(args, "groups", "net-kingdom-users").split(",") - print_dry_run_oidc_claims_verification(getattr(args, "subject", "dryrun-user"), groups) - return 0 if args.command == "handover-checklist": print_handover_checklist() return 0 diff --git a/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py b/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py index 64d507d..d4dc76b 100644 --- a/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py +++ b/tools/security-bootstrap-console/tests/test_security_bootstrap_console.py @@ -134,7 +134,7 @@ def test_lifecycle_flow_template(): tmpl = console.lifecycle_flow_template() assert "flow_version" in tmpl assert "onboard_user_supported" in tmpl - assert tmpl["onboard_user_supported"] is True + assert tmpl["onboard_user_supported"] is False def test_runbook_payloads_includes_dry_run(): # minimal data @@ -143,7 +143,8 @@ def test_runbook_payloads_includes_dry_run(): names = [p["name"] for p in payloads] assert "User lifecycle dry-run (T06)" in names dry = next(p for p in payloads if "dry-run" in p["name"].lower()) - assert "NET-WP-0019" in dry.get("location", "") or "dry-run-nonroot-user.sh" in dry.get("location", "") + assert dry["state"] == "retired" + assert dry["location"] == "docs/legacy-lldap-lifecycle-retirement.md" def test_audit_core_posture_ready_with_bootstrap_risk(): data = { diff --git a/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md b/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md index fc1307d..8dc8e52 100644 --- a/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md +++ b/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: netkingdom created: "2026-10-12" -updated: "2026-10-13" +updated: "2026-10-14" flavor: implementation depends_on: [] state_hub_workstream_id: "17d315f0-829e-57c7-85cc-3a7914152804" @@ -289,6 +289,34 @@ No live credential, resolver, realm, policy or token was touched. LDAP callers, incident-specific rotation/cutover review, full inventory and native recovery acceptance remain open; T02 stays progress and T03 wait. +2026-10-14 legacy lifecycle retirement: `create-user.sh`, +`dry-run-nonroot-user.sh` (including pattern cleanup), and `break-glass.sh` now +refuse with exit 2 before arguments, credential discovery or external calls. +The Make targets no longer interpolate user inputs. Console execution/claims +commands refuse before metadata loading or delegation; the inferred-claims helper +cannot report success. Lifecycle worksheets start blank with false assurances, +and their validators reject them until populated. Existing historical records +remain unchanged. Current guidance routes ordinary onboarding/lifecycle through +User Engine and the existing identity-provisioner; platform-root and independent +recovery retain their separate IAM owner boundaries. + +Eleven corrected-baseline regressions failed against the old source; the first +draft also contained a wrong guide-function name, corrected before that baseline. +Final focused console/retirement tests cover refusal under tracing, no credential +file execution/external calls, Make shell-expansion refusal, no metadata reads, +blank-template rejection and no inferred claims. Active callers and operator +instructions no longer recommend the retired scripts; old architecture and +assessment snapshots carry explicit superseding notices. New reference: +`docs/legacy-lldap-lifecycle-retirement.md`. Evidence is retained by Custodian in +`docs/evidence/2026-10-14-netkingdom-security/legacy-lldap-retirement.json`. + +No account/password/token or deployed workload changed; retirement is not +revocation or proof of independent recovery. `bootstrap-users.sh`, +`manage-group-members.sh`, and `verify-t07.sh` still need credential-path/native +verification repair (only stale lifecycle guidance changed in two of them). +Incident cutover helpers, the full caller/root-output inventory and T03 remain +open. T02 stays progress; no workplan closure is claimed. + ## Verify owner-approved recovery and current native callers ```task