Send Remote Command to Set the Recovery Lock for Apple Silicon Macs

Steven_Xu
Contributor

If the Apple Silicon Mac computers can not be enrolled through PreStage Enrollment, the Recovery Lock can only be set from remote command, here's how to do it in Jamf API.

1. First, we need to know the managementId of the target computer. we can copy it from computer‘s inventory. (Jamf Pro Server 10.50.0 and above)

截屏2023-09-18 上午9.23.02.png

2. Open the Jamf Pro Server API page (https://yourInstance.jamfcloud.com/api, or https://yourjss.domain.com:8443/api) , and choose Jamf Pro API PRODUCTION  View

Screenshot 2022-12-01 at 11.14.54 AM.png

3. Navigate to "MDM", and choose "POST /v2/mdm/commands", then click "Try it now".

截屏2024-04-29 16.12.06.png

4. Modify the request json body. Input the managementId and the recovery lock password. Set to empty ("") will remove the recovery lock password, set to a different one from previous can RESET the existed recovery lock passcode directly. (PS: Firmware/EFI passcode cannot be reset, must be removed and then set a new passcode.)

截屏2023-09-18 上午9.38.27.png

(copy paste and modify it)

 

{
  "clientData": [
    {
      "managementId": "6c1c6fdd-999d-4801-ab61-8b1785cfa129",
      "clientType": "COMPUTER"
    }
  ],
  "commandData": {
    "commandType": "SET_RECOVERY_LOCK",
    "newPassword": "12345678"
  }
}

 

4. Click Execute. 

5. The SetRecoveryLock command status can be found in computer's Management History.

截屏2023-09-18 上午9.54.55.png

That's all steps to set recovery lock passcode for one Apple Silicon computers in Jamf Pro API page. We can also set it for all Apple Silicon computers from a script.

 

 

 

16 REPLIES 16

mschlosser
Contributor II

thanks for the tip that was decent. i recently had to tackle this issue for a dozen or so laptops that were enrolled prior to 11.5 of macOS; i ended up using the following script, it worked well enough after the dependencies wqere installed on the mac.

 

#!/usr/bin/env python3

####
#
# SetRecoveryLockJAMF.py
#
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
# Python3
# Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####

### User-edited Variables ###
# Define how we connect to JAMF
jamf_url = 'jamf.myorg.edu:8443'
jamf_api_username = 'jamf_api'
jamf_api_password = 'ApiPasswordGoesHere'

########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys

# Initialize command line argument parser
parser = argparse.ArgumentParser()

# Adding command line arguments
parser.add_argument(
"SearchString",
help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"-p", "--Passcode",
help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
"-r", "--RandomPasscode", nargs='?', const=20,
help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)


# Read arguments from command line
args = parser.parse_args()

### From git project auth/basic_auth.py
import base64
def auth_token():
# create base64 encoded string of jamf API user credetinals
credentials_str = f'{jamf_api_username}:{jamf_api_password}'
data_bytes = credentials_str.encode("utf-8")
encoded_bytes = base64.b64encode(data_bytes)
encoded_str = encoded_bytes.decode("utf-8")

return encoded_str


### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))

# path of token file in current working directory
token_file = f'{cwd}/token.txt'

def request_token():
"""Generate an auth token from API"""

headers = {
'Accept': 'application/json',
'Authorization': f'Basic {auth_token()}',
'Content-Type': 'application/json',
}

try:
response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
response.raise_for_status()

except requests.exceptions.HTTPError as err:
raise SystemExit(err)

return response.json()['token']

def get_token():
"""Returns a token from local cache or API request"""
current_time = int(time.time())

# check if token is cached and if it is less than 30 minutes old
if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
# return a cached token from file
return read_token_from_local()
else:
# return a token from API
return get_token_from_api()

def get_token_from_api():
"""Returns a token from an API request"""
token = request_token()
cache_token(token)
return token

def cache_token(token):
"""
Cache token to local file
Parameters:
token - str
"""
with open(token_file, 'w') as file_obj:
file_obj.write(token)

def read_token_from_local():
"""Read cached token from local file"""
with open(token_file, 'r') as file_obj:
token = file_obj.read().strip()
return token


### From git project computers.py
import requests
import math

headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}

def get_computer_count():
"""
Returns the number of computers in Jamf Pro
"""

try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
headers=headers
)
response.raise_for_status()

