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

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