security: retire unsafe legacy LDAP lifecycle callers
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
This commit is contained in:
tegwick 2026-10-14 00:08:42 +02:00
parent 276b95f6ba
commit 84c5dbeec8
21 changed files with 390 additions and 1023 deletions

View file

@ -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 \

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 —

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()

View file

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

View file

@ -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 = {

View file

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