Archive
One Admin to Rule Them All
During JNUC 2022 the GOATs, Mark Buffington and Sean Rabbitt, presented “One Account to P0wn Them All: How to Move Away from a Shared Admin Account”. One of the workflows that they presented was to utilize the local admin account that is created during a PreStage enrollment as a local admin account for times when you need an admin account. You know, times like when you need to install software on a machine, or do some other admin task but don’t have a user account that is admin. There’s a better way to handle this with Jamf Connect and just in time provisioning of an admin account, but this workflow is for those that maybe are not using Jamf Connect, yet.
The workflow they outlined is to create the PreStage account and the Management Account that is used for User Initiated Enrollment (UIE) with the same password. Then using policies in Jamf Pro, after the Bootstrap Token has been escrowed to Jamf Pro, you can randomize this account password. By randomizing the password you prevent the same password from being on all of your devices. Then when you need to use that account for admin duties, you can use a Jamf Pro policy to change the password to a known password, do the needful, and then re-randomize the password. So how do we turn this into a workflow that is real world?
Note: This workflow is for devices that are enrolled via Automated Device Enrollment only. Can this workflow be adapted for UIE enrolled devices? Probably, but it would require the creation of our admin account along with the escrow of the Bootstrap token. If both of those can be accomodated, then it is possible this workflow could be adapted.
Scenario
We’re going to build out a Self Service method for our field techs and help desk agents to be able change the password for our hidden management/admin account to a known password (something we perhaps store in a password vault and rotate regularly). We’ll also create a script and LaunchDaemon that will run 30 minutes after the password is changed to reset it back to a randomized one. We will also create a Self Service method for them to reset the password back to a randomized one.
Setup
Following along with Mr. Buffington, and using the screenshot from his GitHub for the presentation, the first thing we need to do is create an Extension Attribute that will capture whether the Bootstrap Token has been escrowed to Jamf Pro or not. We need to insure the token is escrowed before we randomize the password, otherwise we could end up with the first SecureToken user being the admin with a randomized password, and that’s not a good idea. In a normal deployment, the Bootstrap token is created and escrowed when the first user signs into the computer interactively (via the login window or via SSH).
Extension Attribute
The code for the Extension Attribute is the following:
#!/bin/bash
tokenStatus=$(profiles status -type bootstraptoken | awk '{ print $7 }' | sed 1d)
if [ $tokenStatus == "NO" ]
then
echo "<result>Not Escrowed</result>"
elif [ $tokenStatus == "YES" ]
then
echo "<result>Escrowed</result>"
else
echo "<result>Unknown</result>"
fi
Smart Group
Now that we have an EA, let’s create a Smart Group to capture the devices that have escrowed their Bootstrap token. It’s pretty simple, we’re just going to look for “Escrowed” as the results of our EA.

