Skip to content

CampusTech/jamf-fleet-migration-webhook

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jamf-fleet-migration-webhook

Receives Fleet's macOS migration webhook and automatically unmanages the device in Jamf Pro — removing the MDM profile while keeping the inventory record intact.

Built because Fleet ships the migration prompt and webhook but expects you to wire up the Jamf side yourself. This is the Jamf side.

How it works

  1. Fleet shows the migration prompt to the end user (forced or voluntary mode)
  2. User clicks Start — Fleet POSTs a JSON payload with the device's serial number to your webhook URL
  3. This server looks up the device in Jamf by serial number (GET /api/v1/computers-inventory)
  4. Calls POST /api/v1/computer-inventory/{id}/remove-mdm-profile — Jamf queues the unmanage command, the MDM profile is removed on the device's next check-in, and inventory is kept
  5. macOS notices it is no longer MDM-enrolled with Jamf and accepts Fleet's MDM profile

The server always returns HTTP 200 to Fleet regardless of Jamf outcome — Fleet Desktop surfaces non-2xx responses as a user-facing migration error even though Fleet's own flow continues regardless of webhook result.

Setup

Prerequisites

  • Jamf Pro with an API role and OAuth2 client (Settings → System → API Roles and Clients)
    • Required role privileges: Read Computers and Send Computer Unmanage Command
  • Go 1.21+ or download a release binary
  • ngrok account (free tier works) if you need a public URL temporarily

Fleet configuration caveat

In Fleet GitOps, controls.macos_migration is read from the global app config (default.yml), not per-team. The Fleet UI page is labeled "All teams" with no team selector. Configure it once in default.yml:

controls:
  macos_migration:
    enable: true
    mode: forced  # or voluntary
    webhook_url: https://your-host.example.com/webhook/<your-secret>

If your repo already has controls blocks in team YAMLs and GitOps rejects the global one, this is the order of operations — the migration setting cannot be set in a team file even though it appears to apply to all hosts.

Running

Create a config.yaml next to the binary (this file is gitignored):

jamf:
  url: https://yourorg.jamfcloud.com
  client_id: cae2dfb4-1be8-4081-8aa4-a6bc62313ec9
  client_secret: NSdhnEm2ecX-...

ngrok:
  authtoken: 2khh0JVuL17l...
  domain: my-migrator.ngrok.app   # optional — fixed subdomain

webhook:
  secret: a-long-random-string    # webhook serves at /webhook/<secret>

Then:

go run github.com/CampusTech/jamf-fleet-migration-webhook@latest serve

# or with the release binary:
./jamf-fleet-migration-webhook serve

Copy the printed URL into Fleet's webhook_url setting.

Configuration sources

All settings can be provided via (in order of precedence, highest first):

  1. CLI flag (--jamf-url ...)
  2. Environment variable (JAMF_URL=...)
  3. config.yaml in the working directory

Nested YAML keys map to flat env vars with _ separators: jamf.urlJAMF_URL, ngrok.authtokenNGROK_AUTHTOKEN, webhook.secretWEBHOOK_SECRET.

Flags

Flag Env var Config key Default Description
--jamf-url JAMF_URL jamf.url required Jamf Pro base URL
--jamf-client-id JAMF_CLIENT_ID jamf.client_id required OAuth2 client ID
--jamf-client-secret JAMF_CLIENT_SECRET jamf.client_secret required OAuth2 client secret
--port PORT port 8080 Port to listen on (when not using ngrok)
--ngrok ngrok.enabled false Enable ngrok (auto-enabled if NGROK_AUTHTOKEN or NGROK_DOMAIN is set)
--ngrok-token NGROK_AUTHTOKEN ngrok.authtoken ngrok auth token
--ngrok-domain NGROK_DOMAIN ngrok.domain Fixed ngrok domain, e.g. myhost.ngrok-free.app
--secret WEBHOOK_SECRET webhook.secret Secret path segment: webhook served at /webhook/<secret>
--debug DEBUG debug false Enable verbose debug logging (request/response bodies, decision points)

Security notes

  • Always set a webhook.secret in production. Fleet's migration webhook is unauthenticated by design — anyone who discovers your URL can fire fake "user clicked Start" requests at it and trigger unmanage on real devices. The secret path turns the URL into a bearer credential.
  • Fleet's payload contains hardware_serial, host.id, and host.uuid — device inventory data. If you're concerned about the data transiting ngrok or other tunnels, terminate TLS on infrastructure you own.
  • Rotate the Jamf OAuth client secret periodically.

Debug logging

Enable with --debug or DEBUG=true. Includes:

  • Inbound webhook request method, path, remote IP, and raw body
  • Jamf OAuth2 token cache hits/misses and expiry
  • Full Jamf request URL and response status + body for inventory lookup and unmanage calls
  • Decision points in the handler (lookup failed, unmanage failed, etc.)

Useful when bringing the integration up for the first time or diagnosing why a specific device didn't migrate.

License

MIT — Campus, Inc.

About

Receives Fleet macOS migration webhooks and unmanages devices in Jamf Pro

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages