Kandji + Nudge

Why Nudge?

In an environment running Kandji, it’s no secret that there are plenty of issues with the ManagedOS Library item. And honestly, it’s not Kandji’s fault entirely - it has more to do with the underlying MDM framework and its interaction with the softwareupdate command.

If you’re a Kandji admin, you’ve probably seen this rage of a text in the logs:

An error occurred executing this item: Could not complete ScheduleOSUpdateScan after 3 attempts.

From the trusty Mike Boylan, Staff Product Engineer @ Kandji. “We found an issue in macOS Ventura that we reported to Apple in early February (FB11989792) where macOS would never tell us [Kandji] if the update finished downloading. As a result, Managed OS would get “stuck” because it didn’t know if the update was actually ready.”

Erik Gomez wrote and maintained a tool called Nudge, check out the GitHub here. It has been widely adopted by Mac admins at companies big and small. It’s simple – it works by encouraging, errrr… Nudging users to update their system. The MacAdmins Slack channel also has a very active and supportive community to help.

Let’s look at how we have deployed and maintained Nudge through Kandji.

At the bottom of the article, I have included a few tips and commands that I found helpful when testing and deploying. Plus, one question I see a lot I have added to the tips section, but calling it out here at the top!

Don’t be confused by the "acceptableCameraUsage" and "acceptableScreenSharingUsage" keys. This DOES NOT mean that Nudge won’t deploy, it means it won’t be the frontmost window*! It will still open at the cadence defined by the LA.

If you want to just view my remote json config file, you can check that out here. and my custom LaunchAgent is available here.

The Three Simple Parts of Nudge

  1. Nudge.app, which is located in /Applications/Utilities/

    • an Application Support folder located at /Library/Application Support/Nudge
  2. LaunchAgent located in /Library/LaunchAgents aptly named com.github.macadmins.Nudge.plist

  3. The remote-json file stored in an S3 bucket: https://kibbleme.s3.us-west-2.amazonaws.com/com.github.macadmins.Nudge.json

A Deep Dive into Each of the Components

The Nudge.app

The Nudge.app is safe to install without configuration as it doesn’t do anything unless it is configured and has a LaunchAgent configured to open it on a cadence. The app is deployed through a custom app in Kandji.

The LaunchAgent.plist

The LaunchAgent is what triggers the app to open and which configuration path to pick up.

MacAdmin side-winder: There is a distinct difference between a LaunchAgent and a LaunchDaemon. A LaunchAgent runs as the local user, whereas a Daemon runs as root. A LaunchAgent loads when a user logs into the user account. We do not want the LA running as root.

The two main parts of the LaunchAgent, referred to as LA from here forward– The config path and the cadence.

Let us take a look at the code:

<?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>com.github.macadmins.Nudge</string>
        <key>LimitLoadToSessionType</key>
        <array>
                <string>Aqua</string>
        </array>
        <key>ProgramArguments</key>
        <array>
                <string>/Applications/Utilities/Nudge.app/Contents/MacOS/Nudge</string>
                <string>-json-url</string>
                <string>https://kibbleme.s3.us-west-2.amazonaws.com/com.github.macadmins.Nudge.json</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
        <key>StartCalendarInterval</key>
        <array>
                <dict>
                        <key>Hour</key>
                        <integer>9</integer>
                        <key>Minute</key>
                        <integer>0</integer>
                </dict>
                <dict>
                        <key>Hour</key>
                        <integer>12</integer>
                        <key>Minute</key>
                        <integer>15</integer>
                </dict>
                <dict>
                        <key>Hour</key>
                        <integer>16</integer>
                        <key>Minute</key>
                        <integer>30</integer>
                </dict>
        </array>
</dict>
</plist>

Line 14/15 tells the LA we are using the -json-url flag and passing the url as a string. If we were using a config profile to hold the parameters, it would indicate such and then point to the path where it is stored on the local machine.

More info on the json-url method a bit farther down. ⬇️

Line 19 sets in motion the cadence of Nudge. We are using the StartCalendarInterval to define a deployment at 09:00, 12:15 and, 16:30, local machine time. The LaunchAgent is fully customizable and can be set to whatever fills your needs. For example, the default LaunchAgent fires at the top and bottom of each hour.

Deployment

The App

I kept it two-parted to track the deployment progress and double-check my work.

The first part is just a custom app in Kandji called and has an underlying audit/enforce script that looks like such.

#!/bin/bash
APPPATH="/Applications/Utilities/Nudge.app"
if [ -e "$APPPATH" ]; then
exit 0
else
exit 1
fi

The LaunchAgent and supporting Script

The app itself, deployed in the previous step, does nothing. The secret sauce is in the part where the LaunchAgent rolls in. As a Kandji Library Item, it also runs an audit-and-enforce with a post-install script to move the parts where they need to go. It’s a zip with the LA and the two logos, unzipped to the users /tmp folder at deployment.

#!/bin/bash

# Logo handling. Creates folder in Application Support folder and moves the two logos there.
mkdir /Library/Application\ Support/Nudge
mv /tmp/kibbleme-logo-dark.png /tmp/kibbleme-logo.png /Library/Application\ Support/Nudge


#Moves the LaunchAgent from the tmp directory into the logged in user's LaunchAgents directory.
mv /tmp/com.github.macadmins.Nudge.plist /Library/LaunchAgents


# If you change your LA file name, update the following line
launch_agent_plist_name='com.github.macadmins.Nudge.plist'

# Base paths
launch_agent_base_path='Library/LaunchAgents/'

# Current console user information
console_user=$(/usr/bin/stat -f "%Su" /dev/console)
console_user_uid=$(/usr/bin/id -u "$console_user")


# Unload the agent so it can be triggered
/bin/launchctl asuser "${console_user_uid}" /bin/launchctl unload -w "$3${launch_agent_base_path}${launch_agent_plist_name}"

# Kill Nudge (CYA)
/usr/bin/killall Nudge

# Load the launch agent
/bin/launchctl asuser "${console_user_uid}" /bin/launchctl load -w "$3${launch_agent_base_path}${launch_agent_plist_name}"

exit 0

And the super simple audit script…

#!/bin/bash

LAUNCHAGENTPATH=/Library/LaunchAgents/com.github.macadmins.Nudge.plist
KIBBLEICONDARK=/Library/Application\ Support/Nudge/kibbleme-logo-dark.png
KIBBLEICONLIGHT=/Library/Application\ Support/Nudge/kibbleme-logo.png


if [ -e "$LAUNCHAGENTPATH" ] && [ -e "$DPTYICONDARK" ] && [ -e "$DPTYICONLIGHT" ]; then
  echo "0"; exit 0
else echo "1"; exit 1
fi

The Configuration JSON in AWS

While you could easily read through the Nudge wiki on GitHub to get your info, you’re here, so I’ll run through our config and perhaps it will help with your deployment. There is a lot to go through here so I will highlight the main features in the code.

"acceptableCameraUsage": true"
acceptableScreenSharingUsage": true 

See my note at the top, and in the notes section about this 😉 These two features tell Nudge that if a user has their camera on or a screen share in action dot not the make the Nudge the front-most window.

"aggressiveUserExperience": true

Tells Nudge that after the deferral deadline, enter “aggressive” mode, meaning the focus window and brings all apps “out of focus."

This is the meat of the code. 🥩

"requiredInstallationDate": "2022-08-30T11:00:00Z" is the deadline, in UTC, after this value passes, enter the defined aggressive mode.

"aboutUpdateURL": "https://support.apple.com/en-us/HT212585" defines the release notes shown to the user at the “More Info” link. This can be set to whatever – more of a “nice-to-have” feature.

We made a few changes to the configuration file to capture devices running Monterey and Ventura. We do this by creating a osVersionRequirements array.

Each rule has a targetedOSVersionsRule, listed from oldest to newest (important!), and an accompanying required version and required date.

 "osVersionRequirements": [
    {
      "aboutUpdateURL": "https://support.apple.com/en-us/HT213268",
      "requiredInstallationDate": "2023-03-31T10:00:00Z",
      "requiredMinimumOSVersion": "13.2.1",
      "targetedOSVersionsRule": "12"
    },
    {
      "aboutUpdateURL": "https://support.apple.com/en-us/HT213268",
      "requiredInstallationDate": "2023-03-31T10:00:00Z",
      "requiredMinimumOSVersion": "13.2.1",
      "targetedOSVersionsRule": "13"
    }
  ],

Nudge can also attempt to download both major and minor installers in the background, pulled through the system updater. However, at this point, I have decided to skip this. People using engineering VPNs and other factors may have issues with reliably downloading, so I like to have the user do it so they are prompted for any problems with the download. We will likely be revisiting this shortly because it does work well for the most part.