Scripts
Ok, we’re gonna need a couple of policies and a couple of scripts. Let’s start with the scripts first.
The first script we are going to create will be utilized by the policy to set the password to a static, known value. The script will create a script on the target computer, along with a LaunchDaemon that will run the script we create after a 30 minute period. The script we create on the computer will simply trigger a policy to re-randomize the admin account password. This will make more sense when we see the script.
#!/bin/bash
#########################################################################################
#
# Copyright (c) 2022, JAMF Software, LLC. All rights reserved.
#
# THE SOFTWARE IS PROVIDED "AS-IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
# JAMF SOFTWARE, LLC OR ANY OF ITS AFFILIATES BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN
# THE SOFTWARE, INCLUDING BUT NOT LIMITED TO DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# CONSEQUENTIAL OR PUNITIVE DAMAGES AND OTHER DAMAGES SUCH AS LOSS OF USE, PROFITS,
# SAVINGS, TIME OR DATA, BUSINESS INTERRUPTION, OR PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES.
#
#########################################################################################
#
#
# You will want to update the script path and script name to be what you would like it to be.
#
# Update these variables: script_path and script_name
#
# You will want to update the name of the LaunchDaemon, along with the contents of the daemon
# to match the script path and name that you set.
# Update this variable: launchDaemon
#
#
#########################################################################################
## VARIABLES
script_path="/private/var/acme/scripts/"
script_name="changemgmtpass.sh"
script="$script_path$script_name"
launchDaemon="/Library/LaunchDaemons/com.acme.changeMgmtPass.plist"
#########################################################################################
# create the script on the local machine
# check for our scripts folder first
if [[ ! -d "$script_path" ]]
then
/bin/mkdir -p "$script_path"
fi
tee "$script" << EOF
#!/bin/bash
# run randomize policy
/usr/local/jamf/bin/jamf policy -event changeMgmtPassword
# bootout launchd
/bin/launchctl bootout system "$launchDaemon" 2> /dev/null
# remove launchdaemon
rm -f "$launchDaemon"
rm -f "$script"
exit 0
EOF
# fix ownership
/usr/sbin/chown root:wheel "$script"
# Set Permissions
/bin/chmod +x "$script"
# now create LaunchDaemon
# Check to see if the file exists
if [[ -f "$launchDaemon" ]]
then
# Unload the Launch Daemon and surpress the error
/bin/launchctl bootout system "$launchDaemon" 2> /dev/null
rm "$launchDaemon"
fi
tee "$launchDaemon" << EOF
<?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">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$(basename "$launchDaemon" | sed -e 's/.plist//')</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/private/var/acme/scripts/changemgmtpass.sh</string>
</array>
<key>StartInterval</key>
<integer>120</integer>
</dict>
</plist>
EOF
# Set Ownership
/usr/sbin/chown root:wheel "$launchDaemon"
# Set Permissions
/bin/chmod 644 "$launchDaemon"
# Load the Launch Daemon
/bin/launchctl bootstrap system "$launchDaemon"
exit 0
Now that we have that script in place, we will create a second script that can be run from a Self Service policy to run the policy to re-randomize the password. This policy can be run prior to the LaunchDaemon running and it will unload the LaunchDaemon and the LaunchDaemon and the script we stored on the system.
#!/bin/bash
#########################################################################################
#
# Copyright (c) 2022, JAMF Software, LLC. All rights reserved.
#
# THE SOFTWARE IS PROVIDED "AS-IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
# JAMF SOFTWARE, LLC OR ANY OF ITS AFFILIATES BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN
# THE SOFTWARE, INCLUDING BUT NOT LIMITED TO DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# CONSEQUENTIAL OR PUNITIVE DAMAGES AND OTHER DAMAGES SUCH AS LOSS OF USE, PROFITS,
# SAVINGS, TIME OR DATA, BUSINESS INTERRUPTION, OR PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES.
#
#########################################################################################
#
#
# You will want to update the script path and script name to be what you would like it to be.
#
# Update these variables: script_path and script_name
#
# You will want to update the name of the LaunchDaemon, along with the contents of the daemon
# to match the script path and name that you set.
# Update this variable: launchDaemonPath
#
#
#########################################################################################
### Variables
script_path="/private/var/acme/scripts/"
script_name="changemgmtpass.sh"
script="$script_path$script_name"
launchDaemon="/Library/LaunchDaemons/com.acme.changeMgmtPass.plist"
# Run the management randomization policy
/usr/local/jamf/bin/jamf policy -event changeMgmtPassword
# now bootout the launch daemon we loadead and delete
# bootout launchd
/bin/launchctl bootout system "$launchDaemon" 2> /dev/null
# remove launchdaemon
rm -f "$launchDaemon"
# remove the script
rm "$script"
exit 0
Policies
Now that our scripts are in place we can create our policies. We are going to create four (4) policies:
- A policy to randomize the management account password on recurring check-in, once.
- A policy to randomize the management account password with a custom trigger and set to ongoing.
- A policy to change the management account password to a known static value, set to ongoing, and available in Self Service
- A policy to randomize the management account password via Self Service, set to ongoing.
Policy 1 – Randomize on check-in
The first policy will simply use the “Management Actions” policy payload set to “Change Account Password” and “Randomly generate new password”.
This policy will be scoped to our “Bootstrap Token Escrowed” Smart Group that we created at the begining. Set this policy to trigger on “Recurring Check-In” and set it to an “Execution Frequency” of “Once Per Computer”. The policy will trigger after the first user has signed into the computer for the first time.
Policy 2 – Randomize on custom event
The second policy can be created by cloning the first policy we created and changing the trigger and the frequency. Uncheck the “Recurring Check-in” trigger and instead check “Custom” and enter a value in the text box. For my policy I set this to “changeMgmtPassword”, but it can be whatever you want. Change the “Execution Frequency” to “Ongoing” and save the policy.
Why did we make those changes to the second policy? Well, we want this policy to be availble to our scripts, so we’re using the custom event, and we want it to run anytime we need it so we set the frequency to Ongoing. Since we will only call this policy via that custom event, we can be fairly certain knowing this policy will only run when we want it to.
Policy 3 – Change to static password via Self Service
We’re on to the third policy. This is the first of our Self Service policies. This policy will have no triggers since it is a Self Service policy, and we want the “Execution Frequency” to be set to “Ongoing”. We will add the first script we created to this policy (it doesn’t matter if it is set to Before or After). Head over to the “Management Actions” portion of the policy and in here you will set the known static password you want this account to use.
Notice the warning we have above our password box. Best practice is for us to randomize the Management Account password, so that is why we’re letting you know this is a bad idea. But we’ll ignore it for now.
Head over to the Scope tab and we’ll set this one to our “Bootstrap Token Escrowed” Smart Group. While you’re here, we’re going to use a trick to hide this policy from most users. Click on the “Limitations” tab and then the Add button. Click on “LDAP User Groups” and add the group you have all of your techs in (you do have an LDAP group for all of your techs, right?). For me that group is named “Jamf Admins” but it can be whatever you want.
Why did we do that? Well, by adding that group as a limitation, a tech will need to login to Self Service so that the policy will be visible. This will prevent normal users from seeing that policy and running it. If you do not have login enabled for Self Service, you can read about it here. Also, you can set it so that users do not have to login to get into Self Service, just that a login button is available. You can also use the login method for scoping policies to users.
After the Scope is done, you can head over to the Self Service tab and setup the way the item will appear in Self Service. In the “Description” field you may want to put info about where the SuP3r SekReT password is stored. Maybe put in the fact that the password will re-randomize after 30 minutes (or whatever timeframe you want) and a reminder to run the Self Service policy to re-randomize.
Once you’re done there, go ahead and save this policy.
Policy 4 – Randomize password via Self Service
Our last policy to create, this policy will randomize the password via Self Service so that a tech can make sure when they are done the password is changed back. For this policy we will have no triggers, since it is Self Service, and the “Execution Frequency” will be set to “Ongoing”. We’ll be doing our work via the second script we created, so go ahead and attach that second script to this policy. Again, it doesn’t matter if it is set to “Before” or “After”.
On the Scope tab you have the choice of making so everyone sees it, or using our “Limitations” trick from Policy 3 to make it visible only to our techs. Scope to our “Bootstrap Token Escrowed” Smart Group and make your decision on the visibility.
Once you’ve done that, head over to the Self Service tab and setup the look of the policy in Self Service. Once you’re done, go ahead and save our policy.
What’s Next?
Now that we have all of the parts and pieces in place, how do we take advantage of this? Well, any computer that gets enrolled will have the Management Account created, and once the Bootstrap token gets escrowed that computer can take advantage of this workflow. A tech will be able to walk up to the computer, open Self Service, login to Self Service, and then utilize our static password policy to use that Management Account to do the needful.
If you wanted to store what type of password (random or static) was in use, you could use a “reverse Extension Attribute” to do that. Basically, store a value in a plist on the computer indicating if the password is “S”tatic or “R”andom. Then use an Extension Attribute to grab that value. You could put this in the scripts that we created above (make sure to include a recon so the value gets into Jamf Pro).
You can find the screenshots, scripts, and the XML of the Extension Attribute in my GitHub repository here.
Using Postman with Jamf Pro Part 3: Computer Groups
Welcome back to my series on using Postman with Jamf Pro. If you haven’t checked out the previous posts, or you’ve never used Postman with the Jamf Pro API, you may want to go read through these:
Using Postman for Jamf Pro API Testing – Part 1: The Basics
Using Postman for Jamf Pro API Testing – Part 2: Creating And Updating Policies
I’ve decided to change the title of this series, slightly, to reflect the fact that Postman can be used for far more than simply testing the API, but actually using the API to get work done. In the upcoming posts in this series I will go over the use of variables for more than just storing username and password and how to use the Runner functionality to run more than one API command.
But before we get too far into this post, I wanted to bring up an important update that is coming to the Jamf Pro Classic API: the use of Bearer Tokens for authorization. Up until the 10.35 release of Jamf Pro, the only method for authentication was “Basic Authentication” which meant the sending of a username and password combination. From a security standpoint, this is not the best way to do API calls. When Jamf released the “Jamf Pro API” they made it to only work with OAuth (ROPC grant type). This is more secure than basic auth and now they have brought that to the Classic API (sidebar: to read more about the two API frameworks in Jamf Pro, go here.) The release notes for 10.35 have a link to the changes made in the Classic API for authentication. In these notes it is mentioned that the Basic Authentication method has been deprecated and will be removed later this year.
Ok, back to our series. In our last post I showed you how to use Postman to create and update a policy. We also talked about how to create Collections within Postman to store these API requests for later use. By creating API requests for specific tasks we will be able to re-use them more quickly, and as you’ll see in a later post, we can use them via a Runner to perform more than one request at a time.
Create A Smart Group
Just like creating a Policy, creating a Computer Group can be as simple as providing just a name for the group, or it can be as complicated as setting the the criteria for a Smart Group. We are going to create a Smart Group that searches for all computers that have not checked in for more than 90 days. I feel like this is a typical task that a Jamf admin might complete.
We are going to be using the “Create Computer Group by ID” POST call from within Postman. The API endpoint on a Jamf Pro server for this is:
/JSSResource/computergroups/id/<id>
The default XML code that is provided in Postman is as follows:
<computer_group>
<name>Group Name</name>
<is_smart>true</is_smart>
<site>
<id>-1</id>
<name>None</name>
</site>
<criteria>
<criterion>
<name>Last Inventory Update</name>
<priority>0</priority>
<and_or>and</and_or>
<search_type>more than x days ago</search_type>
<value>7</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>
</criteria>
</computer_group>
We can provide as much information as we want, or as little. For our Smart Group we’re going to use the following:
- Name: Last Check-In More Than 90 Days
- Is_smart: true
- Criterion Name: Last Check-in
- Criterion and_or: and
- Criterion Search_type: more than x days ago
- Citerion Value: 90
The rest of the information in the XML is optional. Since we are only providing one criteria we do not need to worry about the “opening_paren” or “closing_paren” fields. With our specific information, our new XML should look like this:
<computer_group>
<name>Last Check-In More Than 90 Days</name>
<is_smart>true</is_smart>
<criteria>
<criterion>
<name>Last Check-in</name>
<and_or>and</and_or>
<search_type>more than x days ago</search_type>
<value>90</value>
</criterion>
</criteria>
</computer_group>
If we send that to Jamf via Postman, we should have a new Smart Computer Group in our Jamf Pro server.
Pretty simple, right? From here we can get more complicated if we need to, adding more criteria to the query. Perhaps we want to refine our search to machines that haven’t checked in for more than 90 days and that have Adobe Photoshop 2021 installed. This type of search would allow us to identify stale Photoshop licenses. The XML for that Group might look like this:
<computer_group>
<name>Adobe Photoshop Stale Licenses</name>
<is_smart>true</is_smart>
<criteria>
<criterion>
<name>Last Check-in</name>
<and_or>and</and_or>
<search_type>more than x days ago</search_type>
<value>90</value>
</criterion>
<criterion>
<name>Application Title</name>
<and_or>and</and_or>
<search_type>is</search_type>
<value>Adobe Photoshop 2021.app</value>
</criterion>
</criteria>
</computer_group>
As you can see, it’s easy to create these groups. If you need to do more complicated Smart Groups, you can always create the group in the Jamf Pro interface and then use the GET call in Postman to inspect the XML. Using that method will allow you to understand how to construct even more complicated groups (like those with parenthesis and such).
Modify A Smart Group
When it comes to modifying an existing Smart Group, the process is very similar to the creation process. I suggest using the GET method to find the Smart Group you need to modify, then copy the XML out, make the changes, and paste that into the PUT method for updating.
Let’s use our last example from above, the Adobe Photoshop Smart Group. Maybe we made a mistake and it’s not the 2021 version we want to find, but the 2020 version. From my demo server, using the GET method, I get the following XML returned:
<?xml version="1.0" encoding="UTF-8"?>
<computer_group>
<id>9</id>
<name>Adobe Photoshop Stale Licenses</name>
<is_smart>true</is_smart>
<site>
<id>-1</id>
<name>None</name>
</site>
<criteria>
<size>2</size>
<criterion>
<name>Last Check-in</name>
<priority>0</priority>
<and_or>and</and_or>
<search_type>more than x days ago</search_type>
<value>90</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>
<criterion>
<name>Application Title</name>
<priority>0</priority>
<and_or>and</and_or>
<search_type>is</search_type>
<value>Adobe Photoshop 2021.app</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>
</criteria>
<computers>
<size>0</size>
</computers>
</computer_group>
For me to change the version I want to look at, the Application Title, I need to send the following XML to the server:
<computer_group>
<id>9</id>
<name>Adobe Photoshop 2020 Stale Licenses</name>
<criteria>
<criterion>
<name>Last Check-in</name>
<priority>0</priority>
<and_or>and</and_or>
<search_type>more than x days ago</search_type>
<value>90</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>
<criterion>
<name>Application Title</name>
<priority>0</priority>
<and_or>and</and_or>
<search_type>is</search_type>
<value>Adobe Photoshop 2020.app</value>
<opening_paren>false</opening_paren>
<closing_paren>false</closing_paren>
</criterion>
</criteria>
</computer_group>
Again, using the PUT method, sending that XML to the server will update the Smart Group. You can see that now we’ve corrected the name of the Group and we’ve changed the Application Title we are looking for:

Wrap Up
That’s it for Smart Groups. Using the skills you’ve learned with the previous post and this one, you should be able to leverage the API via Postman to create, update, list, and delete just about any object in the Jamf Pro server.
Next up in our series we’re going to talk about variables and the Runner functionality. Leveraging these two things will allow us to create batch jobs to do things like setup policies or create smart groups or even delete items. So stay tuned for that next post.
How To Quickly Open a Policy in Jamf Pro
We have a lot of policies. I mean over 1,000 policies in our Jamf Pro Server. Don’t ask. Part of it is out of necessity, but I’ll bet some of it is just because we were running so fast in 2018 to get systems enrolled and agencies under management, that we didn’t have time to, as Mike Levenick (@levenimc) recently put it, “trim the fat”. That’s what 2019 is all about. But I’m missing the point of this post: how to quickly open a policy. You can imagine how long it takes to load the list of policies when you have over 1,000 of them.
There are a couple of tools you’ll need. First up you’ll want a tool like Text Expander to create snippets or macros. I’m sure there are some free alternatives out there that will expand a text shortcut into something but Text Expander is what I’ve been using for many years, of course I’m using version 5 which is a perpetual license version and not the current subscription model. (Here’s an article about text expansion apps)
The second tool you’ll need is jss_helper from Shea Craig (@shea_craig). This will help us pull a list of the policies in our system, including the ID of the policy.
Now that you have your tools in place, the first thing you want to do is grab the URL of one of your policies. Just open a policy and copy the URL. Now go into Text Expander (or whatever tool you chose) and create a new snippet from the contents of the clipboard. Edit the URL removing everything after the equals (=) sign in the URL. Give your new snippet a shortcut and voila! You now have an easy way to get 90% of the way to quickly opening policies. Your URL snippet should look similar to this:
https://jss.yourcompany.com/policies.html?id=
Now let’s turn our attention to jss_helper. Once you have it installed and configured to talk to your JPS, you’re going to want to pull a list of the policies in your system. Open up Terminal (if it isn’t already) and run jss_helper with the following options:
jss_helper policies > ~/Desktop/policy_list.txt
Obviously you want to name that file whatever you want, but the cool thing is that you now have a list of every policy in your JPS along with its ID. If you open that file up in Excel or a text editor, you’ll see something like this:
ID: 2034 NAME: Installer Adobe Acrobat DC 19 ID: 1214 NAME: Installer Adobe Acrobat DC Reader ID: 2030 NAME: Installer Adobe After Effects CC 2019 ID: 2031 NAME: Installer Adobe Animate CC 2019 ID: 2032 NAME: Installer Adobe Audition CC 2019 ID: 2033 NAME: Installer Adobe Bridge CC 2019 ID: 638 NAME: Installer Adobe Codecs ID: 532 NAME: Installer Adobe Creative Cloud Desktop elevated App ID: 314 NAME: Installer Adobe Creative Cloud Desktop Non elevated App
Now let’s put it together. Open up your web browser and in the address bar type whatever shortcut you created for the policy URL above. Once the URL expands, before pressing enter, type in the ID number of the policy you want to open and then press enter. The policy should open up without having to wait for the list of policies to load or having to search the web interface for the specific policy.
Hopefully this will help speed up your game and help you become quicker and getting stuff done.
Using AWS Lambda To Relay Jamf Pro Webhooks to Slack
I recently got interested in utilizing webhooks in Jamf Pro but had no idea where to start. I went and watched Bryson Tyrrell’s (https://twitter.com/bryson3gps) presentation from JNUC 2017 Webhooks Part Deaux! and then went over to take a peek at Jackalope on the Jamf Marketplace. I read the docs, I tried to figure out how to do this in AWS ElasticBeanstalk, but I just couldn’t get it going. Just too much going on to devote enough time to it. So, I went over to Zapier and signed up for their free account so I could get this going. I got it working, but I quickly got throttled because I decided to enable the “ComputerCheckIn” webhook to make sure it worked. I think we flooded the 100 connection limit within 30 seconds and wound up having thousands of items in Zapier.
Well, that wasn’t going to work, so I changed it to “ComputerAdded” and waited for my month of Zapier to renew so I’d get 100 new “zaps”. That worked, until we went over the 100 limit again and had to wait. There had to be a better way that wasn’t going to cost me a ton of money. So I went Googling and came across an article on how to use AWS Lambda to do what I wanted to do: AWS Lambda For Forwarding Webhook To Slack.
I walked through the steps outlined on the page to setup the function in Lambda and everything worked great until I got to the part where I was making requests out to Slack. Lambda had a problem with the request method. Specifically this line of code:
var post_req = https.request(post_options, function(res) {
So another round of Googling and I came up with the Node.js docs page on HTTPS and I figured out how to properly make the call:
const req = https.request(post_options, | |
(res) => res.on("data", () => callback(null, "OK"))) | |
req.on("error", (error) => callback(JSON.stringify(error))); | |
req.write(post_data); | |
req.end(); |
Once I was able to get past the https connection issues, I was able to utilize the rest of Patrick’s example to get my webhook from Jamf feeding into a Slack channel. We uploaded a custom emoji to our Slack channel and used the Slack documentation on basic message formatting and on attachments to get the notification to look how we wanted.
Ultimately we created two Lambda functions, one for ComputerAdded and another for ComputerInventoryComplete, each feeding into their own channel in our Slack. This was fairly easy to accomplish, the next step is to find a way to feed DataDog, or some other service, the ComputerCheckIn webhook so I can get a count of how many check-ins we have each day.
The code we used is below, but I wanted to point out one or two things. Where I got hung up the most was how to pull things like Computer Name or Serial Number from the JSON we were getting from the Jamf Pro server. Since the JSON contains two arrays, “webhook” and “event”, it took me a little bit to understand how to grab that data. To be honest, my skills here are lacking considerably so it took me longer than it should. Ultimately I figured out that you just have to dot walk to get the data you want. So to get the Computer Name it’s:
body.event.deviceName
“body” is the JSON object that we parse the webhook into. Once I figured that out I was all set to grab whatever data from the event, or webhook, array that I needed. Hopefully my head banging will help others not stumble quite so much.
Here’s the Node.js code we used as the template:
var https = require('https'); | |
exports.handler = (event, context, callback) => { | |
console.log("MYLOG" + JSON.stringify(event)) | |
var body = JSON.parse(event.body); | |
var name = body.event.deviceName; | |
var sernum = body.event.serialNumber; | |
var user_name = body.event.username; | |
var building = body.event.building; | |
var curr_time = Math.floor(new Date() / 1000); | |
var post_data = JSON.stringify({ | |
"username": "Jamf Pro Server", | |
"icon_emoji": ":balloon:", | |
"channel": "#<yourslackchannelname>", | |
"text": "Computer Enrolled", | |
"attachments": [ | |
{ | |
"color": "good", | |
"fields": [ | |
{ | |
"title": "Computer: " + name, | |
"value": "Serial number: " + sernum | |
+ "\nUser name: " + user_name | |
+ "\nBuilding: " + building, | |
"short": false | |
} | |
], | |
"footer": "Jamf Webhook", | |
"footer_icon": ":balloon:", | |
"ts": curr_time | |
} | |
] | |
}); | |
var post_options = { | |
host: 'hooks.slack.com', | |
port: '443', | |
path: '/services/YOURWEBHOOK', | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'Content-Length': Buffer.byteLength(post_data) | |
} | |
}; | |
const req = https.request(post_options, | |
(res) => res.on("data", () => callback(null, "OK"))) | |
req.on("error", (error) => callback(JSON.stringify(error))); | |
req.write(post_data); | |
req.end(); | |
var details = { | |
"status": "OK" | |
} | |
var response = { | |
'statusCode': 200, | |
'headers': { 'Content-Type': 'application/json' }, | |
'body': JSON.stringify(details) | |
} | |
console.log("LOG:: " + JSON.stringify(response)) | |
callback(null, response); | |
}; |
Scripting Remote Desktop Bookmarks
A few years ago I was searching for a way to easily create bookmarks in Microsoft Remote Desktop 8 on the Mac. Prior to version 8 you could drop an .RDP file on a machine and that was really all you needed to do to give your users the ability to connect to servers. Granted, you can still use this method, it’s just a bit sloppier, in my opinion.
So I went searching for a way to script the bookmarks, and that led me to my good friend Ben Toms’ (@macmuleblog) blog. I found his post, “HOW TO: CREATE A MICROSOFT REMOTE DESKTOP 8 CONNECTION” and started experimenting. After some trial and error, I discovered that using PlistBuddy to create the bookmarks just wasn’t being consistent. So I looked into using the defaults command instead. I finally was able to settle on the following script:
#!/bin/sh | |
# date: 18 Jun 2014 | |
# Name: RDC-Connection.sh | |
# Author: Steve Wood (swood@integer.com) | |
# updated: 29 Feb 2016 - included line to add remote program to start on connection for @gmarnin | |
# grab the logged in user's name | |
loggedInUser=`/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }'` | |
# global | |
RDCPLIST=/Users/$loggedInUser/Library/Containers/com.microsoft.rdc.mac/Data/Library/Preferences/com.microsoft.rdc.mac.plist | |
myUUID=`uuidgen` | |
LOGPATH='/private/var/inte/logs' | |
# set variables | |
connectionName="NAME YOUR CONNECTION" | |
hostAddress="SERVERIPADDRESS" | |
# if you need to put an AD domain name, put it in the userName variable, otherwise leave blank | |
userName='DOMAINNAME\' | |
userName+=$loggedInUser | |
resolution="1280 1024" | |
colorDepth="32" | |
fullScreen="FALSE" | |
scaleWindow="FALSE" | |
useAllMonitors="TRUE" | |
set -xv; exec 1> $LOGPATH/rdcPlist.txt 2>&1 | |
defaults write $RDCPLIST bookmarkorder.ids -array-add "'{$myUUID}'" | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.label -string "$connectionName" | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.hostname -string $hostAddress | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.username -string $userName | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.resolution -string "@Size($resolution)" | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.depth -integer $colorDepth | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.fullscreen -bool $fullScreen | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.scaling -bool $scaleWindow | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.useallmonitors -bool $useAllMonitors | |
#comment out the following if you do not need to execute a program on start of connection | |
# You can adjust the string to be any app that is installed. | |
defaults write $RDCPLIST bookmarks.bookmark.{$myUUID}.remoteProgram -string "C:\\\\Program Files\\\\\\\\Windows NT\\\\Accessories\\\\wordpad.exe" | |
chown -R "$loggedInUser:staff" /Users/$loggedInUser/Library/Containers/com.microsoft.rdc.mac |
You can find that code in my GitHub repo here.
RDC URI Attribute Support
I had posted that script up on JAMF Nation back in June 2014 when someone had asked about deploying connections. Recently user @gmarnin posted to that thread asking if anyone knew how to add an alternate shell key to the script. After no response, he reached out to me on the Twitter (I’m @stevewood_tx in case you care). So, I dusted off my script, fired up my Mac VM, and started experimenting.
The RDC GUI does not allow for a place to add these URI Attributes. I read through that web page and Marnin forwarded me this one as well. Marnin explained that he was able to get it to work when he exported the bookmark as an .RDP file and then used a text editor to add the necessary “alternate shell:s:” information. Armed with this knowledge, I went to the VM and started testing.
First I created a bookmark in a fresh installation of RDC. I had no bookmarks at all. After creating a bookmark I jumped into Terminal and did a read of the plist file and came up with this:
YosemiteVM:Preferences integer$ defaults read /Users/integer/Library/Containers/com.microsoft.rdc.mac/Data/Library/Preferences/com.microsoft.rdc.mac.plist | |
{ | |
QmoteUUIDKey = "ff870b10-7e8e-47c2-98bd-f14f3f0cd1b0"; | |
"bld_number" = 26665; | |
"bookmarklist.expansionStates" = { | |
GENEREAL = 1; | |
}; | |
"bookmarkorder.ids" = ( | |
"{2a3925d6-659e-456e-ab03-86919b30b54b}" | |
); | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.fullscreenMode" = "@Variant(\177\017FullscreenMode\001)"; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.hostname" = "termserv.company.com"; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.label" = Test; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.username" = ""; | |
"connectWindow.geometry" = <01d9d0cb 00010000 000001b4 000000a0 000003b9 0000033f 000001b4 000000fc 000003b9 0000033f 00000000 0000>; | |
"connectWindow.windowState" = <000000ff 00000000 fd000000 00000002 06000002 44000000 04000000 04000000 08000000 08fc0000 00010000 00020000 00010000 000e0074 006f006f 006c0042 00610072 01000000 00ffffff ff000000 00000000 00>; | |
lastdevinfoupd = 1456781093; | |
lastdevresourceupd = 1456781153; | |
"preferences.ignoredhosts" = ( | |
"10.93.209.210:3389" | |
); | |
"preferences.resolutions" = ( | |
"@Size(640 480)", | |
"@Size(800 600)", | |
"@Size(1024 768)", | |
"@Size(1280 720)", | |
"@Size(1280 1024)", | |
"@Size(1600 900)", | |
"@Size(1920 1080)", | |
"@Size(1920 1200)" | |
); | |
"show_whats_new_dialog" = 0; | |
"stored_version_number" = "8.0.26665"; | |
tlmtryOn = 1; | |
} |
Now that we had a baseline, I exported the bookmark to the desktop of the VM, edited it to add the “alternate shell” bits, and then re-imported it into RDC as a new bookmark. I then tested to make sure it would work as advertised. After some trial and error, I was able to get the exact syntax for the “alternate shell” entry to work. Now I just needed to see what changes were made in the plist file. A quick read showed me the following:
YosemiteVM:Preferences integer$ defaults read /Users/integer/Library/Containers/com.microsoft.rdc.mac/Data/Library/Preferences/com.microsoft.rdc.mac.plist | |
{ | |
QmoteUUIDKey = "ff870b10-7e8e-47c2-98bd-f14f3f0cd1b0"; | |
"bld_number" = 26665; | |
"bookmarklist.expansionStates" = { | |
GENEREAL = 1; | |
}; | |
"bookmarkorder.ids" = ( | |
"{2a3925d6-659e-456e-ab03-86919b30b54b}" | |
); | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.fullscreenMode" = "@Variant(\177\017FullscreenMode\001)"; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.hostname" = "termserv.company.com"; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.label" = Test; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.username" = ""; | |
"bookmarks.bookmark.{2a3925d6-659e-456e-ab03-86919b30b54b}.remoteProgram" = "C:\\\\Program Files\\\\\\\\Windows NT\\\\Accessories\\\\wordpad.exe"; | |
"connectWindow.geometry" = <01d9d0cb 00010000 000001b4 000000a0 000003b9 0000033f 000001b4 000000fc 000003b9 0000033f 00000000 0000>; | |
"connectWindow.windowState" = <000000ff 00000000 fd000000 00000002 06000002 44000000 04000000 04000000 08000000 08fc0000 00010000 00020000 00010000 000e0074 006f006f 006c0042 00610072 01000000 00ffffff ff000000 00000000 00>; | |
lastdevinfoupd = 1456781093; | |
lastdevresourceupd = 1456781153; | |
"preferences.ignoredhosts" = ( | |
"10.93.209.210:3389" | |
); | |
"preferences.resolutions" = ( | |
"@Size(640 480)", | |
"@Size(800 600)", | |
"@Size(1024 768)", | |
"@Size(1280 720)", | |
"@Size(1280 1024)", | |
"@Size(1600 900)", | |
"@Size(1920 1080)", | |
"@Size(1920 1200)" | |
); | |
"show_whats_new_dialog" = 0; | |
"stored_version_number" = "8.0.26665"; | |
tlmtryOn = 1; | |
} |
The key is the line that has “remoteProgram” as part of the entry. You have to get the full path on the Windows machine to the application you want to run on connection to the server. Once you know that path, you can adjust your bookmark script however you need.
The script I posted above, and is linked in my GitHub repo, contains the line to add that Remote Program (alternate shell). If you do not need it, just comment it out of the script.
Custom CrashPlan Install With Casper
I’m a fanboy. There, I said it and I’m proud of it. I’m a fanboy of JAMF Software’s Casper Suite. I’m also a fanboy of Code42 and their CrashPlan software. Put them together and it’s like when the two teens discovered peanut butter and chocolate as an amazing combination.
I am all about trying to minimize the amount of time my users need to be interrupted due to IT needs. That’s a large part of the reason we use Casper, so that my users do not have to be inconvenienced. Let’s face it, the more time I take performing IT tasks on their computer that cause them to not be able to work, the less money they are making for our agency. It’s one of my primary tenets of customer support: make every reasonable effort to not disturb the end user, period. So when I discovered several of my end user machines were not backing up via CrashPlan, I needed to find a way to deploy CrashPlan with as little interruption as possible. In steps Casper and CrashPlan together.
Our original setup of CrashPlan that has been running for several years, was setup using local logins. At the time when we first deployed, we were not on a single LDAP implementation, so I didn’t want to deploy an LDAP integrated CrashPlan. Fast forward to now, and we have a single LDAP (AD) and I want to take advantage of that implementation to provide “same password” logins for my users.
Fortunately JAMF has a technical paper outlining how to do this, titled Administering CrashPlan PROe with The Casper Suite. This paper was written back when CrashPlan PROe was still a thing. With the release of version 5 of CrashPlan, it has now become simply Code42 CrashPlan. This document still works for the newer version of the software.
Get The Template
The first step is to get ahold of CrashPlan custom template for the installer. Following the paper, you can download the custom template by navigating to this URL:
http://YourServerAddress:4280/download/CrashPlanPROe_Custom.zip
NOTE: If you are deploying version 5 or higher of CrashPlan, you can use this URL to download a newer version of the kit:
http://YourServerAddress:4280/download/Code42CrashPlan_Custom.zip
While there are two different URLs, you can use either one to customize your install.
Edit Away
After downloading and expanding the zip file, you will need to edit the userInfo.sh
file to set some settings. First of which is to hide the application from your users during installation. Simply set the following line:
startDesktop=false |
The next thing you will want to edit are the user variables. CrashPlan uses these variables to grab the user’s short name and their home folder location. An assumption is made when it comes to the user’s home folder, and that is the assumption that the home folder lives in /Users. If your home folders do not live there, or you want to script the generation using dscl, you can. I’m lazy and so I simply went with the /Users setting.
Also, the method to grab the user short name is based on the user that is logged in currently. Now, we didn’t discuss before how you were deploying this via Casper (login, logout, Self Service, etc), but suffice it to say, it is preferable to deploy this when a user is logged in to the computer. There have been many discussions on JAMF Nation about CrashPlan and how to grab the user, I used the information found in this post to grab the info I needed:
user=`/usr/bin/defaults read /Library/Preferences/com.apple.loginwindow lastUserName` | |
CP_USER_HOME="/Users/$user" | |
userGroup=`id -gn "$user"` | |
CP_USER_NAME="$user" |
Now that you have the edits done, keep going through the technical paper, running the custom.sh
script next to build the Custom folder we will need in a minute, and to download the installers. The custom.sh
script will download the installers for Windows, Mac, and Linux, and slipstream the Custom folder into the installer package for us. In our case, since we are only concerned with the Mac installer, it places a hidden .Custom folder at the root of the DMG. We want that folder. So follow along in the tech paper to mount the Mac installer DMG and copy the .Custom folder out somewhere.
Package It All Up
We are going to need to deploy these custom settings alongside the CrashPlan installer. The tech paper has you using Composer (no surprise since it is their product), but I personally like to use Packages for my packaging fun. I’m not going to get into a discussion about what the best packaging method is, because that’s like debating which Star Trek movie was the best.
Using your method of packaging, create a package that drops that Custom folder (notice we are dropping the period so it is not hidden) into the following location:
/Library/Application Support/CrashPlan
Now that we’ve got our custom settings, we can move over to the JSS to work on our deployment. I’m going to skip discussing how to do this via Self Service, and instead stick with either a Login trigger or Recurring Check-In trigger. But first things first, go ahead and upload that custom settings package you just created into the JSS. Once it’s uploaded set the priority to something low, like 8:
Create Your Policies
The tech paper discusses uploading the CrashPlan installer along with the custom properties, but I like the method that is discussed in this JAMF Nation post. It’s towards the bottom, and basically it uses curl to download the installer from the CrashPlan server. This method insures you have the latest version for your server. Of course, if you are trying to deploy to end users around the globe that may not have curl access to your CrashPlan server, uploading the installer to Casper may be your only option. For me, however, it was not.
First step is to create a new script in the JSS (or upload a script if your scripts are not stored in the database). The script itself is nothing special, it checks for the presence of the CrashPlan launch daemon, and if it is there unloads it and removes CrashPlan. Then the script continues on to install the custom properties (via a second policy) and finally installs CrashPlan:
#!/bin/sh | |
# unload CrashPlan LaunchDaem if it exists | |
if [[ -e /Library/LaunchDaemons/com.crashplan.engine.plist ]]; then | |
launchctl unload /Library/LaunchDaemons/com.crashplan.engine.plist | |
/Library/Application\ Support/CrashPlan/Uninstall.app/Contents/Resources/uninstall.sh | |
rm -rf /Library/Application\ Support/CrashPlan | |
fi | |
# install the custom properties folder | |
jamf policy -event CrashInstall | |
# now install CrashPlan | |
curl http://yourserveraddress:4280/download/Code42CrashPlan_Mac.dmg > /var/tmp/CP.dmg | |
hdiutil attach /var/tmp/CP.dmg | |
installer -pkg /Volumes/Code42CrashPlan/Install\ Code42\ CrashPlan.pkg -target / | |
hdiutil detach /Volumes/Code42CrashPlan | |
rm -rf /var/tmp/CP.dmg |
As you can see, I’m using a second policy to install the custom properties. You could do everything with one policy and two scripts, or one policy and curl the custom properties from another location. The key point is that if you are removing an existing installation (like I was), you cannot install the custom properties until you are done removing the existing. Make sense?
Now that we have all of our pieces and parts up there, you will create your two policies, one to install the custom properties and the other to run the script.
To Trigger Or Not To Trigger
With your policies created, you now need to determine how you want to trigger these policies. Obviously you will need to trigger one from within the script, but what about the main policy that kicks it all off? Well, I would probably do this via a recurring check-in trigger. It keeps the user from having to wait for the policy to complete before their login completes.
Of course, you could use the login trigger and throw up a nice notification using jamfHelper, Notification Center, or CocoaDialog. That sounds like a nice post for another day.
I Didn’t Do It
I cannot take the credit for this process. It was people like Bob Gendler and Kevin Cecil on JAMF Nation, along with the folks at JAMF and Code42, that did the heavy lifting. I just put it all into one location for me to remember later.
JAMF Nation User Conference 2014
Every year as October gets closer, I get anxious. I know that at some point, usually toward the end of the month, I will be traveling up to Minneapolis for the JAMF Nation User Conference, JNUC. The conference is located at the Guthrie Theater in downtown, in the Mill district of Minneapolis, right on the Mississippi river. It’s one of my favorite locations to go to.
The JNUC is one of my favorite conferences to attend. Not just for the great content, but for the relationships that get formed and strengthened there. There are friends at JNUC that I’ve known for well over 10 years now, just from attending different conferences in the past. It’s great to catch up with these friends.
This year was also special because I was presenting there. I had the opportunity to present on imaging in a session titled “Unwrap the Imaging Enigma”. It was a wonderful experience, and one that I will repeat again. Giving back to the community by presenting is important for any admin. If you’re interested in the slides, you can find them on my GitHub repository.
Now that I’ve called more attention to this blog, and to myself, I will try to post more relevant content regularly. If there is a topic you’d like to see, just post it in the comments and I’ll see if I can come up with something. Or reach out to me on Twitter: @stevewood_tx