security: retire unsafe legacy LDAP lifecycle callers
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
This commit is contained in:
parent
276b95f6ba
commit
84c5dbeec8
21 changed files with 390 additions and 1023 deletions
12
Makefile
12
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 \
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
68
docs/legacy-lldap-lifecycle-retirement.md
Normal file
68
docs/legacy-lldap-lifecycle-retirement.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <uid> activity-core-operators
|
||||
|
||||
# Remove
|
||||
./manage-group-members.sh remove <uid> 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 —
|
||||
|
|
|
|||
|
|
@ -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 <uid> 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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
#
|
||||
# <lldap-url> default: https://lldap.coulomb.social
|
||||
# <secrets-dir> 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=<from gen-secrets.sh>"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 <username> <email> [display-name] [--admin] [--test] [lldap-url] [secrets-dir]
|
||||
#
|
||||
# <username> LDAP uid — e.g. "bernd" or "testuser"
|
||||
# <email> e.g. "bernd@coulomb.social"
|
||||
# <display-name> defaults to <username>
|
||||
# --admin also add to net-kingdom-admins
|
||||
# --test set password automatically as <DisplayName-Pwd> (spaces→hyphens)
|
||||
# e.g. display "Test User" → password "Test-User-Pwd"
|
||||
# <lldap-url> default: https://lldap.coulomb.social
|
||||
# <secrets-dir> 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 <username> <email> [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
|
||||
|
|
|
|||
|
|
@ -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 <username> <email> "<Display Name>" [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 <username>
|
||||
#
|
||||
# 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 <username> <email> \"<Display>\" [--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 <secrets-dir> 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
109
tests/test_legacy_lldap_retirement.py
Normal file
109
tests/test_legacy_lldap_retirement.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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(" <username> <email> \"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 <username>")
|
||||
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 <username>")
|
||||
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=<username> EMAIL=<email> DISPLAY=\"<name>\"")
|
||||
print("")
|
||||
print("Or direct script:")
|
||||
print(" cd sso-mfa/k8s/lldap")
|
||||
print(" ./dry-run-nonroot-user.sh <username> <email> \"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 <username> --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 <username> <email> \"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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue