Home > Casper, Mac administration, Mac OS X, Scripting > Automatically fixing MDM certificate enrollment with Casper 9.x

Automatically fixing MDM certificate enrollment with Casper 9.x


UPDATE – 9-22-2014: JAMF brought back the jamf mdm command in Casper 9.4, so it’s possible to use the workflow described in this post in Casper 9.4 and later.


A while back, I wrote a post on fixing Casper Mac MDM enrollment. This post covered my experiences with Casper 8.7.x and provided a method to automatically fix any problems with the MDM certificate. Unfortunately, the method that works in 8.7.x does not work in 9.x because the command that I use to do the MDM enrollment in Casper 8.x is jamf mdm. As part of the change from Casper 8.x to 9.x, the function performed by the jamf mdm command is now handled by the jamf manage command in Casper 9.x. The jamf mdm command itself does not exist in Casper 9.x

To duplicate the general process which I’m using in Casper 8.x, I needed to run the following commands:

/usr/sbin/jamf removeMdmProfile -verbose
/usr/sbin/jamf manage -verbose
/usr/sbin/jamf recon

The issue I ran into is that jamf manage waits until all policies are finished running, which meant that the MDM fix is running after the jamf recon command completes its inventory update and sends it on to the Casper server. The consequence is that the Casper server would never be informed that the machine had actually been fixed, which potentially cues an infinite loop of fixing a problem which is already fixed.

So I had two issues:

1. I wanted to fix my problem with a Casper smart group that would contain only affected machines and an associated Casper policy that would fix the machines in the smart group. This would allow the problem to be automatically detected and then fixed without the need for human intervention.
2. I needed to make reasonably sure all policies were finished running before trying to run the jamf manage command. Otherwise, running jamf manage would result in the recon running before the MDM certificate gets fixed.

On top of that, I preferred that jamf manage only be run once rather than building a process that potentially ran it a large number of times.

To sum up:

A) I wanted to fix the problem automatically with a Casper policy.
B) I couldn’t directly fix this with a Casper policy. Running the commands above using a policy would mean that jamf manage and jamf recon would not run in the order I wanted them to, with the undesired “infinite loop” consequences described above.

Shea Craig gave me the idea of using a LaunchDaemon and script to run the commands I needed, but I still needed a reliable way of determining if Casper policies were running. Shea’s approach relies on killing the jamf process as needed, but that ran the risk of interrupting any active policies or other tasks that were running.

After mulling over the problem for a while, I thought of another way to determine if a policy was running. /var/log/jamf.log is updated when Casper policies or check-ins run on an individual Mac, so if the log hasn’t been updated in a while, it is very unlikely that a policy is running.

Using this idea, I wrote a script and an associated LaunchDaemon to perform the following tasks:

1. Verify that the Mac can contact the Casper server.
2. Verify that /var/log/jamf.log has not been written to in the past five minutes.
3. If /var/log/jamf.log has not been written to in the past five minutes, fix the MDM certificate and communicate that it is fixed to the Casper server.
4. Delete itself and its associated launchdaemon.

For the details, see below the jump.

Script:


#!/bin/bash
#
# User-editable variables
#
# For the jss_server_address variable, put the complete
# fully qualified domain name address of your Casper server
jss_server_address="casper.server.here"
# For the jss_server_address variable, put the port number
# of your Casper server. This is usually 8443; change as
# appropriate.
jss_server_port="8443"
CheckSiteNetwork (){
# CheckSiteNetwork function adapted from Facebook's check_corp function script.
# check_corp script available on Facebook's IT-CPE Github repo:
#
# check_corp:
# This script verifies a system is on the corporate network.
# Input: CORP_URL= set this to a hostname on your corp network
# Optional ($1) contains a parameter that is used for testing.
# Output: Returns a check_corp variable that will return "True" if on
# corp network, "False" otherwise.
# If a parameter is passed ($1), the check_corp variable will return it
# This is useful for testing scripts where you want to force check_corp
# to be either "True" or "False"
# USAGE:
# check_corp # No parameter passed
# check_corp "True" # Parameter of "True" is passed and returned
site_network="False"
ping=`host -W .5 $jss_server_address`
# If the ping fails – site_network="False"
[[ $? -eq 0 ]] && site_network="True"
# Check if we are using a test
[[ -n "$1" ]] && site_network="$1"
}
CheckTomcat (){
# Verifies that the JSS's Tomcat service is responding via its assigned port.
tomcat_chk=`nc -z -w 5 $jss_server_address $jss_server_port > /dev/null; echo $?`
if [ "$tomcat_chk" -eq 0 ]; then
/usr/bin/logger "Machine can connect to $jss_server_address over port $jss_server_port. Proceeding."
else
/usr/bin/logger "Machine cannot connect to $jss_server_address over port $jss_server_port. Exiting."
exit 0
fi
}
CheckLogAge (){
# Verifies that the /var/log/jamf.log hasn't been written to for at least five minutes.
# This should help ensure that jamf manage can run and not have to wait for a policy to
# finish running.
jamf_log="/var/log/jamf.log"
current_time=`date +%s`
last_modified=`stat -f %m "$jamf_log"`
if [[ $(($current_time-$last_modified)) -gt 300 ]]; then
/usr/bin/logger "Log has not been modified in the past five minutes. Proceeding."
else
/usr/bin/logger "Log has been modified in the past five minutes. Exiting."
exit 0
fi
}
FixMDM (){
# Verifies that the Mac can communicate with the Casper server.
# Once communication is verified, it takes the following actions:
#
# 1. Removes the existing MDM certificate if one exists
# 2. Runs jamf manage to fix the certificate
# 3. Runs a recon to send an updated inventory to the JSS to report
# that the MDM certificate is fixed.
#
jss_comm_chk=`/usr/sbin/jamf checkJSSConnection > /dev/null; echo $?`
if [[ "$jss_comm_chk" -gt 0 ]]; then
/usr/bin/logger "Machine cannot connect to the JSS. Exiting."
exit 0
elif [[ "$jss_comm_chk" -eq 0 ]]; then
/usr/bin/logger "Machine can connect to the JSS. Fixing MDM"
/usr/sbin/jamf removeMdmProfile -verbose
/usr/sbin/jamf manage -verbose
/usr/sbin/jamf recon
fi
}
SelfDestruct (){
# Removes script and associated LaunchDaemon
if [[ -f "/Library/LaunchDaemons/com.company.fixcaspermdm.plist" ]]; then
/bin/rm "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
fi
srm $0
}
CheckSiteNetwork
if [[ "$site_network" == "False" ]]; then
/usr/bin/logger "Unable to verify access to site network. Exiting."
fi
if [[ "$site_network" == "True" ]]; then
/usr/bin/logger "Access to site network verified"
CheckTomcat
CheckLogAge
FixMDM
SelfDestruct
fi
exit 0

view raw

fixcaspermdm.sh

hosted with ❤ by GitHub

The LaunchDaemon assumes the script above has been installed as /var/root/fixcaspermdm.sh. The LaunchDaemon runs the script when the LaunchDaemon is loaded and then attempts to run it once every 10 minutes thereafter.

LaunchDaemon:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.company.fixcaspermdm</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>/var/root/fixcaspermdm.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>600</integer>
</dict>
</plist>

I still had the issue of deploying this solution. I decided to fix this with a payload-free package, which performs the following tasks

1. Determines if the Mac is running 10.7.x or later.
2. If any previous instances of a specified LaunchDaemon and script exist, unload the LaunchDaemon and remove the LaunchDaemon and script.
3. Create the LaunchDaemon described above by using cat input redirection to write the XML contained below to a new file.
4. Create the script by using cat input redirection to write the script described above to a new file.
5. Once the LaunchDaemon file has been created, fix the permissions so that the file is owned by root:wheel and set to not be executable.
6. After the permissions have been updated, move the LaunchDaemon into place in /Library/LaunchDaemons.
7. Once the script file has been created, fix the permissions so that the file is owned by root:wheel and set to be executable.
8. After the permissions have been updated, move the script into the place that it will be executed from.
9. After the LaunchDaemon and script are in place with proper permissions, load the LaunchDaemon to begin the script’s execution.

Payload-Free Package Script:


#!/bin/bash
# Determine OS version
OSVERS=$(sw_vers -productVersion | awk -F. '{print $2}')
# Macs running 10.6.x or earlier are not able to use profiles.
# If the script detects that it is running on an OS earlier than
# 10.7.0, the script will exit at this point to avoid problems.
if [[ ${OSVERS} -lt 7 ]]; then
/usr/bin/logger "MDM profiles are not supported on this version of Mac OS X."
exit 0
fi
if [[ ${OSVERS} -ge 7 ]]; then
# If any previous instances of the fixcaspermdm LaunchDaemon and script exist,
# unload the LaunchDaemon and remove the LaunchDaemon and script files
if [[ -f "/Library/LaunchDaemons/com.company.fixcaspermdm.plist" ]]; then
/bin/launchctl unload "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
/bin/rm "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
fi
if [[ -f "/var/root/fixcaspermdm.sh" ]]; then
/bin/rm "/var/root/fixcaspermdm.sh"
fi
# Create the fixcaspermdm LaunchDaemon by using cat input redirection
# to write the XML contained below to a new file.
#
# The LaunchDaemon will run at load and every ten minutes thereafter.
/bin/cat > "/tmp/com.company.fixcaspermdm.plist" << 'CASPER_MDM_FIX_LAUNCHDAEMON'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.company.fixcaspermdm</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>/var/root/fixcaspermdm.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>600</integer>
</dict>
</plist>
CASPER_MDM_FIX_LAUNCHDAEMON
# Create the fixcaspermdm script by using cat input redirection
# to write the shell script contained below to a new file.
#
# You will need to change the "jss_server_address" variable in the
# script below. Please put the complete fully qualified domain name
# address of your Casper server.
#
# You may need to change the "jss_server_port" variable in the
# script below. Please put the port number of your Casper server
# if it is different than 8443.
/bin/cat > "/tmp/fixcaspermdm.sh" << 'CASPER_MDM_FIX_SCRIPT'
#!/bin/bash
#
# User-editable variables
#
# For the jss_server_address variable, put the complete
# fully qualified domain name address of your Casper server
jss_server_address="casper.server.here"
# For the jss_server_address variable, put the port number
# of your Casper server. This is usually 8443; change as
# appropriate.
jss_server_port="8443"
CheckSiteNetwork (){
# CheckSiteNetwork function adapted from Facebook's check_corp function script.
# check_corp script available on Facebook's IT-CPE Github repo:
#
# check_corp:
# This script verifies a system is on the corporate network.
# Input: CORP_URL= set this to a hostname on your corp network
# Optional ($1) contains a parameter that is used for testing.
# Output: Returns a check_corp variable that will return "True" if on
# corp network, "False" otherwise.
# If a parameter is passed ($1), the check_corp variable will return it
# This is useful for testing scripts where you want to force check_corp
# to be either "True" or "False"
# USAGE:
# check_corp # No parameter passed
# check_corp "True" # Parameter of "True" is passed and returned
site_network="False"
ping=`host -W .5 $jss_server_address`
# If the ping fails – site_network="False"
[[ $? -eq 0 ]] && site_network="True"
# Check if we are using a test
[[ -n "$1" ]] && site_network="$1"
}
CheckTomcat (){
# Verifies that the JSS's Tomcat service is responding via its assigned port.
tomcat_chk=`nc -z -w 5 $jss_server_address $jss_server_port > /dev/null; echo $?`
if [ "$tomcat_chk" -eq 0 ]; then
/usr/bin/logger "Machine can connect to $jss_server_address over port $jss_server_port. Proceeding."
else
/usr/bin/logger "Machine cannot connect to $jss_server_address over port $jss_server_port. Exiting."
exit 0
fi
}
CheckLogAge (){
# Verifies that the /var/log/jamf.log hasn't been written to for at least five minutes.
# This should help ensure that jamf manage can run and not have to wait for a policy to
# finish running.
jamf_log="/var/log/jamf.log"
current_time=`date +%s`
last_modified=`stat -f %m "$jamf_log"`
if [[ $(($current_time-$last_modified)) -gt 300 ]]; then
/usr/bin/logger "Log has not been modified in the past five minutes. Proceeding."
else
/usr/bin/logger "Log has been modified in the past five minutes. Exiting."
exit 0
fi
}
FixMDM (){
# Verifies that the Mac can communicate with the Casper server.
# Once communication is verified, it takes the following actions:
#
# 1. Removes the existing MDM certificate if one exists
# 2. Runs jamf manage to fix the certificate
# 3. Runs a recon to send an updated inventory to the JSS to report
# that the MDM certificate is fixed.
#
jss_comm_chk=`/usr/sbin/jamf checkJSSConnection > /dev/null; echo $?`
if [[ "$jss_comm_chk" -gt 0 ]]; then
/usr/bin/logger "Machine cannot connect to the JSS. Exiting."
exit 0
elif [[ "$jss_comm_chk" -eq 0 ]]; then
/usr/bin/logger "Machine can connect to the JSS. Fixing MDM"
/usr/sbin/jamf removeMdmProfile -verbose
/usr/sbin/jamf manage -verbose
/usr/sbin/jamf recon
fi
}
SelfDestruct (){
# Removes script and associated LaunchDaemon
if [[ -f "/Library/LaunchDaemons/com.company.fixcaspermdm.plist" ]]; then
/bin/rm "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
fi
srm $0
}
CheckSiteNetwork
if [[ "$site_network" == "False" ]]; then
/usr/bin/logger "Unable to verify access to site network. Exiting."
fi
if [[ "$site_network" == "True" ]]; then
/usr/bin/logger "Access to site network verified"
CheckTomcat
CheckLogAge
FixMDM
SelfDestruct
fi
exit 0
CASPER_MDM_FIX_SCRIPT
# Once the LaunchDaemon file has been created, fix the permissions
# so that the file is owned by root:wheel and set to not be executable
# After the permissions have been updated, move the LaunchDaemon into
# place in /Library/LaunchDaemons.
/usr/sbin/chown root:wheel "/tmp/com.company.fixcaspermdm.plist"
/bin/chmod 755 "/tmp/com.company.fixcaspermdm.plist"
/bin/chmod a-x "/tmp/com.company.fixcaspermdm.plist"
/bin/mv "/tmp/com.company.fixcaspermdm.plist" "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
# Once the script file has been created, fix the permissions
# so that the file is owned by root:wheel and set to be executable
# After the permissions have been updated, move the script into the
# place that it will be executed from.
/usr/sbin/chown root:wheel "/tmp/fixcaspermdm.sh"
/bin/chmod 755 "/tmp/fixcaspermdm.sh"
/bin/chmod a+x "/tmp/fixcaspermdm.sh"
/bin/mv "/tmp/fixcaspermdm.sh" "/var/root/fixcaspermdm.sh"
# After the LaunchDaemon and script are in place with proper permissions,
# load the LaunchDaemon to begin the script's execution.
/bin/launchctl load -w "/Library/LaunchDaemons/com.company.fixcaspermdm.plist"
fi
exit 0

