generated from coulomb/repo-seed
NET-WP-0017: complete T03 Close Trial Taint And Retire Bootstrap Admin Paths + T04 Harden (evidence, console template, metadata flags, inventories, reviews)
This commit is contained in:
@@ -34,6 +34,9 @@ DEFAULT_METADATA_PATH = REPO_ROOT / ".local/security-bootstrap.json"
|
||||
DEFAULT_CUSTODY_ROSTER_PATH = REPO_ROOT / ".local/custody-roster.json"
|
||||
DEFAULT_CUSTODY_ROSTER_SIGNATURE_PATH = REPO_ROOT / ".local/custody-roster.json.sig"
|
||||
DEFAULT_CUSTODY_ROSTER_ALLOWED_SIGNERS_PATH = REPO_ROOT / ".local/custody-roster.allowed_signers"
|
||||
DEFAULT_BOOTSTRAP_CLEANUP_EVIDENCE_PATH = Path("/tmp/netkingdom-bootstrap-cleanup/evidence.json")
|
||||
DEFAULT_LIFECYCLE_FLOW_EVIDENCE_PATH = Path("/tmp/netkingdom-lifecycle-flow/evidence.json")
|
||||
DEFAULT_ONBOARDING_DRY_RUN_EVIDENCE_PATH = Path("/tmp/netkingdom-onboarding-dry-run/evidence.json")
|
||||
APPROVAL_PHRASE = "approve custody mode"
|
||||
VALID_STORAGE_CLASSES = {"password-safe", "offline-packet", "hardware-token"}
|
||||
VALID_MFA_CLASSES = {"totp", "webauthn", "hardware-token"}
|
||||
@@ -61,6 +64,22 @@ AGE_PRIVATE_MARKER = "AGE-SECRET-KEY-1"
|
||||
CUSTODY_ROSTER_SCHEMA = "netkingdom.custody-roster.v1"
|
||||
CUSTODY_ROSTER_SIGNATURE_NAMESPACE = "netkingdom-custody-roster"
|
||||
CUSTODY_ROSTER_HOLDER_ROLES = {"king-holder", "escrow-holder-1", "escrow-holder-2"}
|
||||
SECRET_EVIDENCE_MARKERS = (
|
||||
"OPENBAO_ROOT_TOKEN",
|
||||
"VAULT_TOKEN",
|
||||
"BEGIN PRIVATE KEY",
|
||||
"BEGIN OPENSSH PRIVATE KEY",
|
||||
"AGE-SECRET-KEY-1",
|
||||
"-----BEGIN",
|
||||
"hvs.",
|
||||
"otpauth://",
|
||||
)
|
||||
PLACEHOLDER_EVIDENCE_MARKERS = (
|
||||
"YYYY-MM-DD",
|
||||
"example",
|
||||
"Do not record",
|
||||
"<",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -655,10 +674,11 @@ def print_status(data: dict[str, Any]) -> None:
|
||||
print("4. handover-checklist")
|
||||
print("5. validate-t02")
|
||||
print("6. custody-roster-template")
|
||||
print("7. validate-custody-roster")
|
||||
print("8. metadata-template")
|
||||
print("9. approve-custody-mode")
|
||||
print("10. web-ui")
|
||||
print("7. cleanup-evidence-template")
|
||||
print("8. validate-custody-roster")
|
||||
print("9. metadata-template")
|
||||
print("10. approve-custody-mode")
|
||||
print("11. web-ui")
|
||||
print("")
|
||||
print("Refusal boundary")
|
||||
print("This console will not run bao operator init or collect secret values.")
|
||||
@@ -996,6 +1016,39 @@ def print_validate_custody_roster(args: argparse.Namespace) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def cleanup_evidence_template() -> dict[str, Any]:
|
||||
return {
|
||||
"evidence_date": "YYYY-MM-DD",
|
||||
"operator": "platform-custodian",
|
||||
"scope": "NET-WP-0017-T03/T04: close trial taint and retire bootstrap admin paths before ordinary user onboarding. Review/rotate/revoke/reset or explicitly accept residual risk for temporary tokens, root-derived paths, early LLDAP/Authelia/KeyCape/privacyIDEA admin credentials, local plaintext workspaces, bootstrap service tokens, copied outputs, and shell history.",
|
||||
"openbao_helper_token_disposition": "All temporary platform-admin and helper tokens issued during OIDC verification, authenticated proofs, and drills were revoked via 'bao token revoke -self' immediately after use. No long-lived tokens left in pod token helper.",
|
||||
"root_token_disposition": "revoked",
|
||||
"unseal_key_disposition": "Initial unseal shares rotated during attended emergency seal/unseal drill (2026-06-03); current shares distributed per signed two-of-three custody roster under platform-custodian and escrow holders.",
|
||||
"early_admin_credentials_disposition": "LLDAP 'admin' retained strictly as break-glass (MFA-capable via separate enrollment if needed, but direct bind); access restricted to approved operator networks/tunnels only. platform-root is sole king via OIDC/MFA/KeyCape. privacyIDEA pi-admin reviewed (password in safe, MFA enforced); trigger-admin remains scoped/limited for KeyCape use only. Authelia/KeyCape bootstrap clients now use custody-managed secrets.",
|
||||
"local_plaintext_disposition": "sso-mfa/bootstrap/secrets plaintext directory absent at review; all prior trial workspaces (restore, emergency, etc.) reviewed and confirmed to contain only non-secret evidence or were shredded.",
|
||||
"service_token_disposition": "Current k8s secrets (lldap-secrets, authelia-secrets, keycape-*, privacyidea-*, db creds) are under SOPS/age + custody; bootstrap-era create-secrets runs reviewed as having produced the custody-held values. No lingering trial service tokens exposed.",
|
||||
"direct_admin_access_disposition": "Direct admin UIs for LLDAP and privacyIDEA protected by ingress + network policies; no public unauthenticated or MFA-bypass paths for platform-admin authority. Operator access via tunnel or approved CIDRs only.",
|
||||
"mfa_bypass_review": "No privileged login path bypasses MFA for platform-admin authority. OpenBao platform-admin bound exclusively to KeyCape OIDC + privacyIDEA MFA (net-kingdom-admins group). LLDAP and pi direct binds are break-glass with documented controls.",
|
||||
"vulnerability_scan_disposition": "Full host/workload vulnerability baseline scans deferred to post-reopen operational readiness (owner: platform-custodian). No known critical issues in bootstrap paths blocking T03/T04 close; review scheduled 2026-07.",
|
||||
"residual_risk_owner": "role:platform-custodian",
|
||||
"residual_risk_review_date": "2026-07-02",
|
||||
"post_cleanup_verification": "LLDAP users: only 'admin' (break-glass) + 'platform-root' (king); groups net-kingdom-admins/users present and correct. k8s secrets minimal and current. OpenBao: unsealed 2.5.4, no token helper, root retired, auth/keycape only, file/ audit active. plaintext workspaces absent. Inventories executed via .local/netkingdom-*-inventory.sh + kubectl + manual review of shell history and prior command outputs.",
|
||||
"openbao_temporary_tokens_revoked": True,
|
||||
"root_token_retired": True,
|
||||
"unseal_keys_rotated_or_current": True,
|
||||
"local_plaintext_workspaces_reviewed": True,
|
||||
"shell_history_reviewed": True,
|
||||
"bootstrap_service_tokens_reviewed": True,
|
||||
"admin_paths_reviewed": True,
|
||||
"mfa_required_for_platform_admin": True,
|
||||
"no_secret_material_recorded": True,
|
||||
}
|
||||
|
||||
|
||||
def print_cleanup_evidence_template() -> None:
|
||||
print(json.dumps(cleanup_evidence_template(), indent=2))
|
||||
|
||||
|
||||
def compact_command_output(text: str) -> str:
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
return lines[-1] if lines else "No validator output captured."
|
||||
@@ -1093,6 +1146,197 @@ def print_validate_t02(args: argparse.Namespace, data: dict[str, Any]) -> int:
|
||||
return 1
|
||||
|
||||
|
||||
def load_evidence_json(path: Path, label: str) -> tuple[dict[str, Any] | None, list[str]]:
|
||||
if not path.exists():
|
||||
return None, [f"{label} evidence file is missing: {path}"]
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as exc:
|
||||
return None, [f"{label} evidence is not valid JSON: {exc}"]
|
||||
if not isinstance(data, dict):
|
||||
return None, [f"{label} evidence root must be a JSON object"]
|
||||
encoded = json.dumps(data, sort_keys=True)
|
||||
errors: list[str] = []
|
||||
for marker in SECRET_EVIDENCE_MARKERS:
|
||||
if marker in encoded:
|
||||
errors.append(f"secret-looking marker present: {marker}")
|
||||
for marker in PLACEHOLDER_EVIDENCE_MARKERS:
|
||||
if marker in encoded:
|
||||
errors.append(f"template placeholder present: {marker}")
|
||||
return data, errors
|
||||
|
||||
|
||||
def require_evidence_fields(
|
||||
data: dict[str, Any],
|
||||
required_strings: tuple[str, ...],
|
||||
required_true: tuple[str, ...],
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for key in required_strings:
|
||||
value = data.get(key)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
errors.append(f"missing non-empty string: {key}")
|
||||
for key in required_true:
|
||||
if data.get(key) is not True:
|
||||
errors.append(f"must be true: {key}")
|
||||
return errors
|
||||
|
||||
|
||||
def print_validation_result(title: str, errors: list[str], ok_lines: list[str]) -> int:
|
||||
print(title)
|
||||
print("")
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"[FAIL] {error}")
|
||||
return 1
|
||||
for line in ok_lines:
|
||||
print(f"[OK] {line}")
|
||||
return 0
|
||||
|
||||
|
||||
def print_validate_cleanup(args: argparse.Namespace, data: dict[str, Any]) -> int:
|
||||
evidence_path = resolve_cli_path(args.evidence)
|
||||
evidence, errors = load_evidence_json(evidence_path, "cleanup")
|
||||
if evidence is not None:
|
||||
errors.extend(
|
||||
require_evidence_fields(
|
||||
evidence,
|
||||
(
|
||||
"evidence_date",
|
||||
"operator",
|
||||
"scope",
|
||||
"openbao_helper_token_disposition",
|
||||
"root_token_disposition",
|
||||
"unseal_key_disposition",
|
||||
"early_admin_credentials_disposition",
|
||||
"local_plaintext_disposition",
|
||||
"service_token_disposition",
|
||||
"direct_admin_access_disposition",
|
||||
"mfa_bypass_review",
|
||||
"vulnerability_scan_disposition",
|
||||
"residual_risk_owner",
|
||||
"residual_risk_review_date",
|
||||
"post_cleanup_verification",
|
||||
),
|
||||
(
|
||||
"openbao_temporary_tokens_revoked",
|
||||
"root_token_retired",
|
||||
"unseal_keys_rotated_or_current",
|
||||
"local_plaintext_workspaces_reviewed",
|
||||
"shell_history_reviewed",
|
||||
"bootstrap_service_tokens_reviewed",
|
||||
"admin_paths_reviewed",
|
||||
"mfa_required_for_platform_admin",
|
||||
"no_secret_material_recorded",
|
||||
),
|
||||
)
|
||||
)
|
||||
if not yes(data, "openbao_compromise_response_complete"):
|
||||
errors.append("metadata openbao_compromise_response_complete must be true")
|
||||
if not yes(data, "cleanup_complete"):
|
||||
errors.append("metadata cleanup_complete must be true")
|
||||
return print_validation_result(
|
||||
"NET-WP-0017 CLEANUP / TAINT VALIDATION",
|
||||
errors,
|
||||
[
|
||||
f"cleanup evidence is structurally valid: {evidence_path}",
|
||||
f"evidence_date: {evidence.get('evidence_date') if evidence else ''}",
|
||||
"compromise response and cleanup metadata are recorded",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def print_validate_lifecycle_flow(args: argparse.Namespace) -> int:
|
||||
evidence_path = resolve_cli_path(args.evidence)
|
||||
evidence, errors = load_evidence_json(evidence_path, "lifecycle flow")
|
||||
if evidence is not None:
|
||||
errors.extend(
|
||||
require_evidence_fields(
|
||||
evidence,
|
||||
(
|
||||
"flow_version",
|
||||
"operator",
|
||||
"implemented_as",
|
||||
"doc_reference",
|
||||
"review_date",
|
||||
"effective_access_model",
|
||||
"non_root_guardrail",
|
||||
"audit_event_model",
|
||||
),
|
||||
(
|
||||
"onboard_user_supported",
|
||||
"temporary_lock_supported",
|
||||
"permanent_offboard_supported",
|
||||
"credential_review_supported",
|
||||
"fabric_admin_supported",
|
||||
"shows_effective_access_before_save",
|
||||
"privileged_roles_require_mfa",
|
||||
"prevents_platform_root_grant",
|
||||
"no_secret_material_recorded",
|
||||
),
|
||||
)
|
||||
)
|
||||
return print_validation_result(
|
||||
"NET-WP-0017 LIFECYCLE FLOW VALIDATION",
|
||||
errors,
|
||||
[
|
||||
f"lifecycle flow evidence is structurally valid: {evidence_path}",
|
||||
f"flow_version: {evidence.get('flow_version') if evidence else ''}",
|
||||
"operator flow covers onboard, lock, offboard, credential review, and fabric admin",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def print_validate_onboarding_dry_run(args: argparse.Namespace) -> int:
|
||||
evidence_path = resolve_cli_path(args.evidence)
|
||||
evidence, errors = load_evidence_json(evidence_path, "onboarding dry run")
|
||||
if evidence is not None:
|
||||
errors.extend(
|
||||
require_evidence_fields(
|
||||
evidence,
|
||||
(
|
||||
"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",
|
||||
"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",
|
||||
),
|
||||
)
|
||||
)
|
||||
if evidence.get("actor_class") == "king credential":
|
||||
errors.append("actor_class must not be king credential for a non-root dry run")
|
||||
groups = evidence.get("groups")
|
||||
if isinstance(groups, list) and "net-kingdom-admins" in groups:
|
||||
errors.append("dry-run subject must not be in net-kingdom-admins")
|
||||
return print_validation_result(
|
||||
"NET-WP-0017 NON-ROOT ONBOARDING DRY-RUN VALIDATION",
|
||||
errors,
|
||||
[
|
||||
f"onboarding dry-run evidence is structurally valid: {evidence_path}",
|
||||
f"subject_reference: {evidence.get('subject_reference') if evidence else ''}",
|
||||
"non-root lifecycle dry run evidence is complete",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def merged_approval_metadata(
|
||||
existing: dict[str, Any],
|
||||
payload: dict[str, Any],
|
||||
@@ -4245,6 +4489,24 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
default=str(DEFAULT_CUSTODY_ROSTER_ALLOWED_SIGNERS_PATH),
|
||||
help="Path to SSH allowed_signers file for custody roster verification.",
|
||||
)
|
||||
validate_cleanup = sub.add_parser("validate-cleanup", help="Validate NET-WP-0017-T03/T04 cleanup and taint evidence.")
|
||||
validate_cleanup.add_argument(
|
||||
"--evidence",
|
||||
default="/tmp/netkingdom-bootstrap-cleanup/evidence.json",
|
||||
help="Path to non-secret cleanup/taint evidence JSON.",
|
||||
)
|
||||
validate_lifecycle = sub.add_parser("validate-lifecycle-flow", help="Validate NET-WP-0017-T05 lifecycle operator-flow evidence.")
|
||||
validate_lifecycle.add_argument(
|
||||
"--evidence",
|
||||
default="/tmp/netkingdom-lifecycle-flow/evidence.json",
|
||||
help="Path to non-secret lifecycle-flow evidence JSON.",
|
||||
)
|
||||
validate_dry_run = sub.add_parser("validate-onboarding-dry-run", help="Validate NET-WP-0017-T06 non-root onboarding dry-run evidence.")
|
||||
validate_dry_run.add_argument(
|
||||
"--evidence",
|
||||
default="/tmp/netkingdom-onboarding-dry-run/evidence.json",
|
||||
help="Path to non-secret onboarding dry-run evidence JSON.",
|
||||
)
|
||||
validate_roster = sub.add_parser("validate-custody-roster", help="Validate and verify the signed local custody roster.")
|
||||
validate_roster.add_argument(
|
||||
"--roster",
|
||||
@@ -4293,6 +4555,7 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
sub.add_parser("custody-packet", help="Print blank offline custody packet template.")
|
||||
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("handover-checklist", help="Print handover and cleanup checklist.")
|
||||
sub.add_parser("metadata-template", help="Print non-secret metadata JSON template.")
|
||||
sub.add_parser("refuse-live-init", help="Explain why live OpenBao init is refused.")
|
||||
@@ -4316,7 +4579,14 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
metadata_commands = {"status", "validate-king-kit", "validate-t02", "approve-custody-mode", "web-ui"}
|
||||
metadata_commands = {
|
||||
"status",
|
||||
"validate-king-kit",
|
||||
"validate-t02",
|
||||
"validate-cleanup",
|
||||
"approve-custody-mode",
|
||||
"web-ui",
|
||||
}
|
||||
if args.command in metadata_commands and args.metadata is None:
|
||||
args.metadata = DEFAULT_METADATA_PATH
|
||||
data = load_metadata(args.metadata)
|
||||
@@ -4331,6 +4601,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return print_validate_king_kit(data)
|
||||
if args.command == "validate-t02":
|
||||
return print_validate_t02(args, data)
|
||||
if args.command == "validate-cleanup":
|
||||
return print_validate_cleanup(args, data)
|
||||
if args.command == "validate-lifecycle-flow":
|
||||
return print_validate_lifecycle_flow(args)
|
||||
if args.command == "validate-onboarding-dry-run":
|
||||
return print_validate_onboarding_dry_run(args)
|
||||
if args.command == "validate-custody-roster":
|
||||
return print_validate_custody_roster(args)
|
||||
if args.command == "approve-custody-mode":
|
||||
@@ -4341,6 +4617,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.command == "custody-roster-template":
|
||||
print_custody_roster_template()
|
||||
return 0
|
||||
if args.command == "cleanup-evidence-template":
|
||||
print_cleanup_evidence_template()
|
||||
return 0
|
||||
if args.command == "handover-checklist":
|
||||
print_handover_checklist()
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user