Repository navigation
Security Cloud Workflows
Copy-paste recipes for Jamf Security Cloud administration. For command details, see Jamf Security Cloud Commands. For shared patterns (scaffold, --set, dry-run, destructive confirmation), see CLI Patterns.
Two halves, two credentials. The Radar APIs (risk, device-lifecycle, stream, status, verification, well-known, jwks) take the three scoped application pairs jamf-cli security setup writes. The Jamf Platform gateway serves everything else (dns-*, ztna-*, content-categories, device-groups, uem-*, enrollment-activation-profiles), and those take a platform gateway credential, from jamf-cli platform setup. Each recipe below says which it needs.
# Every command, with the API that serves it
jamf-cli commands -o json | jq -r '.[] | select(.product=="security") | .command + " " + (.api // "")'
# The gateway-served half only
jamf-cli commands -o json | jq -r '.[] | select(.product=="security" and .api=="platform-gateway") | .command'jamf-cli security --help and jamf-cli security <TAB> carry the same fact per command, as (Security Cloud · platform gateway) or (Security Cloud · Radar API). Seven resources carry the Radar marker: risk, device-lifecycle, stream, status, verification, jwks, well-known. Everything else is gateway-served.
Both setup commands merge into the profile they name, so run both against one name for a profile that reaches everything.
# Radar half: interactive wizard, one application ID/secret pair per API,
# leave an application ID blank to skip that API
jamf-cli security setup --profile-name jsc
# Platform gateway half: region, client credentials, and the scope the
# integration was created at (environment is the level to prefer)
jamf-cli platform setup --profile-name jsc
jamf-cli -p jsc security risk list # Radar
jamf-cli -p jsc security dns-zones list # gatewayplatform setup validates the credentials with a token exchange and probes the scope ID you typed, since a mis-pasted environment ID saves without error and then refuses every command. It closes by reporting which Platform API resource groups your credential's level declares. Permissions are granted per operation, so a 403 is what names the one you are missing.
Non-interactive (CI):
# Radar half
export JAMFSECURITY_RISK_CLIENT_ID="..."
export JAMFSECURITY_RISK_CLIENT_SECRET="..."
export JAMFSECURITY_SSE_CLIENT_ID="..."
export JAMFSECURITY_SSE_CLIENT_SECRET="..."
jamf-cli security risk list
# Gateway half: the ordinary platform variables. The gateway HOST is what
# selects platform auth; there is no auth-method environment variable.
export JAMF_URL="https://eu.api.jamfcloud.com"
export JAMF_CLIENT_ID="..."
export JAMF_CLIENT_SECRET="..."
export JAMF_ENVIRONMENT_ID="<platform-environment-id>" # or JAMF_TENANT_ID, never both
jamf-cli security dns-zones listSet both halves of a Radar pair or neither: an ID from the environment with the secret coming from a profile is refused. JAMF_ENVIRONMENT_ID and JAMF_TENANT_ID are mutually exclusive. See Configuration & Profiles#Jamf Security Cloud.
Scope level for the gateway half. Prefer environment scope: one EU environment credential reaches every gateway-served Security Cloud command, with content categories, DNS, ZTNA, UEM Connect and device groups all answering 200. Tenant scope is the legacy level, a tenant credential lacking the Security Cloud grants answers
403 BAD_PERMISSIONS, anddevice-groupshits a gateway fault under it. See Managing Device Groups.
Radar half: Risk credentials.
# List device risk status (JSON by default)
jamf-cli security risk list
# Table view
jamf-cli security risk list -o table
# Export the full risk report to CSV
jamf-cli security risk list -o csv --out-file device-risk.csv
# Look up one device by guid
jamf-cli security risk list --guid <device-guid>
# Look up one device by externalId
jamf-cli security risk list --external-id <external-id>Radar half: Risk credentials.
# 1. Generate the request-body template
jamf-cli security risk override --scaffold > override.json
# 2. Edit override.json: set deviceIds, risk, and source
# (deviceIds has no element schema upstream, so the scaffold renders it as [])
# 3. Apply the override
jamf-cli security risk override --from-file override.jsonRadar half: Device Lifecycle credentials. The most destructive command in the CLI.
Purge requires --yes or an interactive confirmation. The --dry-run preview prints before any confirmation prompt, so previewing does not need --yes.
# 1. Build the purge body from the scaffold
jamf-cli security device-lifecycle purge --scaffold > purge.json
# 2. Edit purge.json: fill externalIds and/or guids
# 3. Preview (method, resolved path and body to stderr; nothing is sent)
jamf-cli security dl purge --from-file purge.json --dry-run
# 4. Execute
jamf-cli security dl purge --from-file purge.json --yesjamf-cli fills the customer ID in from the Device Lifecycle credentials at request time. A body that scopes nothing is refused before the preview.
Radar half: Shared Signals & Events credentials.
# 1. Inspect the current stream configuration
jamf-cli security stream get
# 2. Scaffold, edit (aud, delivery, events_requested, format...), and apply
jamf-cli security stream update --scaffold > stream.json
# ... edit stream.json ...
jamf-cli security stream update --from-file stream.json
# 3. Check the stream status
jamf-cli security status get
# 4. Trigger a verification event to confirm end-to-end delivery
jamf-cli security verification trigger
# 5. Inspect the transmitter's discovery document and signing keys
jamf-cli security well-known get
jamf-cli security jwks get# Update the stream status (status/reason values per the SSE framework)
jamf-cli security status update --set status=<status> --set reason="maintenance window"# Delete the stream configuration (destructive)
jamf-cli security stream delete --yesGateway half: a platform gateway credential.
# 1. Read the current zones
jamf-cli security dns-zones list -o table
# 2. Scaffold the zone body
jamf-cli security dns-zones create --scaffold > zone.json{
"domains": ["corp.example.com"],
"name": "corp-internal",
"nameServers": [
{ "gatewayId": "gw-eu-01", "ip": "198.51.100.53" }
]
}# 3. Preview, then create
jamf-cli security dns-zones create --from-file zone.json --dry-run
jamf-cli security dns-zones create --from-file zone.json
# 4. Read it back by ID, or by the name you gave it
jamf-cli security dns-zones get --name corp-internal
# 5. Adjust later with a merge patch: only what you send changes
jamf-cli security dns-zones patch --name corp-internal --set name=corp-internal-euTwo rules the spec does not state: nameServers[].ip must be a public address (10.0.0.53 is refused 422 NAMESERVER_IP_RESTRICTED, 198.51.100.53 is accepted), and gatewayId must name a gateway that exists (an unknown one is 422 GATEWAY_NOT_FOUND). domains takes 1–100 entries and nameServers 1–20; list does not paginate and --sort accepts only name:asc / name:desc.
# Tenant search domain (a singleton). get renders {} and exits 0 when none is set.
jamf-cli security dns-search-domains get
jamf-cli security dns-search-domains update --set suffix=corp.example.com
# Custom hostname mappings: a whole-list replace whose body is a top-level
# JSON array, so --set cannot express it.
# NOTE: get returns the collection envelope {results, totalCount} while update
# wants the bare array, so unwrap .results on the way through.
jamf-cli security dns-custom-hostname-mappings get | jq '.results' > mappings.json
# ... edit mappings.json: it replaces the entire set, max 500 entries ...
jamf-cli security dns-custom-hostname-mappings update --from-file mappings.jsonGateway half: a platform gateway credential.
# 1. Pick a category. categoryName is REQUIRED and the scaffold renders it
# empty, which is the one value the server rejects.
jamf-cli security content-categories list -o table
# 2. Optional: start from a predefined SaaS definition instead of hostnames
jamf-cli security ztna-predefined-apps list -o table
# 3. Scaffold and edit
jamf-cli security ztna-apps create --scaffold > app.jsonA minimal custom app, routed through a gateway:
{
"name": "Internal CRM",
"categoryName": "Uncategorized",
"hostnames": ["crm.example.com"],
"routing": { "type": "CUSTOM", "gatewayId": "a1b2" },
"assignments": { "inclusions": { "allUsers": true } }
}# 4. Create. This returns {id, href}, NOT the created app
jamf-cli security ztna-apps create --from-file app.json
# 5. So read it back for the fields
APP_ID=$(jamf-cli security ztna-apps create --from-file app.json --field id)
jamf-cli security ztna-apps get "$APP_ID"
# 6. Tighten it later. patch is all-optional
jamf-cli security ztna-apps patch "$APP_ID" \
--set security.riskControls.enabled=true \
--set security.riskControls.levelThreshold=HIGHTraps, wire-verified and invisible in the response:
-
categoryName: ""is refused (400 [INVALID_FIELD] categoryName: must not be blank). A category that does not exist is409 MISSING_CATEGORY_NAME. -
Setting
predefinedAppIdinheritsnameandhostnamesfrom the template, and the server drops anameyou send:201, and the app reads backname: null. Those apps are unreachable by--name, so address them by ID. Reusing a template another app already holds is409 CONFLICT. -
The server accepts an unknown key under
securityand drops it.--set security.riskControls.enabld=trueanswers 201 and does nothing. -
--helplists the allowed enum values, which the scaffold renders as"".
# Jamf-managed shared gateways available to this tenant (id and name only)
jamf-cli security ztna-shared-gateways list -o table
# Dedicated gateways
jamf-cli security ztna-gateways list -o table
jamf-cli security ztna-gateways create --scaffold > gw.json
jamf-cli security ztna-gateways create --from-file gw.json
# A partial IPSec change, deep-merged server-side, so send only what changes.
# Note: patch prints NOTHING on success (204, no body).
jamf-cli security ztna-gateways patch <gatewayId> --set ipsec.esp.lifetimeInSec=14400
jamf-cli security ztna-gateways get <gatewayId>
# Group dedicated gateways for failover / nearest routing
jamf-cli security ztna-grouped-gateways create --from-file group.json-
ipsec.left.subnetsaccepts one element;right.subnetsaccepts several. Two left subnets earn400 [INVALID_FIELD] field=ipsec "IPSec configuration is not valid.", naming neither the field nor the cardinality. Left subnets must be private. -
ipsec.right.vendoris case-sensitive:"cisco"for"Cisco"is a 400 with nofieldand a generic "Request body is missing or malformed." -
contactis required (400 … contact: must not be null). -
ztna-grouped-gateways createrequiresrecoveryDelayInSec, an enum of300,1800,3600,10800,28800. The server rejects0under every routing strategy. - All three ZTNA creates return
{id, href}.
Gateway half: a platform gateway credential.
A tenant holds at most one connector, whatever its vendor. Check before creating: a second create answers 409 CONNECTOR_CONFIG_ALREADY_EXISTS.
# 1. Is there one already?
jamf-cli security uem-connectors list -o table
# 2. Scaffold. The body is a discriminated union over ten vendor shapes keyed
# on "vendor"; --scaffold renders the Jamf Pro branch, the only wire-verified
# one. --help's "Allowed values" for authStrategy spans EVERY variant and is
# wider than the shape you are looking at.
jamf-cli security uem-connectors create --scaffold > connector.jsonEdit it down to one credential shape. The scaffold's deviceSyncAuth carries the union of the OAuth and BASIC pairs, so delete the half your authStrategy does not use:
{
"vendor": "JAMF_PRO",
"authStrategy": "JAMF_PRO_OAUTH",
"url": "https://yourcompany.jamfcloud.com",
"isoCountry": "US",
"deviceSyncAuth": {
"clientId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"clientSecret": "…"
}
}# 3. Create. Keep the secret in the file, never in --set, which puts it in
# shell history and ps output.
jamf-cli security uem-connectors create --from-file connector.json
# The connector read model carries no name, so --name cannot resolve one.
# A tenant holds at most one connector, so the id off the list is the id.
CONFIG_ID=$(jamf-cli security uem-connectors list --field id)
# 4. Turn synchronization on (idempotent)
jamf-cli security uem-connector-enablement enable "$CONFIG_ID" --set enabled=true
# 5. Sync settings. `update` is a FULL replacement, so start from the current
# values. `get` returns the whole connector config, a wider shape than
# `update` accepts, so read the values off it and build the body from the
# scaffold; piping one into the other fails.
jamf-cli security uem-sync-settings get "$CONFIG_ID"
jamf-cli security uem-sync-settings update --scaffold > settings.json
# ... edit settings.json: the ten fields update accepts, at their current
# values, plus whatever you are changing ...
jamf-cli security uem-sync-settings update "$CONFIG_ID" --from-file settings.json
# 6. Kick off a sync (202 Accepted, it runs in the background) and watch it
jamf-cli security uem-sync trigger "$CONFIG_ID"
jamf-cli security uem-sync list "$CONFIG_ID" -o table
# 7. Cancel a run in flight (best-effort, it may finish first)
jamf-cli security uem-sync cancel "$CONFIG_ID"Before running this in a loop:
-
authStrategy: M2Mtakes atenantIdand no credentials: Security Cloud self-provisions what it needs on the named Jamf Pro, andurlis then ignored. The Jamf Pro API role and integration it provisions survive the connector'sDELETE, so repeated create/delete cycles leak an enabled integration per create onto the target instance, with nothing in the connector response pointing at one. List them withjamf-cli pro api-integrations liston an instance (oauth2) profile; a gateway profile refuses that command. -
Error ladder on create: no
authStrategy→500 INTERNAL_ERROR;authStrategywith nodeviceSyncAuth→422 VALIDATION_FAILED ": invalid auth configuration for Jamf PRO"; a complete body when a connector already exists →409 CONNECTOR_CONFIG_ALREADY_EXISTS. -
refreshRateMinutesenforces its enum:60,120,240,480,720,1440.360,59,1and0are all422 VALIDATION_FAILED; the message leaks the accepted set and leavesfieldnull. -
The server discards
deviceUnmanagedThresholdforJAMF_PROconnectors. It answers204and reads back0whatever you sent, while--helplists0, 1, 3, 5, 7, 14as if it applied. A grace period set on a Jamf Pro connector does not exist. -
The body's
vendormust match the connector's stored vendor: a mismatch is422 VENDOR_MISMATCH. - A connector read returns
clientIdandusernameand no secret, so aJAMF_PRO_OAUTHconnector cannot be re-created from its own output. AnM2Mone can.
Gateway half: a platform gateway credential. Alias:
eap.
# 1. List existing profiles. --origin is required and PUBLIC_API is its only legal value.
jamf-cli security eap list --origin PUBLIC_API
# 2. Optionally scope the profile to a device group
jamf-cli security device-groups list -o table
# 3. Scaffold and edit
jamf-cli security eap create --scaffold > profile.json{
"name": "iOS BYOD onboarding",
"origin": "PUBLIC_API",
"platforms": ["iOS"],
"groupId": "grp-abc123",
"capabilities": {
"dataPolicy": true,
"networkSecurity": true,
"vulnerabilityManagement": true,
"note": "BYOD pilot"
}
}# 4. Create. The response is {"code": "…"} and the code is the only handle
CODE=$(jamf-cli security eap create --from-file profile.json --field code)
# 5. Deploy the Jamf Security configuration profiles to the UEM for that code
# (the last step of automated UEM Connect onboarding)
jamf-cli security uem-activation-profiles deploy-to-uem "$CODE" \
--set platform=SUPERVISED_IOS --set uem=JAMF
# 6. Take it out of service without deleting it (204, prints nothing)
jamf-cli security eap pause "$CODE"
jamf-cli security eap resume "$CODE"
# 7. Delete a batch (destructive; codes is an array)
jamf-cli security eap delete-multiple --set codes="[\"$CODE\"]" --yesCaveats specific to this resource:
-
The read model is a
codeand nothing else.getreturns{"code": "…"}and omits the name, platforms, capabilities and state you sent, so a create's input cannot be read back, andpause/resumehave no observable effect on any GET. -
delete-multipleis a soft delete the read surface does not reflect. After it answers204,get <code>still answers200andliststill returns the code;pauseon a deleted code is409 STATE_CONFLICT("… is already deleted."). It also skips a code it cannot delete (non-existent, restricted by origin, or in use by an unfinished device transition) and still answers204, so a204is not evidence anything was deleted. -
capabilities.networkSecurityandcapabilities.vulnerabilityManagementmust be both on or both off:400 [INVALID_FIELD] capabilities: networkSecurity and vulnerabilityManagement must both be enabled or both disabled. -
groupIdis not validated. A mistyped group answers201with the profile scoped to nothing. -
platformsacceptsiOSandMAConly, and the server applies the 1–2 limit after de-duplication, so["iOS","MAC","iOS"]is accepted.
Gateway half: a platform gateway credential, and environment-scoped for this resource (see the fault below).
jamf-cli security device-groups list -o table
jamf-cli security device-groups create --set name="Engineering Team"
jamf-cli security device-groups get --name "Engineering Team"
jamf-cli security device-groups update <groupId> --set name="Engineering"
jamf-cli security device-groups delete <groupId> --yes
# Idempotent create-or-update, keyed on the name in the body
jamf-cli security device-groups apply --set name="Engineering Team" --yesA group carries a name and nothing else, so update changes the name and prints nothing on success: it sends PUT /securitycloud/v2/groups/{groupId}, which is a 204 with no body. Read the change back with list. The default group cannot be deleted.
The implicit "Default Group" has no ID.
listreturns it as{"name": "Default Group"}with noid, since it is not a stored group; a group you create carries itsidand resolves by--name.applyon that one name refuses instead of creating a second group with it:found 1 item(s) named "Default Group" in /securitycloud/v2/groups, but the list returns no ID for them; identify the item by ID instead
Under tenant scope, device-groups list answers a gateway templating error:
[URI_NOT_TEMPLATED] Failed to (fully) template URI to the downstream service.
See logs from the request templater.
on GET /securitycloud/v2/groups, exit code 1, re-confirmed 2026-09-11 against the same tenant whose environment-scoped credential returns the group list from that same v2 path. The path templates under X-Environment-Id and fails under X-Tenant-Id. Use an environment-scoped credential; this is an upstream gateway fault.
Run the same read across several profiles with multi.
# Risk report across all security profiles (Radar half)
jamf-cli multi --filter 'security-*' -- security risk list -o table
# DNS zones across every gateway profile (gateway half)
jamf-cli multi --filter 'jsc-*' -- security dns-zones list -o table- Output defaults to JSON, so pipe to
jqor use--fieldto extract one value. An empty list prints[], nevernull. See Output Formats. -
Writes take
--from-file(JSON or YAML), piped stdin, and/or--set key=value. Leave--from-fileoff and the body is read from stdin.--setoverlays onto whichever supplied the body, decodes JSON literals (--set codes='["a","b"]'), and errors when a path segment already holds a non-object. A piped body over 10MB is refused, so use--from-filefor one that size. -
applyis create-or-update keyed on thenamein the body, ondns-zones,ztna-apps,ztna-gateways,ztna-grouped-gatewaysanddevice-groups. The exists check and the create are separate requests and no spec here declares a name-uniqueness409, so two jobs racing on the same absent name can both create one: serialiseapplyper resource in a pipeline that can retry. A failed lookup (auth error, 5xx) aborts. -
--select,--compact,--out-file,--quietand--no-hintswork on every command in the namespace. -
-n, --dry-runprints the method, resolved path and body to stderr and sends nothing. The preview comes before the confirmation prompt, so-nworks on destructive commands without--yes. Never pair-n --yesin a script: the day-nfalls off the command line, the confirmation is already suppressed. - Destructive commands (
device-lifecycle purge,stream delete, every gatewaydelete,eap delete-multiple) refuse to run under--no-inputwithout--yes. See CLI Patterns#Destructive Operations. - Each Radar API is independent, so a recipe touching only Risk works with SSE credentials never configured. A command for an unconfigured Radar API names that API and exits 1 (
list: Risk API credentials are not configured; run 'jamf-cli security setup'). A gateway-served command with no platform credentials fails withthis command requires platform gateway authand aconfig add-profileline. -
createon ZTNA apps, gateways and grouped gateways returns{id, href}, so capture--field idand follow with agetwhen the script needs the object. -
A gateway-half
403names the permission in Jamf Account's own words and exits 5, for examplegrant the Jamf Platform API integration these permissions in Jamf Account: Secure enterprise access > Content categories: Read (content-categories:read), with the permissions-map URL, since you search the picker by name. DNS zones report Zero-Trust Network Access (ZTNA): Read (ztna:read): they need theztnagrant and have nodns:one. Exit codes, including 8 (refused by policy) and 5 (a permission denial), are in Error Handling & Exit Codes.
See Jamf Security Cloud Commands for the full reference, Platform API GA Migration for the gateway (regions, scope levels, the GA base URL), Jamf Platform Commands for the rest of the platform namespace, and Setup Guide for first-run walkthroughs.
Repository · Issues · Releases
jamf-cli Wiki
- Home
- Community
- Getting Started
- CLI Reference
- Product Commands
- Workflows
- Configuration
- Reference
Products
- Jamf Pro:
jamf-cli pro - Jamf Platform API:
jamf-cli pro(blueprints, benchmarks, DDM reports) - Jamf Platform:
jamf-cli platform(AI Governance, Jamf Account, audit) - Jamf Protect:
jamf-cli protect - Jamf School:
jamf-cli school - Jamf Security Cloud:
jamf-cli security