We disable this with a few keys in the “optionalFeatures” dictionary:

"attemptToFetchMajorUpgrade": false,  
"disableSoftwareUpdateWorkflow": true

Next, we head into the User Experience part of the code.

There are only a few things we will ever change here. Most of them are the default.

"allowedDeferrals": 1000000 is the number of deferrals, not the currently active window, before Nudge enters “aggressive mode." We keep this number high in our environment and rely on the "requiredInstallationDate" key.

"allowedDeferralsUntilForcedSecondaryQuitButton": 14 is the number of times a user can defer Nudge (change it from the currently active window), before both quit buttons need to be actioned. We do not use in our environment because further down, I utilize the "singleQuitButton": true, which cannot have both values set.

"approachingRefreshCycle": 6000,
"approachingWindowTime": 7200,
"elapsedRefreshCycle": 300,

Approached is before the deadline and elapsed is after the deadline. The amount of time, in hours, Nudge will use to determine that the requiredInstallationDate is “approaching”. Say approaching one more time.

The last part is the User Interface.

Most of which parts are pretty self-explanatory

  "userInterface": {
    "fallbackLanguage": "en",
    "forceFallbackLanguage": false,
    "forceScreenShotIcon": false,
    "iconDarkPath": "/Library/Application Support/Nudge/kibbleme-logo-dark.png",
    "iconLightPath": "/Library/Application Support/Nudge/kibbleme-logo.png",
    "screenShotDarkPath": "/somewhere/screenShotDark.png",
    "screenShotLightPath": "/somewhere/screenShotLight.png",
    "showDeferralCount": true,
    "simpleMode": false,
    "singleQuitButton": true,
    "updateElements": [
      {
        "_language": "en",
        "actionButtonText": "Update Device",
        "informationButtonText": "More Info",
        "mainContentHeader": "Your device will restart during this update",
        "mainContentSubHeader": "Updates can take around 30 minutes to complete",
        "mainContentText": "MacOS 13.3.1 contains important updates and security fixes!🔒 Please update at your earliest opportunity.⏰\n\nFailure to do so before the deadline may see your computer forcing the update at a wildly inconvenient time🙁\n\n-Your friends in Corporate Engineering💜",
        "mainHeader": "A reminder from the Kibbles Corporate Engineering team!",
        "oneDayDeferralButtonText": "One Day",
        "oneHourDeferralButtonText": "One Hour",
        "primaryQuitButtonText": "Later ⏰",
        "subHeader": "#StrongerTogether🤩 "
      }
    ]
  }
}

“actionButtonPath" defines the actions for when that actionButtonText is clicked. Previously, you did not need to set this key to have it open the System Preferences. However, I have found it most reliable by adding the following to the userInterface section:

"actionButtonPath": "/System/Library/PreferencePanes/Softwareupdate.prefpane",

We can set this to anything, such as the Kandji self-service portal. However, in its current state, the built-in updater is the most reliable.

Below is a “chart” that labels the different customizable texts in the code.

Emojis are unicode, so those are fair game 🤙

The “Nice To Knows.”

I swear some of these will sound dumb and obvious, but when you have problem tunnel vision, they might come in handy as they did for me.

  1. The easiest way to test a config/Nudge popup quickly is to unload and reload the LaunchAgent

From /Library/LaunchAgents/ run launchctl unload com.github.macadmins.Nudge.plist and then launchctl load com.github.macadmins.Nudge.plist

  1. Always be streaming. Fire up a separate terminal and watch.

log stream --predicate 'subsystem == "com.github.macadmins.Nudge"' --info --style syslog --color none

  1. If you want to remove the Custom deferral option set approachingWindowTime to 10000 (default is 72). By doing so, you make sure the following is not true: amount of hours from the current date to the requiredInstallationDate is always greater than the approachingWindowTime, the custom button will appear.

  2. The computer time/deadline can be confusing, depending on your deployment method. If you use straight json config file it’s UTC. But, as user Siggi Flygenring pointed out, ‘Local time won’t work with remote JSON payload, it needs to be UTC with a Z in the end’

  3. Don’t be confused by the "acceptableCameraUsage" and "acceptableScreenSharingUsage" keys. This DOES NOT mean that Nudge won’t deploy, just just means it won’t be the frontmost window*! It will still open at every cadance definded by the LA.

Move along.