Trouble with macOS updates before ADE enrollment

macOS 14 Sonoma and iOS 17 have introduced a neat new feature that enables the MDM server to force the device to update itself during the Automated Device Enrollment process, right before the actual MDM enrollment.

Enforcing a minimum version of iOS, iPadOS and macOS
MDM solutions can enforce a minimum operating system version on enrolling devices when using Automated Device Enrolment. If the device doesn’t meet the minimum version expected by MDM, the user is guided through a software update or upgrade before they can continue with Setup Assistant. This ensures that devices owned by an organisation are on the necessary version required before being put into production.

Automated Device Enrolment and MDM page in Apple Platform Deployment guide

Feature demonstration on macOS Sonoma

We start with the usual steps on a new Mac:

  1. macOS boots and launches Setup Assistant.
  2. Mac connects to the internet.
  3. If the Mac exists in ABM/ASM and has an MDM server assigned, a cloud profile containing a link to the enrollment profile and additional ADE configuration is downloaded.
  4. (Optional) User is asked to authenticate with the MDM server.
  5. Mac downloads the enrollment profile and starts the enrollment process.
  6. The device checks in and authenticates with the MDM server.

Note: The enrollment process is covered in Managing MDM Connections Apple Developer article

Here is what’s new:

  1. During the first check-in, the device sends a MachineInfo object to the MDM server. MachineInfo contains the macOS version currently installed on the device. There is a new flag, MDM_CAN_REQUEST_SOFTWARE_UPDATE (default: false), which macOS 14+ and iOS 17+ devices going through ADE set to true.
  2. MDM server can compare the client OS version with the desired OS version specified by a system administrator. If the device has an older OS version installed, the server responds with HTTP 403 and ErrorCodeSoftwareUpdateRequired XML/JSON body specifying the OS version the device should update to.

Implementation

Most MDM vendors have implemented this feature with a dropdown menu where the admin can select the target macOS version. Sometimes this list is manually updated by the vendor. Other vendors might pull version information directly from Apple services. This might be the case with Kandji, but I suspect there is manual approval since there is a significant delay (hours) between macOS update availability and the presence of the new target version in the list.

Drop down with list of available macOS updates

Unfortunately, both Apple and MDM vendors failed to take a key fact into account. There is a difference between the list of updates managed and unmanaged Macs retrieve from Apple Software Update services. While managed Macs see a list containing multiple minor versions of each major macOS release, unmanaged Macs see only the latest minor version of each major macOS release.

