The Demise of Firmware Passwords on M1 Macs

Last week, Mr. Macintosh gave a great presentation on the changes introduced with macOS Big Sur on M1 Macs for reinstalling macOS. Click here for the recorded presentation and here for the presentation slides. Having recently begun testing an M1 MacBook Air myself, I was very interested in getting a jump start on this.

There were a number of important takeaways from the presentation, but one in particular I learned was that M1 Macs no longer support the firmware password feature. Sure enough, reviewing Apple’s own support article on firmware passwords:

Having previously worked at a school, setting a firmware password was a critical security feature as it prevented anyone without this password from booting to anything other than the configured default boot volume. This ensured more tech-savvy users could not load their own bootable macOS from the organization’s hardware or boot to the recovery OS and potentially wipe the device entirely.

While Apple indicates in their support article that FileVault achieves the equivalent level of security, the question is even with FileVault enabled does this open up the ability for users to do mischief?

TL;DR

  • On Intel Macs, the only security mechanism that prevents an admin user from booting to a different OS or the recovery OS is a configured firmware password.
  • In Big Sur, external booting of validly signed macOS installers and macOS boot volumes is now permitted by default.
  • Given M1 Macs do not have a firmware password option, any user with valid admin credentials can load macOS installers, other bootable macOS volumes, as well as fully erase and reinstall macOS.
  • So long as your users are not admins, Apple’s claim of FileVault being an equivalent level of security is valid… but only in this context.

For more info, see past the jump.

Read More

Running Jamf Scripts with Custom Command Line Arguments at Runtime (Without Jamf Remote)

In a previous role, I had created custom scripts with Parameter values to be used in Jamf Remote by my IT support colleagues. This provided a simple interface for common support tasks while also being able to supply custom values to scripts without having to copy & paste complex command line arguments.

In the case where a Mac had an incorrect hostname, a support technician would simply launch & authenticate into the Jamf Remote app, select the appropriate device and script, enter the desired hostname in the appropriate Parameter value field, and click Go!

This was made possible by the fact that:

  1. All our Macs were on-premise or remotely connected via VPN.
  2. Our Jamf Pro Server was also on-premise.

In a Jamf Cloud & work from home environment, this simply isn’t possible in the same way even if you are able to remotely connect to your Macs. That said, there are still plenty of use cases where this functionality is useful. This is limited by the fact that scripts attached to policies can have custom variables assigned at the policy level but not at runtime. To accomplish the same hostname functionality would require the support technician to have access and comfort to edit a policy, change a Parameter value associated with it, and trigger the policy from the user’s Mac via sudo jamf policy -event <custom value>. Depending on the level of your support team, this may simply not be an option.

As of Jamf Pro Server version 10.28.0, the previously available jamf runScript for local scripts is being deprecated and while this did not achieve the same functionality of running scripts within Jamf it renewed my interest in finding a replacement.

Thankfully, a fellow Jamf user commented on a related feature request with a solution. With a simple bash script function added to your script tied to a policy, you can call Jamf policies on the command line and supply custom variables at runtime!

https://raw.githubusercontent.com/kennyb-222/Jamf-Policy-CLI-Parameters/main/jamf_script_params.sh

The function parses the Jamf policy command based on its process ID and then parses the parameter values to be used as part of the script.

To utilize this bash function, you’ll need to complete the following:

  1. Copy & paste the checkCustomParamaters function to your desired script(s) and call the function.
  2. In your script where you normally use $4$9, instead assign the appropriate $pXValue where X is the parameter value.
  3. Assign your script to a policy and make it available on an Ongoing basis with a custom trigger.

Now you can call your policy with your selected custom variables! The added benefit of this method is that you have more than just the normal parameter 4 – 9 values available in Jamf scripts.

sudo jamf policy -event <custom trigger name> -p1 <p1 value> -p2 <p2 value> ...

Now, we can accomplish the task of remotely changing a Mac’s hostname:

sudo jamf policy -event sethostname -p1 "new_hostname"
Password:
Checking for policies triggered by "sethostname" for user "testuser"...
Executing Policy Change Mac Hostname
Running script SetHostname...
Script exit code: 0
Script result: P1 value is new_hostname
Successfully changed hostname to new_hostname!

Running Recon...

Keeping Your autopkg Cache So Fresh and So Clean, Clean

It’s been a minute since I felt I had something worthwhile to write about. In the past, the autopkg systems I had configured either had plenty of internal storage or external storage available to more or less “set it and forget it”. The consequence of this approach is over time if you have many software titles your autopkg cache folder will become quite large. Depending on the recipes you use, say you have many that involve naming downloaded or packaged software with the version, this can also greatly increase the size because new software version downloads and packages don’t overwrite the previous ones. So if you find yourself like I do where free space is precious on your autopkg system, you need an automated way to clean it.

While I haven’t yet taken the leap to implement autopkg in a CI workflow that makes this whole issue moot, I wrote a script to periodically clean out old downloads, packages, as well as the tmp files that can result from incomplete downloads.

As written, running this on the command line will remove any .dmg, .pkg, .tar.gz, and .zip files older than 7 days. It also removes those pesky tmp files that can get leftover from incomplete software downloads. This can also be triggered to be run as part of a LaunchDaemon.

If you wish, you can tweak the threshold for removing old files by entering your own day value for DAYS_TO_DELETE. Additionally, if you wish to exclude any items that would normally be deleted, uncomment the EXCLUSIONS variable and enter any desired search terms.

Happy cleaning!

#!/bin/bash
# Script to find and delete .dmg, .pkg, .tar.gz, and .zip files from
# your autopkg cache
# Failed / incomplete downloads (prefixed with tmp) are also removed
# Number of days after which to delete matching files
readonly DAYS_TO_DELETE=7
# Path to autopkg Cache folder for current user
readonly SEARCH_PATH="/Users/${USER}/Library/AutoPkg/Cache"
# List of search terms to exclude from cleanup, separated by spaces
#
# readonly EXCLUSIONS="Slack Firefox"
# Path to autopkg cleanup log. For CLI use, otherwise comment out
readonly LOG="/Library/Logs/autopkg_cleanup.log"
#######################################
# Output errors with timestamp to STDERR
# Globals:
# CLEANUP_LIST
# DAYS_TO_DELETE
# SEARCH_PATH
# EXCLUSIONS
# Arguments:
# None
# Outputs:
# None
#######################################
get_files_to_clean() {
CLEANUP_LIST="$(/usr/bin/find ${SEARCH_PATH} \( -iname "*.pkg" -o -iname "*.dmg" -o -iname "*.zip" -o -iname "tmp*" -o -iname "*.tar.gz" \) -mtime +${DAYS_TO_DELETE} -maxdepth 5 -print | sort)"
# Remove any excluded items from the list
for i in $EXCLUSIONS; do
CLEANUP_LIST=$(echo "$CLEANUP_LIST" | grep -v ${i})
done
}
#######################################
# Output results
# Globals:
# CLEANUP_LIST
# DAYS_TO_DELETE
# Arguments:
# None
# Outputs:
# Prints & writes to log if no files found older
# than specified DAYS_TO_DELETE, or prints & writes
# all found files to clean and indicates if it failed
#######################################
clean_files() {
# Output results
if [ -z "$CLEANUP_LIST" ]; then
# Indicate no files found to delete
writelog "No files found to clean older than ${DAYS_TO_DELETE} days."
else
# Indicate found files to delete
writelog "Files found to clean older than ${DAYS_TO_DELETE} days:"
for i in ${CLEANUP_LIST}; do
writelog "${i}"
done
# Remove found files and output any failures
for f in $CLEANUP_LIST; do
writelog "Removing $(basename ${f})"
/bin/rm -f "$f" || writelog "Failed to remove ${f}."
done
fi
}
start_run() {
writelog "####### START $(/bin/date +'%Y-%m-%d') #######"
}
end_run() {
writelog "####### END $(/bin/date +'%Y-%m-%d') #######"
}
writelog() {
# For LaunchDaemon use
#echo "$(/bin/date +'%Y-%m-%dT%H:%M:%S') – $*" #>&1
# For CLI use
echo "$(/bin/date +'%Y-%m-%dT%H:%M:%S')$*" >> "$LOG"
}
main() {
start_run
get_files_to_clean
clean_files
end_run
}
main "@"