Skip to main content
KBY Technologies Logo
The Ops Playbook

Recovery Lock Rotation for Apple Silicon Macs at Scale

A production-safe workflow for rotating and auditing macOS Recovery Lock passwords via Jamf Pro APIs to stop manual unlock escalations.

Recovery Lock Rotation for Apple Silicon Macs at Scale
Isla Morgan 18 July 20269 min read Tier L2 40 min setupmacOS

Operational brief

Operational outcome

A production-safe workflow for rotating and auditing macOS Recovery Lock passwords via Jamf Pro APIs to stop manual unlock escalations.

Target Tier

L2 support

Implement within an approved test scope before wider deployment.

Time to set aside

About 40 minutes

Includes configuration, validation and deployment.

Share
Tutorial navigation
Innovation note
Verify your deployment with small rollout groups and enforce least-privilege administrative access when implementing these automations.

The old way of handling Recovery Lock on Apple Silicon Macs looks like this: a technician receives a repair or troubleshooting ticket, the Mac boots to macOS Recovery, and it demands a firmware-level passcode nobody in the room has memorised. Someone opens a shared spreadsheet or a ticket comment thread from eighteen months ago, hunts for a serial number match, and reads a static passcode out loud over a call. That passcode has often never been rotated since imaging day, it is frequently pasted into multiple tickets in plain text, and it grants firmware-level control of the machine to anyone who ever read that ticket. It is slow, it is an audit failure waiting to happen, and it does not scale past a handful of devices a week.

The new way treats the Recovery Lock passcode the same way modern identity teams treat any privileged credential: short-lived, rotated on a schedule, retrievable only through an audited path, and never static long enough to matter if it leaks. Apple’s MDM protocol already gives us the primitive we need with the SetRecoveryLock command. This playbook wires that primitive into a rotation schedule, a scoped retrieval workflow, and a compliance evidence trail, so the technician gets an unlock code in seconds through an approved channel instead of digging through old tickets.

#Prerequisites and Permissions

Confirm the following before enabling scheduled rotation on any Smart Group or dynamic collection:

  • Fleet is Apple Silicon (M-series). Recovery Lock replaces the EFI firmware password model used on Intel Macs with T2 chips; this workflow does not apply to Intel-only fleets.
  • Devices are enrolled via Automated Device Enrollment through Apple Business Manager and are in Supervised state. SetRecoveryLock is rejected by unsupervised devices.
  • macOS 12 Monterey or later is installed; Recovery Lock management was introduced in that release.
  • Jamf Pro (or another MDM with equivalent API support) with a dedicated API service account scoped to only two privileges: Send Computer Remote Commands and View Recovery Lock Password. Do not attach this role to a general helpdesk account.
  • Short-lived OAuth client credentials for the service account, token lifetime 30 minutes or less, rotated independently of the Recovery Lock passcodes themselves.
  • A secrets store (HashiCorp Vault, Azure Key Vault, or equivalent) for the break-glass path used when a device cannot reach the MDM to report status.
  • A SIEM or logging pipeline able to accept a webhook so every retrieval of a live passcode is recorded with ticket number, technician identity, and device serial.

Minimum blast radius for a rotation run is one device. Batch rotations should be capped at fifty devices per run with a health check between batches. Monitoring signal is the MDM command history status field; rollback trigger is any batch showing more than five percent command failures or devices dropping off MDM check-in mid-run.