view raw

gistfile1.txt

hosted with ❤ by GitHub

I then created a payload-free package using the script above and Payload-Free Package Creator.

Now I had a solution that would fix the MDM certificate, but I still needed to be able to identify machines with the MDM certificate problem and then deploy the solution with Casper 9.x. Fortunately, here I could use the same strategy on Casper 9.x that I had on Casper 8.x.

To help identify machines with this problem, JAMF provides three extension attributes with your Casper JSS server to help you identify machines with either problematic SSL certificates or missing MDM certificates.

JSS Certificate Validation
Verify Certificate Based Communication
Verify MDM Enrollment

All can be installed from the JAMF Software category of your Casper server’s Computer Extension Attribute Templates.

Screen Shot 2014-06-12 at 9.54.49 PM

From there, you can set up a Smart Group to look for machines that fit the following criteria:

JSS Certificate Validation – Success
Verify Certificate Based Communication – Enabled
Verify MDM Enrollment – Not Enrolled

It should also currently be scoped to look for Macs running 10.7.x or higher, as earlier OSs won’t be enrolled in MDM.

Here’s how the smart group I set up looks in Casper 9.3.x:

Screen Shot 2014-06-14 at 6.12.50 PM

From there, set up a policy that is scoped to run on members of that smart group. The policy I set up will install the payload-free package, which in turn will create and load the LaunchDaemon and script to fix the problem.

Once the script has fixed the MDM certificate issue, the script will trigger a new inventory to run and send the results to the Casper server. The updated inventory should then allow the JSS to detect that the MDM certificate has been installed and take the machine out of the smart group.

Here’s how the policy I set up looks in Casper 9.3.x:

Screen Shot 2014-06-14 at 4.57.36 PM

Screen Shot 2014-06-14 at 5.44.19 PM

Screen Shot 2014-06-14 at 4.58.20 PM

  1. Michael Crispin
    June 21, 2014 at 5:22 am

    Holy script Batman! This gets around a number of cul-de-sacs I had been trying to get my head around for a while. Nice work. So nice in fact, I shall call this “The Delighter” (per Dan Gruber via http://penny-arcade.com/comic/2014/06/20). Too bad it doesn’t work on that hideous Amazon Fire GUI. 😉

  2. Jonathan Perel
    May 29, 2015 at 5:18 pm

    How does this deal with systems on Wi-Fi where the Wi-Fi network has been pushed out via a Configuration Profile? I’ve seen that running jamf removeMdmProfile instantly kicks the user off Wi-Fi making the following jamf manage step not work.

  1. No trackbacks yet.

Leave a comment