This is because the macs don’t see the “managed” assets (like how MDM can specify individual versions. For example, at the setup assistant, macs only see the PublicAssetSets from gdmf, and not the updates listed under AssetSets.

Posted by chilcote on Mac Admins Slack

The problem

So what happens when a Mac running macOS 14.2 is instructed to update itself to macOS 14.3 while a newer version, 14.4.1, is also available?

  1. Attempting to complete the enrollment, macOS checks in with the MDM server. MachineInfo describes the installed macOS version as 14.2.1.
  2. MDM server responds with HTTP 403 and ErrorCodeSoftwareUpdateRequired in the message bodym including OSVersion (target macOS version) set to 14.3.
  3. Unmanaged Mac fetches available update via Software Update. Mac only sees 13.6.5 and 14.4.1
  4. System update could not be installed error message is presented
  5. When user attempts to proceed the process repeats and the same error appears again
System update could not be installed error

Short term solution

Always set the update target to the latest minor version of a major macOS release. Be prepared when new macOS update is about to drop so you can change the setting quickly.

Ask your MDM vendor to require the latest minor version automatically. Kandji already has this feature for iOS.

Drop down with list of available iOS updates and special option called l Latest public release

Submit feedback with Apple. You can duplicate mine FB13691581.

Long term solution:

It is possible that Apple may be able to make changes in the Software Update Service (server-side) to offer the same list of updates to both unmanaged and managed Macs.

Alternatively, Apple could change the implementation on the client side. Perhaps the client could try to update itself to the closest possible version when the desired version is not visible. However, such a change would help Mac admins 1-2 years when majority of Macs in boxes have the OS version containing this feature installed.

Additional resources

Allowed lifetime of certificates issued by internal CA

Last year Apple forced the industry to only accept TLS certificates with validity up to maximum of 398 days. This is documented in HT211025 article. However there is a note explicitly excluding the certificate issued by an internal CA:

This change will not affect certificates issued from user-added or administrator-added Root CAs.

Because of this I assumed I could get away with 3 year validity for a certificate issues by our new internal CA. Turns out I was wrong.

Safari 14.1.1 refuses to connect to a site with freshly issued 3-year TLS certificate. So does Chrome 91 but it is more informative about it and presents an error message: NET::ERR_CERT_VALIDITY_TOO_LONG.

Previous change of TLS certificate requirements from 2019 described in article HT210176 article limits the certificate validity to 825 days. There aren’t any exception listed in this articles. Would Safari and Chrome trust certificate with 2-year validity?

Yes. Both Safari (14.1.1) and Chrome (91) in macOS 11.4 accept the 2-year certificate signed by internal CA as secure.

InstallApplications Swiftly

InstallApplications Swiftly (IAS) intends to be a replacement for InstallApplications (IA) tool. Code is not yet ready for public release. I still need to finish some parts and do the final refactoring not to mention proper testing. Expect alpha version in three to four weeks. 

This blog post outlines the development goals, implementation decisions and differences between IA and IAS.

Main goals

  • Swift code using Apple frameworks whenever possible. Minimum external dependencies.
  • Smallest footprint possible. Less than 10 files with the total size under 1 MB. 
  • Speed. Files downlod in parallel on multiple threads. Option to mark the item to be executed in parallel with other items.
  • Compatibility with InstallApplications JSON control file format.

Implementation

  • OOP model.
  • File downloads handled by URLSession.
  • Multithreading implemented with GCD.
  • LaunchDaemon (iasd) + LaunchAgent (iasagent) model stays but with differences. IAS logic is implemented solely in the iasd. Userscript execution is delegated to iasagent using the XPC remote object calls.
  • No argument parsing. Configuration is provided via Plist.
  • Logging exclusively to the system unified log with Logger.
  • Target is macOS 11.0 or newer.
Main Thread
JSONControlItem.download()
JSONControlItem.download()
IAS.beginRun()
semaphore.signal()
semaphore.signal()
JSONControlItem.parse()
JSONControlItem.parse()
R
R
R
R
R
R
R
R
DispatchGroup.wait()
DispatchGroup.wait()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
Dowload threads
Dowload threads
R
R
R
R
J
J
Extra execution threads
Extra execution thre…
Prefligh.downloadResources()
Prefligh.downloadResources()
Prefligh.execute()
Prefligh.execute()
SetupAssistant phase
SetupAssistant phase
Prefligh phase
Prefligh phase
P
P
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
P
P
R
R
SetupAssistant.downloadResources()
SetupAssistant.downloadResources()
Userland.downloadResources()
Userland.downloadResources()
SetupAssistant.execute()
SetupAssistant.execute()
Userland.execute()
Userland.execute()
R
R
R
R
R
R
U
U
U
U
P
P
Wait for iasagent to be loaded
Wait for iasagent to be loaded
R
R
P
P
DispatchGroup.wait()
DispatchGroup.wait()
R
R
semaphore.signal()
semaphore.signal()
R
R
Userland phase
Userland phase
(1) Sequential task execution
(1) Sequential task execution
(2) Parallel group execution
(2) Parallel group execution
(3) Async (donotwait) task execution
(3) Async (donotwait) task executi…
(4) Sequential task execution
(4) Sequential task execution
Connection to iasagent etablished
Connection to iasagent etablished
DispatchGroup.enter()
DispatchGroup.enter()
semaphore.signal()
semaphore.signal()
R
R
U
U
U
U
IAS.cleanUp() && exit()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
semaphore.signal()
donotwait
item
donotwait…
synchronous item
synchronous i…
parallel group
item
parallel grou…
XPC calls
XPC calls
P = package
P = package
J = JSON
J = JSON
R = Rooscript
R = Rooscript
U = Userscript
U = Userscript
Return value
Return value
NSXPCConnection()
NSXPCConnection()
Viewer does not support full SVG 1.1

Advantages over IA

  • IAS download and install should be much quicker. Current Python.framework (IA 2.1 Alpha 1) has 6000+ files and is 40+ MB in size (compressed). Size of IAS binaries is currently less than 1 MB.
  • Workflow should complete faster since it’s no longer linear cycle download -> execute -> download -> execute.
  • More secure file permission model bacause iasagent only needs to read and execute userscript files.
  • Code signing and notarization should be easier compared to Python framework with many executable files.

Disadvantages over IA

  • If you run Python scripts as part of IA run you will have to package your own Python.framework and deploy as one of the first items in the IAS workflow.
  • Swift standard library does not have argument parser. I did not want to use any Swift package for this so I decided do configure iasd solely via plist (Either passed as first argument or known filename in the same directory).
  • I am not aiming for backward compatibility. macOS 11.0 or newer is currently the target.

Other new features

Multiple preflight scripts

Ability to run multiple preflight rootscript items if desired. Preflight script are always executed in parallel. If either one of them exists with non zero exit code IAS proceeds to the SetupAssistant phase.

Hash validation modes

Ability to control SHA-256 validation. There are three options set in the configuration Plist: 

  • Fail: Abort run if computed hash does not match after multiple redownloads (IA behavior, default)
  • Warning: Proceed with the execution if the hash of the two subsequent file downloads is the same. Issue a warning to the system log.
  • None: Do not check the SHA-256 hash at all.

Item failure responses

Ability to configure response to item failure. There is a new option applicable for every SetupAssistant and Userland phase items:

  • Failable: If download fails item execution is skipped with an error message in the log.
  • FailableExecution: Download failure aborts the IAS run. Item execution finishing with non-zero exit code does not end the IAS run (IA behavior, default).
  • FailureIsNotAnOption: If either download fails or execution returns non-zero exit code IAS run is aborted.

Possible future features

  • Support configuration via the configuration profiles (NSUserDefaults).
  • Reintroduce the option to automatically write some of the status messages to DEP notify control file.
  • (Danger zone) Use private frameworks to get list of package receipts (PKInstallHistory) and start package installs (unkown if possible).

Site update 2021

Macadmin.cz is now dual language site. I decided to do this to be able to reach out to global Mac Admin community.

Older blog posts in Czech language can be found in Czech version of the site.