Run a long script without holding up the policy queue?

Malcolm
Contributor II

I'm working with my mate AI, to write a clever script, to keep my kids in line from using VPN in the school, which will present them with any annoying re-petative pop up on their screen, when ever they engage the use of a VPN.

it runs for 14 minutes on loop, running a 2 minute intervals check on if the local ip of the client, is within one of our networks subnets, and then checks for the reported current public IP from amazon, and if it doesn't come from one of our own, if it flags as being within one of our subnets and not with our public IP, it will proceed to prompt the end user, to turn off VPN, repetitiously, checking in every 3 minutes to see if the vpn has been turned off.

basically they are using VPN to by pass, and play games in class, the constant prompt will ongoingly interrupt their games.

its run every 14 minutes, so that next policy run, it will start over again. 

my issue is, I want it to run separately to the remaining polices, I want it to run the script seperate to the policy process, allowing other processes that might run after it, to continue to run.

13 REPLIES 13

sdagley
Esteemed Contributor III

@Malcolm You could use a LaunchDaemon triggered by a network change to run your script instead of using a Jamf Pro policy. This way execution is independent of your Jamf Pro policies.

We have some savvy enough kids who would find it there I think.

this seems to work: nohup /var/vpn_detector.sh > /var/log/vpn_detector.log 2>&1 &

but I've exceeded my free vpn usage for today, so I can't test it further at the moment.

initially considered making it reboot their device. But I think the ongoing pop up is a suitable deterrent for now.
I might look at forced rebooting, for the excessive repeat offenders.

Just was hoping that it could all be excuted through Jamf, via having to pkg a version of the script and deploy and modify it and repackage as it develops.

sdagley
Esteemed Contributor III

@Malcolm If the kids are savvy enough to find & disable a LaunchDaemon that is restricting their VPN access wouldn't that also imply they're savvy enough to kill the process you're launching via the Jamf Pro agent and/or disable the Jamf Pro agent itself?

My approach for making a LaunchAgent/script combo tamper resistant is to have an Extension Attribute which checks to see if the LaunchDaemon is running, and checksums the LaunchDaemon and script files. A Smart Group reports on any Mac that fails that check, and a policy is scoped to that Smart Group for automatic remediation.

Malcolm
Contributor II

my current approach is to have a script, to write the actual script as a file locally and then to  execute the script as an execute command within Files and Processes

nohup /Library/Scripts/vpn_detector.sh > /dev/null 2>&1 &

but I was hoping for a simpler solution allowing for easy update of the actual script as needed.

I had to use nohup /var/vpn_detector.sh > /var/log/vpn_detector.log 2>&1 &
due to execution rights issues running it from /library/scripts

AJPinto
Esteemed Contributor

To setup or enable a system wide VPN, they would need admin access. Removing the admin access would be the correct solution in this case. I also recommend looking in to network side security tools, you can perform TLS interception and reject all traffic from disallowed hosts or site categorization and blocking sites in the "Gaming" category. 

Could you script something like this? Yes, but it will not be a very good experience.

Just to ask the question, have you confirmed they are using a VPN and not doing something like tethering the devices to their personal phones?

feolaney
New Contributor III

I agree with AJPinto, there are alot of things that can be done with scripts, Launch Daemons, and automations, ultimately though if these students have admin access all your efforts can be removed.

sdagley
Esteemed Contributor III

To address the situation where a student is using their phone as a hotspot the following script will re-order the Wi-Fi list to ensure your preferred network(s) have the highest priority: https://community.jamf.com/t5/jamf-pro/re-order-wifi-preferred-networks/m-p/262367#M241662

GadgetVirtuoso
New Contributor III

Another option you could go with is using a smart group with an extension attribute.

#!/bin/bash

# Known safe IPs (edit these!)
TRUSTED_IPS=("<your trusted IPs")

# Get external IP from reliable source
EXT_IP=$(curl -s --max-time 5 https://ifconfig.me)

if [[ -z "$EXT_IP" ]]; then
echo "<result>Unknown</result>"
exit 0
fi

if printf '%s\n' "${TRUSTED_IPS[@]}" | grep -q "$EXT_IP"; then
echo "<result>Trusted</result>"
else
echo "<result>Untrusted</result>"
fi

 Once you have the attribute, you can then trigger Jamf to do all kinds of things. You could send emails to teachers with the current user doing the undesired behavior, and then address it as a school policy issue that it is.

A_Collins
Contributor II

I am actually performing this task as well by using launchdaemon, Using locally installed script on computer with monitoring network changes 

<?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>edu.moriah.vpn</string>
    <key>ProgramArguments</key>
    <array>
        <string>/var/moriah/vpn-lock.sh</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>WatchPaths</key>
    <array>
        <string>/var/run/resolv.conf</string>
    </array>
</dict>
</plist>

Malcolm
Contributor II

Some interesting comments here and points made.

Yes our students have admin access, as they are BYOD, we don't restrict them to their own device.

Since making the post, I've made two scripts, one which will check running applications for specific bundID's and if present obtain their path and PID to kill the application, this is an interesting ones as it has given me an avenue to target other applications such as apple messenger, which in earlier versions of macOS wasn't too hard to circumvent by duplicating the app and renaming it and opening it.

the other, detects for a non school public IP, and if detected it sends an interrupting popup every 30 seconds, informing them to close it. But most likely the first mentioned script, will cause this script to not occur often.

the scripts are designed to run in loop, seperate from Jamf policies, but the process to do that requires to distribute them as a packaged file, to a local location, and then launch them via the files and processes section of policies.

 

but the aforementioned note that if they could kill a launch task, they can likely kill Jamf, Jamf is a little harder to kill, but in general to do this, you have to unmanaged the device, or block Jamf's communication to the MDM.

unmanaged devices, will get stripped of a FW certificate, which will block certain access externally, so this has always been a significant deterrent for our users.

But it did remind me that, Jamf likes to stop itself from procesing normally, after long time use, which I mostly see when ever self-service is patched with an update, and the client isn't rebooted frequently.

Which puts me onto my next task I guess, to make a script to query logs from Jamf, and if the logs don't change, after a certain duration, to reboot the Jamf unix executable

This one I might run as a launch daemon, Ill have to think about this a little.

 

Malcolm
Contributor II

The script I have just put into testing, but this is something I just worked with AI, to create a Jamf agent monitoring script and launch deamon to check log inactivity within the Jamf log, a not frequently updated Jamf log, is a solid indication that something has happened to the JamfDeamon Unix Executable it will also kill the Jamf Unix Executable, in the event that it has frozen in a process it had started.

A difficult thing to start testing, as I have to find devices not checking in.

I might add to the script in the future to check fore signature error messages in the log, which is a solid indication a re-enrollment is needed, and then force a prompt to the end user to visit IT Support.

Here is the complete in its current V1.0, end-to-end, step-by-step guide for creating, packaging, and deploying your Jamf agent monitoring solution using Jamf Pro. This includes all the script and plist creation steps, packaging instructions, and Jamf Pro policy configuration, with all the crucial details like permissions and directory creation.


Overview of the Solution

You are creating a macOS Launch Daemon that will run a shell script periodically.

  • The Script: (check_jamf_agent.sh) will monitor the last update time of /var/log/jamf.log. If the log hasn't been updated for 3 hours, it will:
    1. Forcefully kill the primary Jamf background process (JamfDaemon).
    2. Forcefully kill any hanging /usr/local/jamf/bin/jamf command-line processes.
    3. Then, it will issue the sudo /usr/local/jamf/bin/jamf agent & command to initiate a full reset and restart of the Jamf client.
  • The Launch Daemon: (com.yourcompany.jamfagentmonitor.plist) will tell macOS to run this script every 1 hour.
  • Deployment: Both the script and the Launch Daemon will be packaged into a .pkg file and deployed via a Jamf Pro policy.

Section 1: Preparation on Your Admin Workstation (Creating Files & Setting Up for Packaging)

Perform these steps on a macOS machine that you use for Jamf administration.

Step 1.1: Create the Monitoring Script File

  1. Open Terminal on your admin workstation.

  2. Create the script file in its intended final location (/usr/local/bin/check_jamf_agent.sh):

    Bash
     
    sudo nano /usr/local/bin/check_jamf_agent.sh
    

    (You'll be prompted for your administrator password).

  3. Paste the following script content into the nano editor:

    Bash
     
    #!/bin/bash
    
    LOG_FILE="/var/log/jamf.log"
    JAMF_DAEMON_EXECUTABLE="/Library/Application Support/JAMF/Jamf.app/Contents/MacOS/JamfDaemon.app/Contents/MacOS/JamfDaemon"
    JAMF_CMD_EXECUTABLE="/usr/local/jamf/bin/jamf"
    LAST_UPDATE_THRESHOLD_SECONDS=$((3 * 60 * 60)) # 3 hours in seconds
    MONITOR_LOG="/var/log/jamf_agent_monitor.log" # Dedicated log for this script
    
    echo "$(date): --- Jamf Agent Monitor Check Started ---" >> "$MONITOR_LOG"
    
    # Get the last modification time of the jamf.log file
    # Using stat -f %m for macOS compatibility
    LAST_MOD_TIME=$(stat -f %m "$LOG_FILE" 2>/dev/null)
    
    if [ -z "$LAST_MOD_TIME" ]; then
        echo "$(date): ERROR: Could not get modification time for $LOG_FILE. Exiting." >> "$MONITOR_LOG"
        exit 1
    fi
    
    CURRENT_TIME=$(date +%s)
    TIME_DIFFERENCE=$((CURRENT_TIME - LAST_MOD_TIME))
    
    echo "$(date): jamf.log last updated $TIME_DIFFERENCE seconds ago." >> "$MONITOR_LOG"
    
    if [ "$TIME_DIFFERENCE" -gt "$LAST_UPDATE_THRESHOLD_SECONDS" ]; then
        echo "$(date): jamf.log has not updated in over 3 hours ($TIME_DIFFERENCE seconds). Initiating Jamf agent reset." >> "$MONITOR_LOG"
    
        # 1. Kill the JamfDaemon process (launchd will typically restart it automatically)
        JAMF_DAEMON_PID=$(pgrep -f "$JAMF_DAEMON_EXECUTABLE")
        if [ -n "$JAMF_DAEMON_PID" ]; then
            echo "$(date): Found JamfDaemon running with PID: $JAMF_DAEMON_PID. Attempting to kill." >> "$MONITOR_LOG"
            sudo kill "$JAMF_DAEMON_PID"
            sleep 3 # Give it a moment to terminate gracefully
            # Check if it was killed successfully, if not, force kill
            if pgrep -f "$JAMF_DAEMON_EXECUTABLE" > /dev/null; then
                echo "$(date): ERROR: JamfDaemon (PID: $JAMF_DAEMON_PID) did not terminate. Attempting kill -9." >> "$MONITOR_LOG"
                sudo kill -9 "$JAMF_DAEMON_PID"
                sleep 2
            fi
        else
            echo "$(date): JamfDaemon process not found running." >> "$MONITOR_LOG"
        fi
    
        # 2. Kill any lingering /usr/local/jamf/bin/jamf processes (these are usually temporary tasks that might be stuck)
        # Using ps aux and grep to find and exclude the daemon and grep itself, then awk for PID
        JAMF_CLI_PIDS=$(ps aux | grep "$JAMF_CMD_EXECUTABLE" | grep -v "$JAMF_DAEMON_EXECUTABLE" | grep -v "grep" | awk '{print $2}')
    
        if [ -n "$JAMF_CLI_PIDS" ]; then
            echo "$(date): Found Jamf CLI processes with PIDs: $JAMF_CLI_PIDS. Attempting to kill." >> "$MONITOR_LOG"
            for PID in $JAMF_CLI_PIDS; do
                echo "$(date): Killing CLI process $PID." >> "$MONITOR_LOG"
                sudo kill "$PID"
                sleep 1 # Small pause between kills
                if ps -p "$PID" > /dev/null; then # Check if process is still running
                    echo "$(date): ERROR: Jamf CLI process (PID: $PID) did not terminate. Attempting kill -9." >> "$MONITOR_LOG"
                    sudo kill -9 "$PID"
                fi
            done
            sleep 2 # Give all CLI processes a moment to terminate
        else
            echo "$(date): No Jamf CLI processes found running." >> "$MONITOR_LOG"
        fi
    
        # 3. Attempt to explicitly relaunch the Jamf agent for a full reset/initialization
        echo "$(date): Attempting to relaunch Jamf agent: $JAMF_CMD_EXECUTABLE agent." >> "$MONITOR_LOG"
        sudo "$JAMF_CMD_EXECUTABLE" agent &
        echo "$(date): Jamf agent relaunch command issued." >> "$MONITOR_LOG"
        echo "$(date): --- Jamf Agent Monitor Check Completed ---" >> "$MONITOR_LOG"
    
    else
        echo "$(date): jamf.log is updating within the threshold. No action needed." >> "$MONITOR_LOG"
        echo "$(date): --- Jamf Agent Monitor Check Completed ---" >> "$MONITOR_LOG"
    fi
    
    exit 0
    
  4. Save the script in nano: Press Ctrl+X, then Y (for Yes), then Enter.

  5. Make the script executable:

    Bash
     
    sudo chmod +x /usr/local/bin/check_jamf_agent.sh
    

Step 1.2: Create the Launch Daemon (.plist) File

  1. Open Terminal (if you closed it).

  2. Create the plist file in its intended final location (/Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist):

    Bash
     
    sudo nano /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
    

    (Replace com.yourcompany with your actual organization's reverse domain name for good practice, e.g., com.myorg.jamfagentmonitor).

  3. Paste the following XML content into the nano editor:

    XML
     
    <?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.yourcompany.jamfagentmonitor</string> <key>ProgramArguments</key>
        <array>
            <string>/usr/local/bin/check_jamf_agent.sh</string>
        </array>
        <key>StartInterval</key>
        <integer>3600</integer> <key>RunAtLoad</key>
        <true/> <key>StandardOutPath</key>
        <string>/var/log/jamf_agent_monitor_stdout.log</string> <key>StandardErrorPath</key>
        <string>/var/log/jamf_agent_monitor_stderr.log</string> <key>UserName</key>
        <string>root</string> <key>GroupName</key>
        <string>wheel</string>
    </dict>
    </plist>
    
  4. Save the plist in nano: Press Ctrl+X, then Y (for Yes), then Enter.

  5. Set the correct ownership and permissions for the plist file:

    Bash
     
    sudo chown root:wheel /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
    sudo chmod 644 /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
    

Step 1.3: Prepare Directories for Packaging

This step creates a temporary structure that pkgbuild will use as the source for your installer package.

  1. Open Terminal (if you closed it).

  2. Create a main temporary working directory for your package source:

    Bash
     
    mkdir ~/jamf_monitor_pkg
    
  3. Create the necessary subdirectories within your working directory that mirror the final installation paths:

    Bash
     
    mkdir -p ~/jamf_monitor_pkg/usr/local/bin
    mkdir -p ~/jamf_monitor_pkg/Library/LaunchDaemons
    
  4. Copy your newly created script and plist into these temporary directories:

    Bash
     
    cp /usr/local/bin/check_jamf_agent.sh ~/jamf_monitor_pkg/usr/local/bin/
    cp /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist ~/jamf_monitor_pkg/Library/LaunchDaemons/
    
  5. Crucially, create the empty directory for pkgbuild's --scripts flag:

    Bash
     
    mkdir /tmp/empty_scripts_dir
    
  6. (Highly Recommended) Verify the structure and permissions within your jamf_monitor_pkg:

    Bash
     
    ls -lR ~/jamf_monitor_pkg
    
    • Verify the script: Should have executable permissions (e.g., -rwxr-xr-x).
    • Verify the plist: Should have read/write for owner, read-only for others (e.g., -rw-r--r--).
    • Verify Ownership: pkgbuild will preserve the permissions of the source files. While the chown root:wheeland chmod 644 commands in Step 1.2 set them correctly on the original files, the cp command will copy them with your user's ownership. Don't worry, pkgbuild handles setting the root/wheel ownership during installation correctly for /Library/LaunchDaemons and /usr/local/bin. The important thing is the file permissions (-rwxr-xr-x, -rw-r--r--) are correct from the chmod commands.

Section 2: Create the PKG File

Now, you'll bundle these files into a deployable .pkg installer.

  1. Open Terminal (if you closed it).

  2. Run pkgbuild to create the package:

    Bash
     
    pkgbuild \
        --root ~/jamf_monitor_pkg \
        --install-location / \
        --scripts /tmp/empty_scripts_dir \
        --identifier com.yourcompany.jamfagentmonitor.pkg \
        --version 1.0 \
        ~/Desktop/JamfAgentMonitor.pkg
    
    • --root ~/jamf_monitor_pkg: Points to your source directory.
    • --install-location /: Tells the installer to place the contents of --root at the root of the target drive.
    • --scripts /tmp/empty_scripts_dir: Points to the empty directory for pkgbuild to satisfy its requirement.
    • --identifier com.yourcompany.jamfagentmonitor.pkg: A unique identifier for your package.
    • --version 1.0: The version number.
    • ~/Desktop/JamfAgentMonitor.pkg: The output path and filename for your .pkg file.

    If successful, you should see output ending with: pkgbuild: Wrote package to /Users/YOUR_USERNAME/Desktop/JamfAgentMonitor.pkg


Section 3: Upload the PKG to Jamf Pro

  1. Log in to your Jamf Pro web console.

  2. Navigate to Settings (gear icon in the top right) > Computer Management > Packages.

  3. Click the + New button.

  4. Select Upload a package.

  5. Click Choose File, then navigate to your Desktop and select JamfAgentMonitor.pkg.

  6. Fill in any other required metadata (e.g., "Display Name" like Jamf Agent Monitor PKG).

  7. Click Save. The package will be uploaded to your Jamf Pro Distribution Point(s).


Section 4: Create a Policy in Jamf Pro

This policy will deploy your package and ensure the Launch Daemon is activated on your target Macs.

  1. Log in to your Jamf Pro web console.

  2. Navigate to Computers > Policies.

  3. Click + New to create a new policy.

  4. Configure the General Payload:

    • Display Name: Give it a clear name, e.g., Deploy & Activate Jamf Agent Monitor
    • Category: Choose an appropriate category (e.g., "Utilities", "System Maintenance").
    • Trigger:
      • Enrollment Complete: Select this to ensure new machines automatically get the solution.
      • Recurring Check-in: Select this and set a frequency (e.g., Once every day). This is crucial for ensuring existing machines get the policy, and if the Launch Daemon ever gets accidentally unloaded, it gets reloaded during a regular check-in.
    • Execution Frequency: Set to Once per computer. The package only needs to be installed once, and the Launch Daemon will run on its own schedule. (I set it to per day, incase the end user removes it, it will put it self back).
  5. Configure the Packages Payload:

    • Click Configure next to "Packages."
    • Click Add and select your JamfAgentMonitor.pkg from the list.
    • Action: Ensure it's set to Install.
  6. Configure the Files and Processes Payload:

    • This is crucial for loading the Launch Daemon after the package is installed, making it active.
    • Click Configure next to "Files and Processes."
    • Under Execute Command, add the following command:
      Bash
       
      launchctl load -w /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
      
      • The launchctl load command tells macOS to register and start the daemon.
      • The -w flag writes the changes to the system's launchd preferences, ensuring the daemon persists across reboots and is loaded automatically by launchd in the future.
      • This command will run as root when executed by the Jamf policy.
  7. Configure the Scope:

    • Define which computers or computer groups should receive this policy.
    • CRITICAL: Start with a small, isolated test group (e.g., your own test machine, a few non-critical devices) to thoroughly verify its behavior before deploying widely across your fleet.
  8. Click Save Policy.


Section 5: Verification After Deployment

After the policy runs on a target macOS machine:

  1. Check for the script's presence and permissions: Open Terminal on the target Mac and run:

    Bash
     
    ls -l /usr/local/bin/check_jamf_agent.sh
    # Expected output should show executable permissions, e.g., -rwxr-xr-x 1 root wheel ...
    
  2. Check for the plist's presence and permissions:

    Bash
     
    ls -l /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
    # Expected output should show proper ownership and permissions, e.g., -rw-r--r-- 1 root wheel ...
    
  3. Verify the daemon is loaded by launchd:

    Bash
     
    sudo launchctl list | grep jamfagentmonitor
    # You should see an entry like: PID -   com.yourcompany.jamfagentmonitor
    # The PID indicates it's running. The '-' means it's not currently active (it only runs on schedule).
    
  4. Check the logs generated by your script:

    Bash
     
    tail -f /var/log/jamf_agent_monitor.log
    
    • Wait for up to 1 hour (your StartInterval) for the script to run for the first time automatically.
    • To force an immediate test run of the daemon (and see output in the log instantly), run:
      Bash
       
      sudo launchctl start com.yourcompany.jamfagentmonitor
      
      Then check tail -f /var/log/jamf_agent_monitor.log again.

By following these detailed steps, you should be able to successfully deploy your Jamf agent monitoring solution.

sdagley
Esteemed Contributor III

@Malcolm When running a script from a. LaunchDaemon it's already running as a privileged process so there's no need to use sudo before commands needing privileges in the script unless you're trying to change what process they're running as.

The JamfDaemon process is initiated by a LaunchDaemon with the KeepAlive key so it will automatically be relaunched every time you use the kill command on it. If you really want to stop it you'd also want to use the following to unload that LaunchDaemon:

/bin/launchctl bootout system /Library/LaunchDaemons/com.jamf.management.daemon.plist

Likewise you would not simply re-launch the process, you'd use the bootstrap option with launchctl:

/bin/launchctl bootstrap system /Library/LaunchDaemons/com.jamf.management.daemon.plist 

The need to use bootcut and bootstrap also applies to the JamfAgent process which is managed by the com.jamf.management.startup.plist LaunchDaemon.