let pb = import "schemas/lib/playbook.ncl" in pb.make_playbook { id = "rotate_keys", name = "Key Rotation", description = "Rotates ops-controller key, keeper key, or operator key. For ops-controller: generates new key, proposes delegation patch on state- signed by quorum, activates new key, verifies continuity. Keeper and operator key rotation follow the same quorum-sign pattern.", version = 1, preconditions = [ "Identify which key type to rotate: ops-controller | keeper | operator", "Ensure quorum of existing signers is available to sign the delegation patch", "For ops-controller rotation: audit mirror must be paused or confirmed resilient to a 30-60s gap", "Generate the new key offline before running this playbook", ], params = [ { name = "workspace", description = "Target workspace (e.g. libre-wuji)", required = true, default_val = "" }, { name = "key_type", description = "Which key to rotate: ops-controller | keeper | operator", required = true, default_val = "" }, { name = "new_key_path", description = "Path to the new Ed25519 private key PEM", required = true, default_val = "" }, { name = "new_pubkey_path", description = "Path to the new Ed25519 public key PEM", required = true, default_val = "" }, { name = "ops_vm_host", description = "ops-vm SSH host (required for keeper rotation)", required = false, default_val = "" }, ], steps = [ { id = "validate_new_key", name = "Validate new key is a valid Ed25519 keypair", script = "steps/validate_new_key.nu", dry_run_arg = "--dry-run", on_error = 'Stop, depends_on = [], }, { id = "propose_delegation_patch", name = "Propose delegation patch on state- repo", script = "steps/propose_delegation_patch.nu", dry_run_arg = "--dry-run", on_error = 'Stop, depends_on = ["validate_new_key"], }, { id = "sign_delegation_patch", name = "Await quorum signatures on delegation patch", script = "steps/sign_delegation_patch.nu", dry_run_arg = "--dry-run", on_error = 'Stop, depends_on = ["propose_delegation_patch"], }, { id = "activate_new_key", name = "Activate new key (deploy to target service)", script = "steps/activate_new_key.nu", dry_run_arg = "--dry-run", on_error = 'Rollback, depends_on = ["sign_delegation_patch"], }, { id = "verify_continuity", name = "Verify audit/signing continuity with new key", script = "steps/verify_continuity.nu", dry_run_arg = "--dry-run", on_error = 'Continue, depends_on = ["activate_new_key"], }, { id = "revoke_old_key", name = "Revoke old key from delegation set", script = "steps/revoke_old_key.nu", dry_run_arg = "--dry-run", on_error = 'Continue, depends_on = ["verify_continuity"], }, { id = "emit_audit", name = "Emit key-rotation audit event", script = "steps/emit_audit.nu", params = { event_type = "key_rotation" }, dry_run_arg = "--dry-run", on_error = 'Continue, depends_on = ["revoke_old_key"], }, ], rollback_strategy = 'manual, success_criteria = [ "New key is active and accepted by the target service", "Delegation patch is merged in state- Radicle repo", "Old key is removed from delegation set", "ops history shows key-rotation audit event", "No interruption in audit trail during rotation", ], emit_audit = true, adr_refs = ["adr-037", "adr-038"], }