Fix Recovery Lock password desync on MDM re-enrollment#43827
Draft
mostlikelee wants to merge 3 commits intomainfrom
Draft
Fix Recovery Lock password desync on MDM re-enrollment#43827mostlikelee wants to merge 3 commits intomainfrom
mostlikelee wants to merge 3 commits intomainfrom
Conversation
Adds the datastore-layer coverage for the MDMResetEnrollment soft-delete and the DeleteHost non-cascade: reset behavior on every row state, null-propagation guards against re-animation leaks, a table-driven host-detail status matrix, and notFound contracts for view/rotate readers on soft-deleted rows.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #43827 +/- ##
==========================================
+ Coverage 66.91% 66.93% +0.01%
==========================================
Files 2600 2600
Lines 208985 209001 +16
Branches 9305 9305
==========================================
+ Hits 139846 139895 +49
+ Misses 56397 56372 -25
+ Partials 12742 12734 -8
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue: Resolves #43786
Summary
Apple wipes a macOS host's Recovery Lock password whenever the MDM profile is removed, but Fleet's
host_recovery_key_passwordsstate machine had no hook into re-enrollment. Two concrete desyncs resulted:Verified-but-gone — a host has a verified Recovery Lock; the MDM profile is later removed (manually on the device, by an admin, or via Fleet); the device re-enrolls. Fleet still shows
status='verified'with the old password, but the device no longer has a Recovery Lock. The recovery-lock cron won't re-SET becauseGetHostsForRecoveryLockActionrequiresstatus IS NULL.Stuck-pending — the cron enqueued a
SetRecoveryLock, the device re-sentAuthenticatebefore acking (common during re-enrollment / SCEP renewal), and nanomdm'sClearQueuemarked the queue entryactive=0. The command was never delivered. Fleet's row sits atstatus='pending'indefinitely; no cron query matches that state.Both states share the same exit point:
mdmLifecycle.Do(HostActionReset)→MDMResetEnrollmentfires on every deviceAuthenticate. That function already deletes other state that's invalidated by re-enrollment (MDM profiles, disk encryption keys, bootstrap-package rows) but didn't touchhost_recovery_key_passwords. This PR adds the missing cleanup.What the change does
server/datastore/mysql/apple_mdm.go— InsideMDMResetEnrollment's darwin-specific cleanup block (after the existingscepRenewalInProgressshort-circuit), soft-delete the row and NULLpending_encrypted_password,pending_error_message, andauto_rotate_at. Soft-delete (rather than hard DELETE) preserves the row for support diagnostics; all live readers already filterdeleted=0, andSetHostsRecoveryLockPasswords'INSERT … ON DUPLICATE KEY UPDATEre-animates it withdeleted=0on the next cron tick. The extra NULLs are essential: the upsert only resetsencrypted_password,status,operation_type,error_message, anddeleted, so without this a staleauto_rotate_at(from a previously viewed password) would fire auto-rotation against a freshly re-SET password.server/datastore/mysql/hosts.go— Added a comment to theDeleteHostdocumentation block explaining thathost_recovery_key_passwordsis intentionally preserved across host deletion. The device may still be enrolled in Apple MDM with the password intact; Orbit re-enrollment recreates the host row and the existing password record remains usable for view/rotate. Only a real MDM re-enrollment invalidates the device-side state, andMDMResetEnrollmenthandles that path.Authenticatefrom the affected device. Hosts that never re-authenticate need manual SQL remediation (UPDATE host_recovery_key_passwords SET deleted=1 WHERE host_uuid=?); next cron tick re-enqueues a freshSetRecoveryLock.Why not
VerifyRecoveryLockor a refetch-time resetConsidered and deferred: periodic
VerifyRecoveryLocksweeps, refetch-time detection ofhost_mdm.enrolled = 0, and reset-on-MDMTurnOff(CheckOut).MDMResetEnrollmentis the right semantic boundary (re-enrollment implies the device-side lock is gone) and covers both desync scenarios with a single, minimal hook. The others would shrink stale-display windows but are not required for correctness and can land separately.Status matrix
statuspassword_availablepending,install, pw onlyverified,installfailed,installpending/failed,install, pending rotationpending/NULL,removefailed,removeChecklist for submitter
changes/43786-recovery-lock-reenroll-desync.SELECT *.Testing
host_uuid.Automated tests added
server/datastore/mysql/apple_mdm_test.goRecoveryLockResetOnMDMReEnrollment(6 subtests): verified/stuck-pending/rotation soft-delete,pending_encrypted_password/pending_error_message/auto_rotate_atnull-propagation guards, clean re-animation end-to-end, SCEP-renewal preserves the row.DeleteHostPreservesRecoveryLockPassword: byte-for-byte preservation acrossDeleteHost, including thedeletedflag.HostRecoveryLockStatusMatrix: 10 table-driven cases lockingGetHostRecoveryLockPasswordStatus+PopulateStatusoutput for every observable(status, operation_type, has_password, has_pending_pw, deleted)combination.RecoveryLockReadersReturnNotFoundForSoftDeleted:GetHostRecoveryLockPassword,GetRecoveryLockRotationStatus,HasPendingRecoveryLockRotation,GetHostRecoveryLockPasswordStatusall behave correctly for soft-deleted rows — critical because the EE rotate endpoint depends on the notFound branch to return "Host does not have a recovery lock password to rotate."server/service/integration_mdm_test.gore-enrollment soft-deletes stored password and cron re-SETssubtest inTestRecoveryLockPasswordIntegration: full enroll → verified →mdmClient.Reenroll()(canonical SCEP + Authenticate + TokenUpdate) → host detail API confirms absent → cron re-SETs → verified with a different password and different command UUID.Database migrations
New Fleet configuration settings
fleetd/orbit/Fleet Desktop