Posted on 05-26-2025 07:48 PM
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.
Posted on 05-26-2025 08:16 PM
@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.
05-26-2025 10:53 PM - edited 05-26-2025 10:54 PM
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.
Posted on 05-27-2025 05:25 AM
@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.
Posted on 05-26-2025 08:24 PM
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.
Posted on 05-26-2025 10:49 PM
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
05-27-2025 06:58 AM - edited 05-27-2025 07:01 AM
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?
Posted on 05-27-2025 11:25 AM
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.
Posted on 05-27-2025 12:32 PM
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
Posted on 05-27-2025 07:52 AM
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.
Posted on 05-27-2025 04:12 PM
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>
Posted on 06-01-2025 07:41 PM
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.
Posted on 06-01-2025 10:20 PM
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.
You are creating a macOS Launch Daemon that will run a shell script periodically.
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:
JamfDaemon
)./usr/local/jamf/bin/jamf
command-line processes.sudo /usr/local/jamf/bin/jamf agent &
command to initiate a full reset and restart of the Jamf client.com.yourcompany.jamfagentmonitor.plist
) will tell macOS to run this script every 1 hour..pkg
file and deployed via a Jamf Pro policy.Perform these steps on a macOS machine that you use for Jamf administration.
Open Terminal on your admin workstation.
Create the script file in its intended final location (/usr/local/bin/check_jamf_agent.sh
):
sudo nano /usr/local/bin/check_jamf_agent.sh
(You'll be prompted for your administrator password).
Paste the following script content into the nano
editor:
#!/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
Save the script in nano
: Press Ctrl+X
, then Y
(for Yes), then Enter
.
Make the script executable:
sudo chmod +x /usr/local/bin/check_jamf_agent.sh
Open Terminal (if you closed it).
Create the plist file in its intended final location (/Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
):
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
).
Paste the following XML content into the nano
editor:
<?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>
Save the plist in nano
: Press Ctrl+X
, then Y
(for Yes), then Enter
.
Set the correct ownership and permissions for the plist file:
sudo chown root:wheel /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
sudo chmod 644 /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
This step creates a temporary structure that pkgbuild
will use as the source for your installer package.
Open Terminal (if you closed it).
Create a main temporary working directory for your package source:
mkdir ~/jamf_monitor_pkg
Create the necessary subdirectories within your working directory that mirror the final installation paths:
mkdir -p ~/jamf_monitor_pkg/usr/local/bin
mkdir -p ~/jamf_monitor_pkg/Library/LaunchDaemons
Copy your newly created script and plist into these temporary directories:
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/
Crucially, create the empty directory for pkgbuild
's --scripts
flag:
mkdir /tmp/empty_scripts_dir
(Highly Recommended) Verify the structure and permissions within your jamf_monitor_pkg
:
ls -lR ~/jamf_monitor_pkg
-rwxr-xr-x
).-rw-r--r--
).pkgbuild
will preserve the permissions of the source files. While the chown root:wheel
and 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.Now, you'll bundle these files into a deployable .pkg
installer.
Open Terminal (if you closed it).
Run pkgbuild
to create the package:
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
Log in to your Jamf Pro web console.
Navigate to Settings (gear icon in the top right) > Computer Management > Packages.
Click the + New button.
Select Upload a package.
Click Choose File, then navigate to your Desktop and select JamfAgentMonitor.pkg
.
Fill in any other required metadata (e.g., "Display Name" like Jamf Agent Monitor PKG
).
Click Save. The package will be uploaded to your Jamf Pro Distribution Point(s).
This policy will deploy your package and ensure the Launch Daemon is activated on your target Macs.
Log in to your Jamf Pro web console.
Navigate to Computers > Policies.
Click + New to create a new policy.
Configure the General Payload:
Deploy & Activate Jamf Agent Monitor
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.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).Configure the Packages Payload:
JamfAgentMonitor.pkg
from the list.Install
.Configure the Files and Processes Payload:
launchctl load -w /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
launchctl load
command tells macOS to register and start the daemon.-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.root
when executed by the Jamf policy.Configure the Scope:
Click Save Policy.
After the policy runs on a target macOS machine:
Check for the script's presence and permissions: Open Terminal on the target Mac and run:
ls -l /usr/local/bin/check_jamf_agent.sh
# Expected output should show executable permissions, e.g., -rwxr-xr-x 1 root wheel ...
Check for the plist's presence and permissions:
ls -l /Library/LaunchDaemons/com.yourcompany.jamfagentmonitor.plist
# Expected output should show proper ownership and permissions, e.g., -rw-r--r-- 1 root wheel ...
Verify the daemon is loaded by launchd
:
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).
Check the logs generated by your script:
tail -f /var/log/jamf_agent_monitor.log
StartInterval
) for the script to run for the first time automatically.sudo launchctl start com.yourcompany.jamfagentmonitor
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.
Posted on 06-02-2025 05:45 AM
@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.