except requests.exceptions.HTTPError as err:
raise SystemExit(err)

count = response.json()['totalCount']
#print("Number of computers in JAMF = " + str(count))

return count

computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)

def get_arm64(filter = None):
"""
Returns Jamf IDs of all arm64 type computers

Parameters:
filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
Computer name in filter is not case sensitive
"""

computers_id = []

for pageIndex in range(number_of_pages):

try:
response = requests.get(
url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
headers=headers
)
response.raise_for_status()

except requests.exceptions.HTTPError as err:
raise SystemExit(err)

computers = response.json()['results']

for computer in computers:
if computer['hardware']['processorArchitecture'] == 'arm64':
computers_id.append(computer['id'])

if computers_id == []:
sys.exit("No Apple Silicon computers found in Jamf that match search string.")

return computers_id


def get_mgmt_id(computers_id):
"""
Returns Jamf computers management id

Parameters:
computers_id - (e.g. ['10', '12']]). List of Jamf computers id
"""
computers_mgmt_id = []

for pageIndex in range(number_of_pages):
try:
response = requests.get(
url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
headers=headers
)
response.raise_for_status()

except requests.exceptions.HTTPError as err:
raise SystemExit(err)

computers = response.json()['results']

for computer_id in computers_id:
for computer in computers:
# Find computers that given computer id in list of computers
if computer['id'] == computer_id:
computer_mgmt_id = computer['managementId']
computer_name = computer['name']
# Add computer to list
computers_mgmt_id.append({
'id': computer_id,
'name': computer_name,
'mgmt_id': computer_mgmt_id
})
break

return computers_mgmt_id

 

### From git project recovery_lock.py
import requests

headers = {
'Accept': 'application/json',
'Authorization': f'Bearer {get_token()}',
'Content-Type': 'application/json',
}

def set_key(computer_name, management_id, recovery_lock_key):
"""Sets a Recovery Lock key for a given computer"""

print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')

payload = {
'clientData': [
{
'managementId': f'{management_id}',
'clientType': 'COMPUTER'
}
],
'commandData': {
'commandType': 'SET_RECOVERY_LOCK',
'newPassword': f'{recovery_lock_key}'
}
}

try:
response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)

response.raise_for_status()

except requests.exceptions.HTTPError as err:
raise SystemExit(err)

 

### From git project main.py
from random import randint

computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)


print("These are the changes you will be making:")

