Safer Client API interactions in Jamf Pro? (Zapier Example)

In a past post we discussed some alternatives to making Jamf Pro API calls directly from clients because the API credentials might be misused by a malicious user or an attacker who gains access to a user’s device. Here we’ll take a look at implementing some middleware to make the API calls on the devices’ behalf to limit risk.

As an example, suppose you wanted to offer a way for users to opt-in to our early-adopter program where they get new software releases a little before the rest of the org. There are lots of ways we could set that up. We could have people shoot us an email or a Slack, or have them call the help desk and then they’ll let us know. Or you could get fancy and create email or chat automations. Then you could put their device into a group in response and use that group when we scope our new software version for its initial deployment. Those methods all work but they might not be super intuitive for the users and some of these methods require some manual effort on the part of the admin. 

If you’ve conditioned your users to regard Self Service as the go-to spot for all things device management, it would be cool to just have a button in there people can click to opt-in to the program. You could have the policy trigger a script policy that puts the device into a group via the Jamf Pro API. 

You could just have the script write a value into an “Testers” extension attribute and then smart group off of that, or we could have the script add the computer directly into a static group. The latter is probably safer because the API user used to authenticate the API call would only need Update permission on Static Computer Groups whereas the extension attribute/smart computer group method would require update on both Computers and Users, which are probably way more sensitive than update on static groups. 

Here’s a script we might run on the clients if we wanted to let them add themselves to a static group…

#!/bin/bash
# Script to add computer to Jamf Pro static group. 

# Set these values...
staticGroupName='BetaTesters'
jssURL='https://myorg.jamfcloud.com'
apiUserPass='api_computergroups_write_user:myverylongandcomplexpassword'

# --------------------------------------------

url="${jssURL}/JSSResource/computergroups/name/$staticGroupName"

ioregData=$(/usr/sbin/ioreg -c IOPlatformExpertDevice -l -ac2)
udid=$( /usr/libexec/plistbuddy -c 'print "IORegistryEntryChildren:0:IOPlatformUUID"' /dev/stdin <<< "${ioregData}" )

# The xml payload for the API call to jamf Pro

read -r -d '' xml <<EOF
<computer_group>
  <computer_additions>
    <computer>
      <udid>$udid</udid>
    </computer>
  </computer_additions>
</computer_group>
EOF

curl \
  --request PUT \
  --silent \
  --show-error \
  --max-time 30 \
  --user "${apiUserPass}" \
  --data "$xml" \
  --header "content-type: application/xml" \
  --url "${url}"

We’ve limited the api user to just update on computer groups so that might be an OK way to do it as long as we’re certain we’re not using computer group membership as the gateway to anything sensitive. But there might be other cases where you need some event on a client to trigger an API call to some part of Jamf Pro that could cause a lot more mayhem if the API credentials leaked out. In that case we really do not want API login info going anywhere near the clients. 

There are a few alternative methods. Like we could have the script write some value to a temp file at a known path and then call “jamf recon”. An extension attribute script can look for a file at the pre-determined path and set the extension attribute value based on the data stored in the file and a smart group can pick up on the extension attribute value to make some scoping change happen. That’s a little convoluted, but probably safer than having the device do a direct API write. 

But what if your workflow requires on-demand read or write to something other than a computer extension attribute and it gets into an area of the Jamf Pro data that has to be protected? 

What we can do here is implement some middle-ware that lets us limit what a client can actually do, like limiting the scope of a change to just a specific item (the middleware only changes membership for one specific computer group or the value for one specific extension attribute, for example) and/or allow a client to only effect a specific computer or set of computers. 

There are lots of options for implementing these things. If you want to run your own server, you can use things like Jamf’s JAWA or write a simple Flask app of your own and run it on AWS in Elastic Beanstalk, LightSail, or as a Lambda. Google and Microsoft have analogous services. But the learning curve is pretty steep if you’re not a programmer or already into all that cloud stuff. 

For the rest of us, there are some services that make it a lot easier. Zapier, PipeDream, and Microsoft Power Automate are just some examples of services that simplify setting up chains of triggers and actions. They let you do things like have a device call a URL with a one-line curl script, then the service makes a Jamf Pro API call on the device’s behalf, logs the action to a Google Sheet, and posts a notification to your team’s Slack channel, for example.

Let’s take a look at doing our “opt-in to the testers group” with the Zapier approach. 

Prep work:

We’ll need some info to supply to Zapier. It’s the exact same thing we’d do if we were using the client script method…

  1. Set up a new API user in Jamf Pro and only give it rights to update Computer Groups. Note the username and password. 
  2. Note the Jamf Pro API URL for the group to update, something like “https://myorg.jamfcloud.com/JSSResource/computergroups/name/BetaTesters”. 

Zapier Setup:

We’re going to need Zapier’s webhook functions for both trigger and action, and those are only available on their paid tier, but they do have a free trial we can use to try things out. 

After signing up for a trial, we’ll click “+ Create Zap”…