#Implementation Steps

  1. Scope the fleet. Action: build a Smart Group (or equivalent Intune-side compliance filter) matching Apple Silicon, Supervised = true, Recovery Lock Enabled = true. Expected result: a discrete, countable device list, not the entire fleet at once. Evidence to capture: exported group membership count and timestamp, stored with the rotation ticket.

  2. Generate a fresh passcode per device. Action: run the generator script below inside the rotation job, never reuse a passcode across devices. Expected result: one cryptographically random alphanumeric string per serial number, excluding ambiguous characters. Evidence to capture: the script’s log line showing serial number mapped to a passcode hash (not the plaintext) for later reconciliation.

  3. Push the SetRecoveryLockPassword command. Action: call the MDM command against each device ID using the API pattern in the code sample below. Expected result: command status of Acknowledged within one check-in cycle. Evidence to capture: command UUID, timestamp, and status returned by the API, logged to the audit trail.

  4. Mirror the new passcode to the secrets vault. Action: on Acknowledged status, write the passcode to the vault path keyed by serial number with a 90 day TTL. Expected result: vault entry exists and is retrievable by the break-glass role only. Evidence to capture: vault write confirmation and access policy attached to the entry.

  5. Schedule recurring rotation. Action: run the job every 90 days, aligned with the same cadence Apple recommends for FileVault personal recovery key hygiene, and trigger an immediate out-of-cycle rotation any time a passcode is retrieved by a technician. Expected result: no passcode remains valid for more than one retrieval event or 90 days, whichever is shorter. Evidence to capture: scheduler run history and the retrieval-triggered rotation log entry.

  6. Build the scoped retrieval path for L1 technicians. Action: expose a single lookup action in the service desk tool (ServiceNow, Jira Service Management, or similar) that calls the View Recovery Lock Password endpoint using the service account, gated behind a ticket number field and asset tag confirmation. Expected result: technician receives the current passcode only after the ticket reference is logged. Evidence to capture: the webhook payload sent to the SIEM, shown in the sample below.

  7. Confirm command completion before closing the rotation cycle. Action: query command history for any device still Pending after 24 hours; do not mark that device compliant. Expected result: a residual queue of offline devices that rotate on next check-in rather than being silently skipped. Evidence to capture: exception report listing serials, last check-in timestamp, and pending command age.

Diagram of Recovery Lock rotation lifecycle from Jamf Pro to secrets vault to service desk retrieval

#Code: Rotation and Retrieval

Obtain a short-lived bearer token, then issue the SetRecoveryLockPassword command through the API:

1JAMF_URL="https://yourinstance.jamfcloud.com"
2TOKEN=$(curl -s -X POST "${JAMF_URL}/api/oauth/token" 
3  -H "Content-Type: application/x-www-form-urlencoded" 
4  -d "client_id=${JAMF_CLIENT_ID}&grant_type=client_credentials&client_secret=${JAMF_CLIENT_SECRET}" 
5  | python3 -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
6
7DEVICE_ID=1013
8NEW_PASSCODE=$(python3 - <<'PY'
9import secrets, string
10alphabet = string.ascii_uppercase + string.ascii_lowercase + string.digits
11alphabet = alphabet.replace('l','').replace('I','').replace('O','').replace('0','')
12print(''.join(secrets.choice(alphabet) for _ in range(14)))
13PY
14)
15
16curl -s -X POST 
17  "${JAMF_URL}/JSSResource/computercommands/command/SetRecoveryLockPassword/id/${DEVICE_ID}/passcode/${NEW_PASSCODE}" 
18  -H "Authorization: Bearer ${TOKEN}" 
19  -H "Accept: application/json"

Expected output on success:

1{
2  "computer_command": {
3    "command": "SetRecoveryLockPassword",
4    "computers": [
5      { "id": 1013, "name": "MBP-SEC-1013" }
6    ]
7  }
8}

Mirror the passcode to the vault and record a hash-only audit line, so the plaintext never sits in a rotation log:

1import hashlib
2import hvac
3
4def rotate_and_store(serial, new_passcode, vault_addr, vault_token):
5    client = hvac.Client(url=vault_addr, token=vault_token)
6    client.secrets.kv.v2.create_or_update_secret(
7        path=f"recovery-lock/{serial}",
8        secret={"passcode": new_passcode},
9    )
10    fingerprint = hashlib.sha256(new_passcode.encode()).hexdigest()[:12]
11    print(f"ROTATED serial={serial} fingerprint={fingerprint}")

Every retrieval by a technician emits a webhook to the SIEM so the access is auditable end to end:

1{
2  "event": "recovery_lock_retrieved",
3  "serial_number": "C02XXXXXXXX",
4  "ticket_number": "INC0045213",
5  "technician": "j.osei",
6  "requested_at": "2024-05-14T09:12:31Z",
7  "jamf_computer_id": 1013,
8  "approval_source": "service-desk-portal"
9}

Schedule the rotation job and the out-of-cycle trigger with a simple CI workflow:

1name: recovery-lock-rotation
2on:
3  schedule:
4    - cron: '0 3 1 */3 *'
5  workflow_dispatch:
6    inputs:
7      reason:
8        description: 'Out-of-cycle trigger reason'
9        required: true
10jobs:
11  rotate:
12    runs-on: ubuntu-latest
13    steps:
14      - name: Checkout
15        uses: actions/checkout@v4
16      - name: Run rotation script
17        env:
18          JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
19          JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
20          VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
21        run: python3 rotate_recovery_lock.py --batch-size 50

#Verification and Expected Evidence

  • Jamf Pro computer inventory record shows Recovery Lock Enabled = true and a command status of Acknowledged with a timestamp inside the current rotation window.
  • Vault entry for the serial number exists, has a 90 day TTL, and its access log shows only the break-glass role touched it.
  • SIEM contains one retrieval event per unlock, each tied to a ticket number; a retrieval with no matching ticket number is itself an alertable finding.
  • An extension attribute or custom field reporting days-since-last-rotation stays under 90 for every in-scope device; anything higher flags in the compliance dashboard.
  • Command history exception report is empty, or every entry on it corresponds to a device confirmed offline for a known reason (loaner in storage, decommissioned, awaiting repair).

Service desk dashboard showing audited Recovery Lock retrieval events tied to ticket numbers

#Rollback

If a rotation run causes downstream failures, for example a repair vendor was mid-service and needs the prior passcode reissued, the rollback path is another SetRecoveryLockPassword command, not a manual firmware reset. Reissue the last known-good value stored in the vault’s version history:

1curl -s -X POST 
2  "${JAMF_URL}/JSSResource/computercommands/command/SetRecoveryLockPassword/id/${DEVICE_ID}/passcode/${PREVIOUS_PASSCODE}" 
3  -H "Authorization: Bearer ${TOKEN}" 
4  -H "Accept: application/json"

To fully disable Recovery Lock on a device pending investigation, send an empty passcode value, which Apple’s protocol defines as the clear condition. Only use this on devices under active security review, never as a routine fallback, since it removes firmware-level protection until re-enabled.

#Failure and Escalation Conditions

  • A batch shows more than five percent command failures: pause the scheduler, do not proceed to the next batch, and open an L2 investigation into whether the failure is network-side (device offline) or authorisation-side (token or scope issue).
  • Recovery Lock reports Enabled = true on a device with no corresponding vault entry: treat this as a security incident, not a sync bug. It indicates the passcode was set outside this pipeline, possibly at the firmware level directly, and should wake an on-call security engineer immediately.
  • A retrieval webhook fires without a matching, open ticket number in the service desk system: escalate to L3 security operations for identity review of the technician account used.
  • Command status remains Pending for the same device across two consecutive rotation cycles (180 days): escalate to asset management to confirm the device has not been lost, stolen, or silently decommissioned.

#Measuring Ticket Deflection

Baseline the volume of tickets categorised as Recovery Lock unlock request or firmware passcode request for the 90 days before rollout. After rollout, track three numbers: the count of the same ticket category, the average handle time from ticket open to passcode delivered, and the percentage resolved entirely through the self-service retrieval path versus escalated to a human for manual lookup. A successful rollout should show the manual lookup path approaching zero, handle time dropping from minutes of ticket archaeology to under a minute of scoped API lookup, and a parallel rise in audit completeness, since every remaining retrieval now carries a ticket number and technician identity rather than a shared spreadsheet lookup.

#References

For the underlying Apple platform behaviour, see Apple’s own deployment guidance on Recovery Lock for Mac computers with Apple silicon and the command reference for the SetRecoveryLock MDM command. For the MDM-side implementation used in this playbook, see Jamf’s documentation on managing Recovery Lock in Jamf Pro.

Sources and verification

Vendor documentation and external technical references used by this playbook.

Advanced suggested reading

Ready to go deeper?

These articles come from KBY’s senior engineering editorial. Expect denser architecture, fewer introductory explanations and deeper production trade-offs.

Leaving Ops Playbook
Reader Interaction

Comments

Add a thoughtful note on Recovery Lock Rotation for Apple Silicon Macs at Scale. Comments are checked for spam and held for moderation before appearing.

Loading comments...

Loading anti-spam check...