for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
print(" "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
elif args.RandomPasscode:
print(" "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
else:
print(" "+computer_name+" will have its Recovery Lock cleared")

print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")

if go_ahead == ("y" or "Y"):
for computer in computers_mgmt_id:
computer_name = computer['name']
computer_mgmt_id = computer['mgmt_id']
if args.Passcode:
recovery_lock_key = args.Passcode
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
elif args.RandomPasscode:
rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
rand_val_high=pow(10,int(args.RandomPasscode)) - 1
recovery_lock_key = randint(rand_low_val,rand_val_high)
print(" Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
else:
recovery_lock_key = ''
print(" Command sent to clear Recovery Lock on "+computer_name)


set_key(computer_name, computer_mgmt_id, recovery_lock_key)
print("")

Cool, great to see that there is a batch operation script.

Am I correct in that if this is in a policy scoped to devices without a recovery password set, it will assign a random key to each individually?  Or is it designed to be run outside of Jamf and will pick out those devices needing a password and assign?  Sorry for the noob questions - this looks really useful but I'm barely literate in python at this point.  Was also wondering if you could repost with proper code formatting - was running it through an interpreter and it appears to be missing all its necessary indents. thanks much

Sure. As best I can recall in order to use the script; python3 and the associated dependencies mentioned in the script must be installed on whatever mac you are using ot administer your jamf instance. It can be on any network as long as it has an internet connection (assuming the instance is jamf cloud based), as it just needs to auth to your jamf instance and use some API calls.

After the dependencies have been installed and the script customized with your FQDN and API user; you can execute the script from the terminal as follows:

python3 SetRecoveryLockJAMF.py Tad -r

The above command would loop through your Macs and set a random recovery password, for the first computer it found with Tad in the name. Repeat the process as necessary, replacing the name to hit all the macs you need. Hope that makes sense. Here is the script in a code block:

 

#!/usr/bin/env python3

####
#
# SetRecoveryLockJAMF.py
#
#
# Script to set recovery lock for macOS computers in JAMF Pro
# Requires:
#   Python3
#   Python module: requests (can be installed by running 'python3 -m pip install requests')
#
# Adapted from https://github.com/shbedev/jamf-recovery-lock
#
####

### User-edited Variables ###
# Define how we connect to JAMF
jamf_url            = ''
jamf_api_username   = ''
jamf_api_password   = '' 

########## DO NOT EDIT BELOW THIS LINE ##########
import argparse, sys

# Initialize command line argument parser
parser = argparse.ArgumentParser()
 
# Adding command line arguments
parser.add_argument(
    "SearchString",
    help = "String to use to search JAMF computer names"
)
group = parser.add_mutually_exclusive_group()
group.add_argument(
    "-p", "--Passcode", 
    help = "Specify Recovery Lock passcode (default is blank)"
)
group.add_argument(
    "-r", "--RandomPasscode", nargs='?', const=20,
    help = "Generate a different random Recovery Lock passcode for each computer (default length is 20, specify a value for a different length)"
)

 
# Read arguments from command line
args = parser.parse_args()
 

### From git project auth/basic_auth.py
import base64
def auth_token():
    # create base64 encoded string of jamf API user credetinals
    credentials_str = f'{jamf_api_username}:{jamf_api_password}'
    data_bytes = credentials_str.encode("utf-8")
    encoded_bytes = base64.b64encode(data_bytes)
    encoded_str = encoded_bytes.decode("utf-8")

    return encoded_str


### From git project auth/bearer_auth.py
import os, time, requests
# current working directory
cwd = os.path.dirname(os.path.realpath(__file__))

# path of token file in current working directory
token_file = f'{cwd}/token.txt'

def request_token():
    """Generate an auth token from API"""
    
    headers = {
        'Accept': 'application/json',
        'Authorization': f'Basic {auth_token()}',
        'Content-Type': 'application/json',
    } 

    try:
        response = requests.request("POST", f'https://{jamf_url}/api/v1/auth/token', headers=headers)
        response.raise_for_status()

    except requests.exceptions.HTTPError as err:
        raise SystemExit(err)

    return response.json()['token']

def get_token():
    """Returns a token from local cache or API request"""
    current_time = int(time.time())

    # check if token is cached and if it is less than 30 minutes old
    if os.path.exists(token_file) and ((current_time - 1800) < os.stat(token_file)[-1]):
        # return a cached token from file
        return read_token_from_local()
    else:
        # return a token from API
        return get_token_from_api()

def get_token_from_api():
    """Returns a token from an API request"""
    token = request_token()
    cache_token(token)
    return token

def cache_token(token):
    """
    Cache token to local file
    Parameters:
        token - str
    """
    with open(token_file, 'w') as file_obj:
        file_obj.write(token)

def read_token_from_local():
    """Read cached token from local file"""
    with open(token_file, 'r') as file_obj:
        token = file_obj.read().strip()
    return token


### From git project computers.py
import requests
import math

headers = {
    'Accept': 'application/json',
    'Authorization': f'Bearer {get_token()}',
    'Content-Type': 'application/json',
}

def get_computer_count():
    """
    Returns the number of computers in Jamf Pro
    """

    try:
        response = requests.get(
            url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page=0&page-size=1&sort=id%3Aasc',
            headers=headers
        )
        response.raise_for_status()
    
    except requests.exceptions.HTTPError as err:
        raise SystemExit(err)

    count = response.json()['totalCount']
    #print("Number of computers in JAMF = " + str(count))

    return count

computers_per_page = 1000
number_of_pages = math.ceil(get_computer_count() / computers_per_page)

def get_arm64(filter = None):
    """
    Returns Jamf IDs of all arm64 type computers
    
    Parameters:
        filter - (e.g. 'filter=general.name=="jdoe-mbp"'). If empty, returns all computers.
        Computer name in filter is not case sensitive 
    """

    computers_id = []

    for pageIndex in range(number_of_pages):

        try:
            response = requests.get(
                url=f'https://{jamf_url}/api/v1/computers-inventory?section=HARDWARE&page={pageIndex}&page-size={computers_per_page}&sort=id%3Aasc&{filter}',
                headers=headers
            )
            response.raise_for_status()
        
        except requests.exceptions.HTTPError as err:
            raise SystemExit(err)

        computers = response.json()['results']

        for computer in computers:
            if computer['hardware']['processorArchitecture'] == 'arm64':
                computers_id.append(computer['id'])

    if computers_id == []:
        sys.exit("No Apple Silicon computers found in Jamf that match search string.")  

    return computers_id
    


def get_mgmt_id(computers_id):
    """
    Returns Jamf computers management id
    
    Parameters:
        computers_id - (e.g. ['10', '12']]). List of Jamf computers id 
    """
    computers_mgmt_id = []

    for pageIndex in range(number_of_pages):
        try:
            response = requests.get(
                url = f'https://{jamf_url}/api/preview/computers?page={pageIndex}&page-size={computers_per_page}&sort=name%3Aasc',
                headers=headers
            )
            response.raise_for_status()
            
        except requests.exceptions.HTTPError as err:
            raise SystemExit(err)

        computers = response.json()['results']

        for computer_id in computers_id:
            for computer in computers:
                # Find computers that given computer id in list of computers
                if computer['id'] == computer_id:
                    computer_mgmt_id = computer['managementId']
                    computer_name = computer['name']
                    # Add computer to list
                    computers_mgmt_id.append({
                        'id': computer_id,
                        'name': computer_name,
                        'mgmt_id': computer_mgmt_id
                    })
                    break

    return computers_mgmt_id



### From git project recovery_lock.py
import requests

headers = {
    'Accept': 'application/json',
    'Authorization': f'Bearer {get_token()}',
    'Content-Type': 'application/json',
}

def set_key(computer_name, management_id, recovery_lock_key):
    """Sets a Recovery Lock key for a given computer"""
    
    print(f'Settings recovery lock key: {recovery_lock_key} for {computer_name}')

    payload = {
        'clientData': [
            {
            'managementId': f'{management_id}',
            'clientType': 'COMPUTER'
            }
        ],
        'commandData': {
            'commandType': 'SET_RECOVERY_LOCK',
            'newPassword': f'{recovery_lock_key}'
        }
    }

    try:
        response = requests.request("POST", f'https://{jamf_url}/api/preview/mdm/commands', headers=headers, json=payload)

        response.raise_for_status()
        
    except requests.exceptions.HTTPError as err:
        raise SystemExit(err)



### From git project main.py
from random import randint

computers_id = get_arm64('filter=general.name=="*'+args.SearchString+'*"')
computers_mgmt_id = get_mgmt_id(computers_id)


print("These are the changes you will be making:")

for computer in computers_mgmt_id:  
    computer_name = computer['name']
    computer_mgmt_id = computer['mgmt_id']
    if args.Passcode:
        print("   "+computer_name+" will have its Recovery Lock set to "+args.Passcode)
    elif args.RandomPasscode:
        print("   "+computer_name+" will have its Recovery Lock set to a random "+str(args.RandomPasscode)+"-digit number")
    else:
        print("   "+computer_name+" will have its Recovery Lock cleared")
    

print("")
go_ahead = input("Do you wish to proceed? (y/n)")
print("")

if go_ahead == ("y" or "Y"):
    for computer in computers_mgmt_id:
        computer_name = computer['name']
        computer_mgmt_id = computer['mgmt_id']
        if args.Passcode:
            recovery_lock_key = args.Passcode
            print("   Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
        elif args.RandomPasscode:
            rand_low_val=pow(10,(int(args.RandomPasscode) - 1))
            rand_val_high=pow(10,int(args.RandomPasscode)) - 1
            recovery_lock_key = randint(rand_low_val,rand_val_high)
            print("   Command sent for "+computer_name+" Recovery Lock to be set to "+str(recovery_lock_key))
        else:
            recovery_lock_key = ''
            print("      Command sent to clear Recovery Lock on "+computer_name)

    
        set_key(computer_name, computer_mgmt_id, recovery_lock_key)
    print("")

Fantastic - thanks much!  I did manage to fix most of the formatting issues but was still having a little trouble - found I also had to update the fprime-tools ( python3 -m pip install fprime-tools ) and was getting auth errors - looks like the auth issue was because I had the search term last instead of first...
If I can eventually grok enough python, the only change I might make is to make it operate on a list of serials but still this is a really wonderful adaptation of shbedev's script bundle.

Kudos!

A bash script share to you, you can edit and run it in Terminal on your Mac computer.

#!/bin/bash
# created by Steven Xu (xuhao@solutionkeys.com)
# https://developer.jamf.com/jamf-pro/docs/jamf-pro-api-overview

# your jss url
jss_url="https://xxx.jamfcloud.com"
# Computer Group ID of Apple Silicon Macs which need to set the new Recovery Lock passcode.
cgroup_id="121"
# Recovery Lock passcode, set the value to empty("") means remove the passcode.
passcode="123456"

###############################   don't edit    #######################
read -p "Your jamf pro ($jss_url) account username: " api_user
stty -echo
read -p "Your password: " api_pass
stty -echo
echo

authToken=$(curl -s -u "${api_user}:${api_pass}" "$jss_url/api/v1/auth/token" -X POST -H "accept: application/json" )
bearerToken=$(echo "$authToken" | plutil -extract token raw -)

# Get computer IDs from the group. 
ids=$(curl -ksu "$api_user:$api_pass" -X GET "$jss_url/JSSResource/computergroups/id/$cgroup_id" -H "accept: application/xml" \
| xmllint --format --xpath /computer_group/computers/computer/id - | sed 's/<id>//g' | sed 's/<\/id>/,/g' | rev | sed 's/,//' | rev)

if [ -z "$ids" ]; then
    echo "no devices in this group"
    exit 0
	else 
	echo "Computer IDs in this group: $ids"
fi

# Send command to set recovery lock password
for id in $ids 
do 
	echo "Sending command to computer $id to set the recovery lock passcode..."
    management_id=$(curl -s -H "Authorization: Bearer ${bearerToken}" "$jss_url/api/v1/computers-inventory/$id?section=GENERAL" -X GET -H "Content-type: application/json" | grep managementId | awk -F '"' '{printf $4}')
    
    curl --location --request POST "${jss_url}/api/preview/mdm/commands" \
    --header "Authorization: Bearer ${bearerToken}" \
    --header 'Content-Type: application/json' \
    --data-raw '{
    "clientData": [
        {
            "managementId": "'$management_id'",
            "clientType": "COMPUTER"
        }
    ],
    "commandData": {
        "commandType": "SET_RECOVERY_LOCK",
        "newPassword": "'$passcode'"
        }
    }'
	echo "\n"
done

# Invalidate Bearer Token
echo "Invalidate the Token"
curl -s -H "Authorization: Bearer ${bearerToken}"  "$jss_url/api/v1/auth/invalidate-token" -X POST -w "%{http_code}" -o /dev/null
echo "Done, Recovery Lock commands sent."

 

 

Thanks @Steven_Xu - yes, that was going to be my alternate approach - convert to a shell script.  I'd probably make it a deployable script that would run automatically when a device is part of the no_recovery_lock group.

@Steven_Xu  Getting an error on below lines 

"ids=$(curl -ksu "$api_user:$api_pass" -X GET "$jss_url/JSSResource/computergroups/id/$cgroup_id" -H "accept: application/xml" \ | xmllint --format --xpath /computer_group/computers/computer/id - | sed 's/<id>//g' | sed 's/<\/id>/,/g' | rev | sed 's/,//' | rev) "

you may need to enable "Allow Basic authentication for the Classic API" in Jamf Pro password policy settings. BUT the basic authentication will be deprecated soon, bearer token authentication is recommend.

mike_prather
New Contributor II

Finally got a chance to revisit this and wanted to share: I came up with a group/policy workflow that runs automatically if Macs somehow manage to go through PreStage enrollment without Recovery Lock being enabled.
First step is the script (full credit to DerFlounder for his API token sequence):

#! /usr/bin/env zsh

# Set default exit code
exitCode=0

# Explicitly set initial value for the api_token variable to null:
api_token=""

# Explicitly set initial value for the token_expiration variable to null:
token_expiration=""

## generate random 39-digit numerical passcode
recPasscode=$(jot -r -s '' 39 0 9)

## Get the Mac's UUID string
UUID=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}')

# Set basic api access info
jamfpro_url="https://myjssserver.jamfcloud.com"

# Use the encoded credentials with Basic Authorization to request a bearer token
authToken=$(/usr/bin/curl "${jamfpro_url}/api/v1/auth/token" --silent --request POST --header "accept: application/json" --header "Authorization: Basic $4")

# Parse the returned output for the bearer token and store the bearer token as a variable.
if [[ $(/usr/bin/sw_vers -productVersion | awk -F . '{print $1}') -lt 12 ]]; then
   api_token=$(/usr/bin/awk -F \" 'NR==2{print $4}' <<< "$authToken" | /usr/bin/xargs)
else
   api_token=$(/usr/bin/plutil -extract token raw -o - - <<< "$authToken")
fi

# Verify that API authentication is using a valid token by running an API command
# which displays the authorization details associated with the current API user. 
# The API call will only return the HTTP status code.
api_authentication_check=$(/usr/bin/curl --write-out %{http_code} --silent --output /dev/null "${jamfpro_url}/api/v1/auth" --request GET --header "Authorization: Bearer ${api_token}")

## Pull JSSID from "general" subsection
JSS_ID=$(curl -H "Accept: text/xml" -H "Authorization: Bearer ${api_token}" -sf "${jamfpro_url}/JSSResource/computers/udid/${UUID}/subset/general" | xmllint --xpath '/computer/general/id/text()' -)

## get mgmtID
mgmtID=$(curl -s -X GET "${jamfpro_url}/api/v1/computers-inventory-detail/${JSS_ID}" -H "accept: application/json" -H "Authorization: Bearer ${api_token}" | grep -i "managementid" | awk -F '"' '{print $4}')

#checkpoints for troubleshooting
#echo $api_token
#echo $api_authentication_check
#echo $JSS_ID
#echo $mgmtID

## set Recovery Lock password
curl --location --request POST "${jamfpro_url}/api/preview/mdm/commands" \
--header "Authorization: Bearer ${api_token}" \
--header 'Content-Type: application/json' \
--data-raw '{
"clientData": [
    {
        "managementId": "'$mgmtID'",
        "clientType": "COMPUTER"
    }
],
"commandData": {
    "commandType": "SET_RECOVERY_LOCK",
    "newPassword": "'$recPasscode'"
    }
}'

## tag Mac as Recovery Lock set by API
if [[ ! -f "/usr/local/mylocalorgdir/RLapiSet" ]] && [[ $? == "0" ]]; then
    touch /usr/local/mylocalorgdir/RLapiSet
fi

Note that per the bearer token request, I'm using script parameters to pass along the encoded API creds.

2nd step is to create an extension attribute for the last section of the script so that computers can be detected in a smart group as having had the script's policy run:

#! /usr/bin/env zsh

if [[ -f "/usr/local/mylocalorgdir/RLapiSet" ]]; then
    Result="Yes"
else
    Result="No"
fi

echo "<result>$Result</result>"

 

Then create two smart groups - one for devices that don't have Recovery Lock enabled:
 RLnotEnabledgrp.jpg

and one for those that have completed the script policy:
RLsetbyAPI.jpg

Finally, this will require two policies; one Ongoing policy triggered by Login and a Custom Event and scoped to both Smart Groups above:
resetRLpassword.jpg

 

And one triggered by check-in which runs the policy trigger for the other policy and is scoped only to the Recovery Lock not enabled group:

setRL.jpgsetRLF&P.jpg

 

Appears to work beautifully - also created another smart group for Macs that might end up members of both of the above groups in order to detect ones that have had the policies run but for whatever reason haven't actually had the set Recovery Lock mgmt cmd go through.

Wow, wonderful!!  thanks for sharing.

Hi @mike_prather 

I followed your guide but I'm having trouble getting it to work. The policy with the script runs successfully, but the recovery lock isn't active. The log says: "httpStatus" : 401.

I'm not very familiar with the API, so I'm not sure which permissions are necessary for the API role and how to handle the credentials. I've already created an API role with client-id and secret, but I'm not sure if that's the right approach. Could you give me an example of how you do that?

Thanks!

401 means unauthorized, try to add "View MDM command information in Jamf Pro API" permission to your API Role.

I tried it (also with some other permissions), but it does not help.

mike_prather
New Contributor II

@Mideto Admittedly not best practice, but I just use a full admin account for API calls - in general I think the obfuscation provided by the script parameters is pretty good.  As far as I know the only other ones you might need is the Send Set Recovery Lock Command in the Server Actions section and any of the view settings, so that you can get the JSS & MGMT IDs, as well as the .  If you enable the echo cmds under the checkpoints comment, you should be able to tell which API calls aren't working.

EmreU
New Contributor III

You can enable recovery lock with this script.

https://github.com/euydu/JAMFRecoveryLock