Recently, I implemented an interesting scenario involving Linux networking and VPN. These were the requirements:
Unprivileged LXC container running on Debian 13 host:
All internet-bound traffic from the container is routed via remote VPN.
Container must not be able to communicate with any other networks the host might be directly connected to.
The container itself has no knowledge about the VPN.
If the VPN does not work, the container can’t communicate with internet services.
Debian 13 host:
strongSwan handles IPSec IKEv2 connections with the remote VPN service.
Network traffic originating from the host system is not routed via the VPN service.
Remote IPSec IKEv2 VPN service:
Not under our control.
Assigns a single virtual IPv4 address.
The initial network diagram
IPv4 ranges (RFC1918) used in the example
192.168.99.0/24 – network the host is connected to
192.168.222.0/24 – host-only network for the container(s)
10.9.8.7/32 – VPN virtual IP
Let’s take a brief look at a base VPN configuration. It is very easy to configure a policy-based IKEv2 VPN for the host. Example swanctl.conf:
policy-based-vpn {
version = 2
local_addrs = 192.168.99.98
remote_addrs = remote-vpn-gateway.macadmin.cz
vips = 0.0.0.0
local {
auth = pubkey
id = vpn-client.macadmin.cz
}
remote {
auth = pubkey
id = remote-vpn-gateway.macadmin.cz
}
children {
netvpn {
remote_ts = 0.0.0.0/0
start_action = trap
}
}
}
The challenge is how to make it work for the traffic originating from the container and exclude all traffic originating from the host system. The host only network 192.168.222.0/24, the container is connected to, will have to be NATed behind a single IPv4 address to match the 1:1 expectation of the VPN service. First I tried to come up with a solution using policy-based VPN configuration, but after some thinking I figured a route-based VPN might be a better fit for this situation.
The plan:
Create XFRM interface ipsec0 and assign IPSec policy with match-all-traffic selector 0.0.0.0/0.
Create source routing table which will apply to all traffic originating from the host-only network and use it to route the traffic to ipsec0 interface.
Use nftables to configure Source NAT on ipsec0.
Make sure virtual IPv4 address assigned by the VPN server gets assigned to ipsec0 and not any another interface.
In What’s new in Apple device management and identity WWDC 25 session video, Apple has announced several improvements to Apple Business Manager and Apple School Manager. This article ignores ASM and focuses only on ABM, although most of the features are common for both.
ABM Customer API
The automation problem
It has been quite difficult to do any sort of granular automation with ABM.
For device assignments to MDM servers, admins have had the ability to automatically assign devices based on model family (Mac, iPhone, etc.). This works well in simple setups where all your Macs go to one MDM server. However, if you need to assign your iPhones to two different MDM servers, it would have to be done manually at least for some of them.
If all your purchased Apple devices are added to ABM, it becomes a very good source of inventory information since it has all the identifiers such as serial number, IMEI, MEID, and EID. MAC addresses for iPhone + iPad Wi-Fi and Bluetooth interfaces have just been announced 🥳. To import all of this to an inventory system, a CSV can be manually created. If you are lucky, your MDM vendor exposes ABM information through its API. If you are not lucky or some of your devices have to be in the “unassigned” state, you are back to manual CSV export.
The API solution
ABM is finally getting customer facing API. This has been a highly requested feature for a longtime in Apple admin community.
ABM user with Admin role will be able to issue API token which can then be used to interact with following endpoints:
List All Devices
GET /v1/orgDevices
Get Device Information
GET /v1/orgDevices/{id}
Get Device Management Service Information for a Device
GET /v1/orgDevices/{id}/relationships/assignedServer
List of Device Management Services
GET /v1/mdmServers
Get All Devices Assigned to Device Management Service
GET /v1/mdmServers/{id}/relationships/devices
Assign or Unassign Devices from Device Management Service
Imagine this situation: You have a couple of thousand Apple devices which are all enrolled to HappyOrg MDM solution via Automated Device Enrollment. Unfortunately, over the years, HappyOrg MDM turns into SadOrg MDM, and you finally decide to replace it with another solution, SuperOrg MDM. You integrate SuperOrg MDM with ABM, re-assign all the devices to it, and do the final step – unenroll all devices from SadOrg MDM. The question is what comes next.
On macOS 13 or earlier, “Device Enrollment” notification would appear from time to time.
Users can happily ignore this notification as long as they wish. To solve this problem admins would deploy some sort of persistent service with custom UI (LaunchAgent) which would nag the user more aggressively and run profiles renew -type enrollment behind the scenes with root privileges (LaunchDaemon) to force the notification to appear at the right time while hoping Do Not Disturb is not enabled . Plus these two components would need to talk to each other somehow possibly via XPC.
Of course I had to create such tool because we needed to switch enrollments just a few months before macOS 14 release 🙃.
On macOS 14 and 15, the notification is replaced by a full-screen Setup Assistant experience. Users can click on “Not now,” which defers the process by 8 hours. Then it becomes unskippable.
No need to create a custom UI, just call profiles renew -type enrollment. On macOS 15, root privileges are not even required for that.
On iOS, nothing would happen. Well.. unless you wipe the device instead of unenrollment to force regular ADE 🤡. Since you don’t want to be overly mean, it is probably better to ask your users to do the classic Device Enrollment, which is very obnoxious nowadays, and then keep track of who enrolled and who hasn’t to nudge the right people to do it.
After device management migration
This will only apply to macOS 26 and iOS 26 of course. Admins will set an enrollment deadline during MDM server re-assignment in ABM. It looks like no unenrollment from MDM server itself will be needed.
User will receive a notification letting them know they need to migrate and when the deadline is.
Migration happens in fullscreen Setup Assistant experience.
Managed apps installed by the previous MDM and their data can be preserved if the new MDM server utilizes await device configured feature and initiates app installation before release the device from the await state by sending DeviceConfigured MDM command.
It’s not clear yet whether iOS devices remain in supervised mode after re-enrollment. One would assume so but let’s wait until we can test it the process for real.
Grab bag
Enforce Managed Apple Account sign-in
ABM is getting an ability to limit sign-in to Apple Accounts on company-owned devices only to Managed Apple Accounts. This feature currently works as a single global switch with no dependency on MDM. If you need this to be more granular, make sure to submit feedback to Apple.
Learn which Personal Apple Accounts use the company domain
When initiating the capture process on a company domain in ABM in order to use it only for Managed Apple Accounts, you can see how many Personal Apple Accounts use the domain in their account username. Apple will finally provide an ability to download a list of all these usernames. This will help tremendously with user comms since organizations will be able to target it only on users who actually use the company domain for their PAA.
Update 2025-06-13: Unfortunately there is a fine print. This feature will apply only to a subset of Personal Apple Accounts which has signed into services such as Apple Developer Portal, AppleCare Enterprise Portal and Apple Push Notification services portal. With this limitation the usefulness of this feature is drastically reduced.
Custom URL for account driven enrollment
Account driven enrollments have relied on existence of https://companydomain.tld/.well-known/com.apple.remotemanagement resource. For some reason Apple has not implemented DNS-based verification for this flow. Verification via file served by a web server has proved to be challenge for certain organizations where device management people are just siloed too far way from people running the company main website.
To bypass this problem Account-driven enrollments will be able to use custom service discovery URL provided by MDM server.
Granular privileges for the Device Enrollment Manager role
Follow capabilities can be turned of for Device Enrollment Manager role:
Add and remove device management services <- This one I like a lot 🥳
Assign devices
Remove Activation Lock from organization-owned devices
Release devices from organization
Warranty information
ABM will now display AppleCare information in device details for applicable devices.
Developer services for Managed Apple Accounts
Managed Apple Accounts work for developer services like notarization, including command-line tools like notarytool and stapler, and app-specific passwords.
Previously admin would have to use App Store Connect API key in order to access notarization services.
MDM systems and the devices they manage do not always provide the necessary level of transparency for troubleshooting unexpected issues. Deploying certificates via SCEP is often one such case, where we may encounter multiple black boxes:
Generation of the SCEP configuration for a managed device by the MDM.
The SCEP client on the managed device generates a CSR and sends it to the CA via a SCEP PKIOperation HTTP POST message.
The CA and its process to generate and sign the certificate.
It can be very useful to inspect exactly what the client sends to the server. For PKIOperation, the SCEP client uses a CMS/PKCS#7 payload with an inner envelope encrypted using the public key from one of the certificates provided by the server in the GetCACert message. If you have access to the corresponding private key, the message can be easily intercepted and decrypted.
😞 This method won’t work if the SCEP server disregards the RFC 8894 Section 7.10 recommendation to use plain HTTP and instead places the entire exchange inside a TLS transport tunnel – HTTPS.
‼️The guide uses openssl command. While the subcommands are pretty standard they might not work on LibreSSL variant (which Apple ships with macOS). You may need to install pure OpenSSL.
(1) Use Wireshark or a similar tool to capture the network traffic when the SCEP client requests a certificate from the SCEP server. Interception can be performed on either the client or the server. Locate an HTTP POST message with $operation=PKIOperation in the URL and extract the binary content of the packet into a file – payload.p7b.
You can display certificate data by using following openssl command:
openssl pkcs7 -inform DER -in payload.p7b -print_certs -text
(2) Convert the binary DER format into PEM base64. openssl expects PEM by default so we won’t have explicitly define DER with -inform DER for each command.
openssl pkcs7 -inform DER -outform PEM -in payload.p7b -out payload.p7m
(3) Parse the payload.p7m file to see all data not just certificates:
openssl asn1parse -in payload.p7m
Look for inner PKCS7 envelope defined as pkcs7-data:
(4) We are interested in the hexadecimal data, which in this example starts at offset 62. This is the encrypted inner PKCS#7 envelope containing the CSR and usually the SCEP challenge secret. Extract this data into a separate file.
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.
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.
(Optional) User is asked to authenticate with the MDM server.
Mac downloads the enrollment profile and starts the enrollment process.
The device checks in and authenticates with the MDM server.
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.
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.
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.
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?
Attempting to complete the enrollment, macOS checks in with the MDM server. MachineInfo describes the installed macOS version as 14.2.1.
MDM server responds with HTTP 403 and ErrorCodeSoftwareUpdateRequired in the message bodym including OSVersion (target macOS version) set to 14.3.
Unmanaged Mac fetches available update via Software Update. Mac only sees 13.6.5 and 14.4.1
System update could not be installed error message is presented
When user attempts to proceed the process repeats and the same error appears again
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.
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.
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 (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.
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.
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).