Full step-by-step instructions • All tasks & solutions • Instructor-preferred methods
Key discussion points: Integration service account, Search base, Attribute mappings (and how to modify them), Target users, Extension attributes + Inventory collection.
Useful navigation: Settings → System → LDAP servers | Settings → Computer management → Inventory collection | Settings → Device management → Inventory collection
maildivisionFaculty.https://community.jamf.com/carLicense (used for the tech agreement).Key discussion points: Distribution points, Jamf Cloud Distribution Service, Sites (creation, access, assignment, objects), Network segments, Self Service for iOS and macOS, Jamf Pro health check page, using curl to transfer data.
Useful navigation: Settings → Server → Cloud distribution point / File share distribution points | Settings → Network → Sites / Network segments | Settings → Self Service
printNetworkSegment.zsh to obtain the correct IP range / values.Key discussion points: Jamf Pro API, API Authentication (API Roles & Clients, Bearer Token), Integrating external tools (Jamf Sync), using curl to obtain tokens and call the API.
Navigation: Settings → System → API Roles and Clients
zshShellScript.crLanguage.zip).baseURL="JAMF_PRO_URL"
client_id="CLIENT_ID"
client_secret="CLIENT_SECRET"
access_token=$(curl --silent --location \
--request POST "https://$baseURL/api/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=$client_id" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_secret=$client_secret" | plutil -extract "access_token" raw -)
# Quick test that the token works
curl --silent --header "Authorization: Bearer $access_token" \
https://$baseURL/api/v2/computers-inventory
Key discussion points: Device Enrollment & Supervision, User-Initiated Enrollment with LDAP, Enrollment Invitations / Enrollment Profiles, Local Administrator Password Solution (LAPS), Secure Token / Bootstrap Token, Login & Background Items (macOS 13+).
Useful navigation: Settings → Global → User-initiated enrollment | Settings → Computer Management → Security | Devices → Enrollment Profiles | Computers → Search Inventory → History → Managed Local Administrator Account History
jamfManage.kcox.https://your-jamf-pro-url/api/doc).PUT /v2/local-admin-password/settings.passwordRotationTime to 300 (seconds = 5 minutes).launchctl bootstrap / bootout commands. Do not use the older load / unload commands.Key discussion points: Login Events, Check-in, Daemons, Agents, the structure of a launchd plist, the defaults command, launchctl, the id utility.
Important paths:
~/Library/LaunchAgents//Library/LaunchAgents//Library/LaunchDaemons/Create an agent whose label is com.Lesson5Task1.agent and that opens https://learn.jamf.com when it loads.
#!/bin/zsh --no-rcs
label="com.Lesson5Task1.agent"
plistPath="$HOME/Library/LaunchAgents/$label.plist"
# Clean up any previous version
launchctl bootout gui/$(id -u)/$label 2>/dev/null || true
rm -f "$plistPath"
# Build the plist
defaults write "$plistPath" Label -string "$label"
defaults write "$plistPath" ProgramArguments -array \
-string "/usr/bin/open" \
-string "https://learn.jamf.com"
defaults write "$plistPath" RunAtLoad -boolean TRUE
chmod 644 "$plistPath"
# Load it into the current user's GUI session
launchctl bootstrap gui/$(id -u) "$plistPath"
The script must run jamf manage and append the current UTC date/time to /Users/Shared/managementTime.txt.
#!/bin/zsh --no-rcs
/usr/local/jamf/bin/jamf manage
date -u +%F\ %T >> /Users/Shared/managementTime.txt
Recommended way to place it on disk (adjust the path if your instructor prefers the Desktop):
sudo tee /usr/local/bin/managementUpdate.sh << 'EOF'
#!/bin/zsh --no-rcs
/usr/local/jamf/bin/jamf manage
date -u +%F\ %T >> /Users/Shared/managementTime.txt
EOF
sudo chmod +x /usr/local/bin/managementUpdate.sh
Create a daemon with label com.Lesson5Task3.daemon that executes the script from Task 2 every 1200 seconds.
#!/bin/zsh --no-rcs
label="com.Lesson5Task3.daemon"
plistPath="/Library/LaunchDaemons/$label.plist"
scriptPath="/usr/local/bin/managementUpdate.sh"
sudo rm -f "$plistPath"
sudo launchctl bootout system/$label 2>/dev/null || true
sudo defaults write "$plistPath" Label -string "$label"
sudo defaults write "$plistPath" ProgramArguments -array \
-string "/bin/zsh" \
-string "$scriptPath"
sudo defaults write "$plistPath" RunAtLoad -boolean TRUE
sudo defaults write "$plistPath" StartInterval -integer 1200
sudo chown root:wheel "$plistPath"
sudo chmod 644 "$plistPath"
sudo launchctl bootstrap system "$plistPath"
/Users/Shared/managementTime.txt.# Useful verification commands
launchctl list | grep Lesson5Task1
sudo launchctl list | grep Lesson5Task3
cat /Users/Shared/managementTime.txt
Key discussion points: Mobile device configuration profiles (Notifications payload), Computer configuration profiles (Application & Custom Settings, using defaults and plutil, Custom Schemas created with the Managed App Schema Builder), User-level profiles, Payload variables.
com.jamfsoftware.selfserviceStep A – Create the property list on your Mac:
defaults write ~/Desktop/com.apple.Safari ShowFullURLInSmartSearchField -boolean true
This creates a file named com.apple.Safari.plist on your Desktop.
Step B – Build the configuration profile:
com.apple.Safaricom.apple.Safari.plist file that is sitting on your Desktop.com.apple.dock.json.zip if the instructor supplies it).com.apple.dockDock Orientation| Field | Value |
|---|---|
| Key Name | orientation |
| Key Friendly Name | Dock orientation |
| Select Key Type | string (from list) |
| List of options (human-readable) | Screen Left, Screen Bottom, Screen Right |
| Associated list of values | left, bottom, right |
com.apple.dockKey discussion points: Managed Login Items payload, the different Rule Types (Bundle identifier, Bundle identifier prefix, Label, Label prefix, Team identifier), how to obtain Team IDs and Labels.
Navigation: Computers → Configuration Profiles → Managed Login Items
First discover the Team ID on a Mac that has Chrome installed:
codesign --display --verbose /Applications/Google\ Chrome.app 2>&1 | grep TeamIdentifier
# You should see: TeamIdentifier=EQHXZ8M8AV
EQHXZ8M8AV.com.obsproject.obs-studio.mac-camera-extension.systemextension.com.Lesson5Task1.agent.com.Lesson5Task3.daemon.Key discussion points: System Extension Types (Allowed System Extensions, Non-removable system extensions, Non-removable from UI), Team ID + Bundle ID pairing, the systemextensionsctl command, the database at /Library/SystemExtensions/db.plist.
# Information about the main OBS application
codesign -dv /Applications/OBS.app
# Information about the camera system extension (this is the one you need)
codesign -dv /Applications/OBS.app/Contents/Library/SystemExtensions/com.obsproject.obs-studio.mac-camera-extension.systemextension
Typical values seen in class:
2MMRE5MTB8com.obsproject.obs-studio.mac-camera-extension2MMRE5MTB8.com.obsproject.obs-studio.mac-camera-extension.defaults read /Library/SystemExtensions/db.plist
# Alternative readable form
plutil -p /Library/SystemExtensions/db.plist
# Also useful
systemextensionsctl list
Key discussion points: Transparency, Consent and Control (TCC), user vs MDM approval, the various services (Camera, Microphone, Accessibility, ScreenCapture, AppleEvents, etc.), Identifier (Bundle ID or Path), Code Requirement / code signature, the difference between a PPPC payload and the PPPC Utility app, pre-approving the Jamf management framework and AppleEvents, the tccutil command.
# Create a new folder on the Desktop via AppleScript
osascript -e 'tell application "Finder" to make new folder at desktop'
# After the dialog appears, open System Settings → Privacy & Security → Automation
# and confirm that Terminal has permission for Finder.
# Reset all privacy decisions for the Terminal app
tccutil reset All com.apple.Terminal
com.obsproject.obs-studioidentifier "com.obsproject.obs-studio" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "2MMRE5MTB8"
JamfAppleEvents.mobileconfig from the lesson Files section.com.google.Chrome(identifier "com.google.Chrome" or identifier "com.google.Chrome.beta" or identifier "com.google.Chrome.dev" or identifier "com.google.Chrome.canary") and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = EQHXZ8M8AV
com.apple.mobileslideshow (Photos)com.apple.MobileSMS (Messages)com.apple.facetime (FaceTime)FaceTime.Please use approved conferencing software.#!/bin/zsh --no-rcs
if [ -f "/Users/Shared/.BetaEnrollmentData.plist" ]; then
echo "<result>Enrolled</result>"
else
echo "<result>Not Enrolled</result>"
fi
Key discussion points: Security overview, fdesetup, enabling FileVault with a configuration profile, Enabled User / Secure Token / Bootstrap Token, Personal Recovery Key escrow, audit logs.
Who_Has_SecureToken.zsh file supplied in the lesson Files section is optional exploration material; it is not required for the three scored tasks.Jamf Pro$SERIALNUMBERsudo jamf recon (or wait for the next check-in).Key discussion points: Variables, command substitution, positional parameters, if statements, here documents (often written with tee), creating files with touch, reading files, redirects, cat.
~/Desktop instead of /Library/Scripts. Both locations work for testing; follow whatever path the instructor requests on exam day.#!/bin/zsh --no-rcs
echo "Please enter a Policy ID: "
read policyid
sudo jamf policy -id "$policyid" > /Users/Shared/policy"$policyid".txt
#!/bin/zsh --no-rcs
sudo tee /Library/Scripts/Lesson12.zsh << "EOF"
#!/bin/zsh --no-rcs
echo "Please enter a Policy ID: "
read policyid
sudo jamf policy -id "$policyid" > /Users/Shared/policy"$policyid".txt
EOF
sudo chmod +x /Library/Scripts/Lesson12.zsh
#!/bin/zsh --no-rcs
echo "Google Chrome last used:"
mdls "/Applications/Google Chrome.app" -name kMDItemLastUsedDate
echo ""
echo "Firefox last used:"
mdls "/Applications/Firefox.app" -name kMDItemLastUsedDate
#!/bin/zsh --no-rcs
if [[ -f /Library/Scripts/Lesson12.zsh ]]; then
echo "File exists"
else
echo "File does not exist"
fi
#!/bin/zsh --no-rcs
sudo tee /Library/Scripts/Lesson12Task5.zsh << "EOF"
#!/bin/zsh --no-rcs
if [[ -f /Library/Scripts/Lesson12.zsh ]]; then
echo "File exists"
else
echo "File does not exist"
fi
EOF
sudo chmod +x /Library/Scripts/Lesson12Task5.zsh
/Library/Scripts/Lesson12Task5.zsh
Key discussion points: jamfHelper window types, buttons and return codes, executing AppleScript with osascript, capturing the text a user types into a dialog.
#!/bin/zsh --no-rcs
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"
"$jamfHelper" -windowType hud -description "Hello world"
#!/bin/zsh --no-rcs
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"
returnCode=$("$jamfHelper" -windowType utility \
-description "Would you like to perform an inventory update?" \
-button1 "Yes" \
-button2 "No")
if [[ $returnCode == 0 ]]; then
# User clicked the Yes button
sudo jamf recon
else
# User clicked the No button
"$jamfHelper" -windowType utility \
-description "Inventory will not be updated." \
-button1 "OK"
fi
#!/bin/zsh --no-rcs
osascript -e 'display dialog "Hello world" buttons {"OK"} default button 1'
If you encounter quoting problems, use a here-document instead:
#!/bin/zsh --no-rcs
osascript <
#!/bin/zsh --no-rcs
assetTag=$(osascript -e 'text returned of (display dialog "Please enter your asset tag" default answer "" buttons {"OK"} default button 1)')
sudo jamf recon -assetTag "$assetTag"
Key discussion points: for / while / until loops, the test / [[ ]] expression, policy script parameters ($4, $5, $6, \ldots).
#!/bin/zsh --no-rcs
until [[ -f /Users/Shared/file.txt ]]; do
echo "File not found"
sleep 1
done
echo "File found!"
#!/bin/zsh --no-rcs
number=0
while [[ $number -ne 42 ]]; do
number=$(osascript -e 'text returned of (display dialog "Guess the magic number:" default answer "" buttons {"OK"} default button 1)')
done
echo "Correct! You guessed 42."
Parameter 4 = path of the file to test
Parameter 5 = message that jamfHelper should display if the file exists.
#!/bin/zsh --no-rcs
fileToCheck="$4"
messageToDisplay="$5"
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"
if [[ -e "$fileToCheck" ]]; then
"$jamfHelper" -windowType utility -description "$messageToDisplay" -button1 "OK"
fi
/Users/Shared/managementTime.txtYour management framework was updated recentlyKey discussion points: The mandatory <result>...</result> tags, using awk, grep, tr and date inside extension-attribute scripts, choosing the correct Data Type (String, Date, Integer, \ldots).
Who_Has_SecureToken.zsh script that was supplied in the FileVault lesson Files section.echo "<result>$someVariable</result>"#!/bin/zsh --no-rcs
chromeDate=$(mdls -name kMDItemLastUsedDate "/Applications/Google Chrome.app" 2>/dev/null | awk '{print $3, $4}')
firefoxDate=$(mdls -name kMDItemLastUsedDate "/Applications/Firefox.app" 2>/dev/null | awk '{print $3, $4}')
if [[ -n $chromeDate && $chromeDate != "(null)" ]]; then
echo "<result>$chromeDate</result>"
elif [[ -n $firefoxDate && $firefoxDate != "(null)" ]]; then
echo "<result>$firefoxDate</result>"
else
echo "<result></result>"
fi
#!/bin/zsh --no-rcs
bootTime=$(sysctl kern.boottime | awk '{print $5}' | tr -d ,)
bootTimeFormatted=$(date -juf %s "$bootTime" +"%Y-%m-%d %H:%M:%S")
echo "<result>$bootTimeFormatted</result>"
Key discussion points: XML key/value pairs and the three value types (Integer, String, Boolean), Jamf variables such as $EMAIL, deploying managed apps, converting unmanaged apps to managed, preventing backup of app data, Per-App Networking.
Navigation: Devices → Mobile Device Apps → (select an app) → App Configuration / Managed App Configuration tab
<dict>
<key>NewTabPageLocation</key>
<string>https://www.jamf.com</string>
<key>EdgeDisabledFeatures</key>
<string>password</string>
</dict>
NewTabPageLocation forces the managed homepage to https://www.jamf.com.EdgeDisabledFeatures set to password disables the iOS prompt that asks to save passwords.<dict>
<key>atlassian.devicePolicyLoginAccount</key>
<string>$EMAIL</string>
</dict>
atlassian.devicePolicyLoginAccount restricts login to Atlassian mobile apps so that only the email address Jamf knows for the device/user ($EMAIL) is accepted.Key discussion points: Composer sources, converting a finished PKG back into a source, preinstall and postinstall scripts, setting correct ownership and permissions, building the final .pkg.
Navigation: /Applications/Composer on the Mac | Settings → Packages inside Jamf Pro
On the Desktop create a folder named New Packages. All packages built in this lesson will be saved there.
#!/bin/zsh --no-rcs
defaults write /Library/Preferences/com.sequelpro.SequelPro.plist SUEnableAutomaticChecks -boolean true
~/Desktop/New Packages/.The package must contain:
preinstall script:
#!/bin/zsh --no-rcs
# Remove any previous version of the daemon from launchd
launchctl bootout system /Library/LaunchDaemons/com.Lesson5Task3.daemon.plist 2>/dev/null || true
postinstall script:
#!/bin/zsh --no-rcs
# Correct ownership and mode
chown root:wheel /Library/LaunchDaemons/com.Lesson5Task3.daemon.plist
chmod 644 /Library/LaunchDaemons/com.Lesson5Task3.daemon.plist
chmod +x /usr/local/bin/managementUpdate.sh # adjust the path if you stored the script elsewhere
# Load the daemon
launchctl bootstrap system /Library/LaunchDaemons/com.Lesson5Task3.daemon.plist
~/Desktop/New Packages/./Applications/.~/Desktop/New Packages/.Key discussion points: Server-side versus client-side limitations, caching packages (and the “Collect package receipts” inventory setting), user interaction / Self Service+, the classic notifyUser.sh pattern, retry policies.
First create (or upload) the helper script in Settings → Computer Management → Scripts:
#!/bin/zsh --no-rcs
messageToDisplay="$4"
policyID="$5"
policyAction="$6"
jamfHelper="/Library/Application Support/JAMF/bin/jamfHelper.app/Contents/MacOS/jamfHelper"
buttonClicked=$("$jamfHelper" -windowType utility -description "$messageToDisplay" -button1 "OK" -button2 "Cancel")
if [[ "$buttonClicked" = "0" ]]; then
su "$3" -c "open 'jamfselfservice://content?entity=policy&id=$policyID&action=$policyAction'"
fi
notifyUser.sh and fill the parameters:
The Sequel Pro app is now available in the Self Service + appviewsudo jamf policy -id <PolicyID_from_Task_4>