Repairing Broken Jamf Enrollments for Fun and Profit

Another useful post in the Fun and Profit series!

One of the things that’s been the most difficult to deal with when running a Jamf Pro service is finding ADE devices that never get past this stage:

What’s happened is the enrollment in Jamf process has failed for *reasons*, usually network but could be as simple as someone shutting the laptop lid at the wrong time. The device at this stage has a dummy record in Jamf, it has the MDM profile and the correct communications certificates installed.

But no binary that makes all the policies. Or Self Service.

In the past this required a full nuke from orbit, because it was the only way to be sure. This is still an option in extreme cases but there is now another way. After much investigation (and big thanks to Mark Buffington at Jamf for assistance on this), and intentionally breaking a test Mac repeatedly I can now present a work around for this.

The Mac in the state above will have working MDM but does not have the Jamf binary. We can use this in a piece of zsh code to our advantage. The beauty of this method is that the detection is simple and doesn’t need to be over complicated.

Jamf Pro 10.36 (released in the last couple weeks) has an API command that can be used to re-enroll a Mac into the system. The caveat to this is the MDM component of the product must still be working as it relies on the InstallEnterpriseApp MDM command. The good news is devices in the state as shown above can likely be remediated. For everything else it’s a Monterey remote wipe and do over.

The workaround involves the following things:

  • Device has some form of bootstrapping installed via DEP prestage. In this case it’s a DEP Notify script.
  • A service account created in Jamf to perform the enrollment API command only.
  • Scripting in the DEP Notify script to detect and perform the remediation
  • Ensuring that there is network connectivity when you do this

Let’s start with the Jamf Pro service account. Good practice is to have a service account naming scheme and complex user unfriendly passwords as possible. Once you’ve created this, you need to base64 encode this for future use. I cover that in previous posts.

The account needs the following permissions set in Jamf Pro and no more.

Jamf Pro Server Objects – Computers – Read Only

Jamf Pro Server Actions – Send Computer Remote Command to Install Package

Now that you have the account created and you’ve generated the base64 version of the username and password, here’s the code to do the work step by step.

while ! /usr/bin/pgrep -xq Dock; do /bin/sleep 0.1; done

This loops around until we can confirm there is a user logged into the Mac. If you are using something to perform a cloud IdP login, it ensures that the computer also has a network connection to perform that login.

... invoke dep notify as user ...

if [ ! -f "/usr/local/bin/jamf" ] && [ ! -d "/Library/Application Support/JAMF" ];
then

...

The beginnings of this IF statement in bash/zsh basically look to see if either the jamf binary or the jamf working directory is missing. If it is, then proceed.

... dep notify code to display message to end user ...

# Find the current OS major version and device UDID
osmajver=$( /usr/bin/sw_vers -productVersion | /usr/bin/cut -d"." -f1 )
udid=$( /usr/sbin/ioreg -d2 -c IOPlatformExpertDevice | /usr/bin/awk -F\" '/IOPlatformUUID/{print $(NF-1)}' )

# Jamf API enroll mdm only command and url
# left intentionally blank
api="base 64 credentials here"
jssurl="https://jamf.pro.url/"

# Use credentials to get a temp API bearer token
# Different ways to parse this based on OS version
jsonresponse=$( /usr/bin/curl -s "${jssurl}api/v1/auth/token" -H "authorization: Basic ${api}" -X POST | /usr/bin/tr -d "\n" )

[ "$osmajver" -le 11 ] && token=$( /usr/bin/osascript -l 'JavaScript' -e "JSON.parse(\`$jsonresponse\`).token" )
[ "$osmajver" -ge 12 ] && token=$( /usr/bin/plutil -extract token raw -o - - <<< "$jsonresponse" )

In my previous posts, I discuss the concept and how to obtain the bearer token needed to authenticate to the Jamf Pro API. The api credentials and jssurl lines have to be filled in as there is no other way to obtain this information conveniently!

We also grab the OS major version so we can use appropriate code for the OS version. Big Sur or less uses the osascript to javascript JSON.parse method. Monterey has support in plutil to do this natively.

# Find the Jamf computer ID number
computerinfo=$( /usr/bin/curl -s "${jssurl}api/v1/computers-inventory?section=GENERAL&filter=udid%3D%3D%22${udid}%22" -H "authorization: Bearer ${token}" )

[ "$osmajver" -le 11 ] && computerid=$( /usr/bin/osascript -l 'JavaScript' -e "JSON.parse(\`$computerinfo\`).results[0].id" )

[ "$osmajver" -ge 12 ] && computerid=$( /usr/bin/plutil -extract results.0.id raw -o - - <<< "$computerinfo" )

A lot of Jamf’s new API is reliant on the computer ID number instead of the UDID. So we have to use the UDID to work out the ID number. And of course, different code for different OS versions.

# Send the Jamf MDM re-enrollment command
/usr/bin/curl -x POST "{$jssurl}api/v1/jamf-management-framework/redeploy/${computerid}" -H "accept: application/json" -H "authorization: Bearer ${token}" )

# Invalidate the token we're using
/usr/bin/curl -s "{$jssurl}api/v1/auth/invalidate-token" -H "authorization: Bearer ${token}" ) -X POST

Finally send the re-enrollment command and invalidate the token we were using.

fi

while [ ! -f "/usr/local/bin/jamf" ]; do sleep 0.1; done

Don’t forget to close the if statement! Now we have to wait for the enrollment to take place before we can proceed.

Everything can now proceed as normal from this point onward!

The entire code block looks like this:

# Wait for user login
while ! /usr/bin/pgrep -xq Dock; do /bin/sleep 0.1; done

.. invoke DEP Notify here ...

if [ ! -f "/usr/local/bin/jamf" ] && [ ! -d "/Library/Application Support/JAMF" ];
then

... dep notify code to display message to end user ...

   # Find the current OS major version and device UDID
   osmajver=$( /usr/bin/sw_vers -productVersion | /usr/bin/cut -d"." -f1 )
   udid=$( /usr/sbin/ioreg -d2 -c IOPlatformExpertDevice | /usr/bin/awk -F\" '/IOPlatformUUID/{print $(NF-1)}' )

   # Jamf API enroll mdm only command and url
   # left intentionally blank
   api="base 64 credentials here"
   jssurl="https://jamf.pro.url/"

   # Use credentials to get a temp API bearer token
   # Different ways to parse this based on OS version
   jsonresponse=$( /usr/bin/curl -s "${jssurl}api/v1/auth/token" -H "authorization: Basic ${api}" -X POST | /usr/bin/tr -d "\n" )

   [ "$osmajver" -le 11 ] && token=$( /usr/bin/osascript -l 'JavaScript' -e "JSON.parse(\`$jsonresponse\`).token" )
   [ "$osmajver" -ge 12 ] && token=$( /usr/bin/plutil -extract token raw -o - - <<< "$jsonresponse" )

   # Find the Jamf computer ID number
   computerinfo=$( /usr/bin/curl -s "${jssurl}api/v1/computers-inventory?section=GENERAL&filter=udid%3D%3D%22${udid}%22" -H "authorization: Bearer ${token}" )

   [ "$osmajver" -le 11 ] && computerid=$( /usr/bin/osascript -l 'JavaScript' -e "JSON.parse(\`$computerinfo\`).results[0].id" )
   [ "$osmajver" -ge 12 ] && computerid=$( /usr/bin/plutil -extract results.0.id raw -o - - <<< "$computerinfo" )

   # Send the Jamf MDM re-enrollment command
   /usr/bin/curl -x POST "{$jssurl}api/v1/jamf-management-framework/redeploy/${computerid}" -H "accept: application/json" -H "authorization: Bearer ${token}" )

   # Invalidate the token we're using
   /usr/bin/curl -s "{$jssurl}api/v1/auth/invalidate-token" -H "authorization: Bearer ${token}" -X POST

fi

while [ ! -f "/usr/local/bin/jamf" ]; do sleep 0.1; done

And that’s it! So if a device that doesn’t enroll properly goes out to a user, we can at least attempt to automatically enroll it properly again at the expense of some extra time during any deployment process you may have. I think the extra few minutes is worth the trade off for the ticket reduction this will cause.

I hope this all proves useful for you all out there.