Our client is going to kick off the process by making a URL call, so select “webhook” as the trigger event.  

Zapier will show you a webhook setup screen. Select “Catch Hook” as the Trigger Event and click Continue. You don’t need to pick off child keys… that’s used if we wanted the client to pass in XML or JSON data, which might be super useful for some workflows since you can do fancy things like have your zap vary its action based on the data or even pass it on to Jamf Pro as the data for an API Post or Put. But we’re going to keep it simple in this starter example… all we need to pass to Zapier is an identifier for the device that needs to be added to a group so we’ll just send that as a URL query parameter (the part that comes after a ? at the end of a url). 

Zapier will give us a URL we can call to trigger our Zap.  Once you see that, copy that into a script like this…

#!/bin/bash

# Asks webhook processor to add computer to static group

# Paste your Zap URL in here...
baseUrl='https://hooks.zapier.com/hooks/catch/99999999/z9z9zzz/'

# --------------------------------------------
# Get computer udid...
ioregData=$(/usr/sbin/ioreg -c IOPlatformExpertDevice -l -ac2)
udid=$( /usr/libexec/plistbuddy -c 'print "IORegistryEntryChildren:0:IOPlatformUUID"' /dev/stdin <<< "${ioregData}" )

# Append the computer udid as a query parameter…
# EXAMPLE URL: 
# https://hooks.zapier.com/hooks/catch/99999999/zz9zz9zz/?udid=9A9A9A9A-9A9A-9A9A-9A9A-9A9A9A9A9A9A
url="${baseUrl}?udid=${udid}"

# Call Zap...
curl \
  --request POST \
  --silent \
  --show-error \
  --max-time 30 \
  --url “${url}"

Click the test button in the Zap setup screen then run the script. Zapier will catch the HTTP call and you’ll see the UDID you sent in has been parsed out from the query parameter…

Continuing on, next we’ll set up the action. Basically, we’re just going to tell Zapier to do exactly the same thing as the first script in this post that we would have used if we were going to have the client’s doing their own Jamf Pro API calls directly. 

Select the webhook action from the action template list… 

In the setup screen, we’re going to use a custom request, 

We’re doing a “PUT” to “https://myorg.jamfcloud.com/JSSResource/computergroups/name/BetaTesters“, filling in our Jamf Pro API username and password separated by a pipe (“|”), and the same xml body as we used in our direct client script method but letting Zapier fill in the UDID value we sent in with our query parameter… 

<computer_group>
  <computer_additions>
    <computer>
      <udid>(Insert the udid query parameter here)</udid>
    </computer>
  </computer_additions>
</computer_group>

Here’s what a completed Action looks like…

That’s it. Once you test and publish your Zap, you can put the script we used to call our Zap into Jamf Pro and attach it to a Self Service policy. Any time a user clicks the policy in Self Service their client will make a call to our Zap and the Zap will add the computer to our BetaTesters group. 

It’s a little fiddly to set up your first Zap but once you get the hand of it you can do them really fast. There are ton’s of ways to go with this. You can add lots more query parameters by separating them with an & in the url. Then you could make the group name and deciding if the computer should be added or removed from the group. You can use Zapier filters to only allow computers to move into and out of the specific groups you want to allow. You could have one zap with many different API functions using an “action” parameter and use Zapier’s “Path” actions to call different Jamf Pro API endpoints based on that. Or you could have separate Zaps to do things like read or write to a specific or arbitrary extension variable. 

You can pass dynamic In Jamf Pro, you could make the group name a script variable so different policies can work on different groups.  

Security Assessment

Getting API credentials off clients is a security win for sure so there’s a lot to like about this approach right off the bat. But how could an attacker come after us if we’re using the middle-ware approach? In this example, if an attacker could obtain the UDID of another computer, they could move them into the BetaTesters static group. That would put the computer get on the list of devices that got early deployment of new software. That seems pretty pointless. But what if the zap were doing something that requires more protection? A bunch of very smart people I work with regard UDID as a pretty good secret/device identifier in that it’s hard for a non-admin to get their hands on the UDID for another user’s device. That’s probably true since the list of people with access to UDIDs information is small and probably fairly-well trusted. But it would be cool if we could encrypt the device identifier and put it into an extension attribute on each device and then use that as our device identifier, giving Zapier the decryption key so only it could turn it into the plain-text UDID. 

Do give some thought to the risk of giving your API credentials to a third party, Zapier or otherwise. Depending on the permissions you’re granting to the API account, you may need to have to have a pretty high degree of trust before doing so. You can also host these kinds of automations yourself, perhaps in a cloud service like AWS/Assure/Google which may be safer in theory but maybe not since a dedicated process automation SaaS product may be even more careful about security that you’ll be if you set such a system up on your own. It’s a judgement you’ll need to make balanced against what kinds of permissions you’re granting to API accounts. Read up on the security practices used by your cloud vendors. Zapier has some information here: https://zapier.com/help/account/data-management/security-compliance-at-zapier.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: