Skip to content

Jamf Security Cloud Commands

Neil Martin edited this page Oct 3, 2026 · 8 revisions

Jamf Security Cloud Commands

Jamf Security Cloud commands live under the security namespace (alias sec). For shared CLI patterns (scaffold, --set, dry-run, destructive confirmation), see CLI Patterns.

jamf-cli security <resource> <action> [flags]

Two halves, two credentials

Two APIs serve Jamf Security Cloud. They take different credentials, and one profile can carry both.

Half Marker in --help Resources Served on Credentials from
Radar API (Security Cloud · Radar API) risk, device-lifecycle, stream, status, verification, well-known, jwks (seven, and no others) api.wandera.com / sse.jamf.com jamf-cli security setup (three scoped application ID/secret pairs)
Platform gateway (Security Cloud · platform gateway) everything else: dns-*, ztna-*, content-categories, device-groups, uem-*, enrollment-activation-profiles https://{region}.api.jamfcloud.com/securitycloud/… jamf-cli platform setup (a platform gateway credential)

Each command names its half in its one-line description, which is also what security <TAB> shows:

jamf-cli security --help
#   dns-zones          Manage dns-zones (Security Cloud · platform gateway)
#   risk               Manage risk (Security Cloud · Radar API)

The command catalog carries the same fact as an api field (platform-gateway or radar), for scripts:

jamf-cli commands -o json | jq -r '.[] | select(.product=="security") | .command + "  " + (.api // "")'

# The gateway-served half
jamf-cli commands -o json | jq -r '.[] | select(.product=="security" and .api=="platform-gateway") | .command'

security setup carries no marker: it writes credentials and calls no API.

Authentication Model

The Radar half

Each Radar API is its own "Security Integration" under Settings > Security Integrations in the Radar portal, with its own application ID/secret pair: one for Risk, one for Device Lifecycle, one for Shared Signals & Events. The JWT a pair returns is scoped to that API alone.

jamf-cli tracks the three pairs independently, so configure whichever you have access to. A command for an unconfigured API names that API and exits 1:

{
  "error": "general",
  "exitCode": 1,
  "exitCodeName": "general",
  "message": "list: Risk API credentials are not configured; run 'jamf-cli security setup'"
}

Device Lifecycle and SSE produce the equivalent message for themselves. A profile carrying only the gateway credential runs the gateway-served commands and reports which Radar pair is missing when you reach for a Radar command.

There is no per-tenant URL to configure: the three APIs share one production host, and tenancy travels inside the credentials. Host overrides exist for regional or sandbox hosts; see Configuration & Profiles#Jamf Security Cloud.

The platform gateway half

The DNS, ZTNA, content-category, device-group, UEM Connect and enrollment resources are served on the Jamf Platform gateway and need a platform gateway credential (an API integration created in Jamf Account) instead of the Radar pairs. Run jamf-cli platform setup, or write the profile by hand:

# Platform environment scope: the level to prefer
jamf-cli config add-profile my-jsc \
  --url https://eu.api.jamfcloud.com \
  --auth-method platform \
  --environment-id <platform-environment-id>

# Tenant scope: the legacy level
jamf-cli config add-profile my-jsc-tenant \
  --url https://eu.api.jamfcloud.com \
  --auth-method platform \
  --tenant-id <security-cloud-tenant-id>

A gateway-served command with no platform credentials resolved fails with this command requires platform gateway auth and a config add-profile line. See Platform API GA Migration for regions, scope levels and the GA base URL.

Scope level. Prefer environment scope: one EU environment credential reaches the whole gateway-served Security Cloud surface, with content categories, DNS, ZTNA, UEM Connect and device groups all answering 200. Tenant scope (X-Tenant-Id) is the legacy level, and device-groups hits a gateway fault under it (below). A tenant credential lacking the Security Cloud grants answers 403 BAD_PERMISSIONS, which is an answer about grants and not about the level. The two levels are mutually exclusive: supplying both is refused.

One profile, both halves

security setup and platform setup merge into the named profile, so run both against one name to get a profile that reaches everything:

jamf-cli security setup --profile-name jsc    # Radar pairs
jamf-cli platform setup --profile-name jsc    # gateway credential + scope
jamf-cli -p jsc security risk list            # Radar
jamf-cli -p jsc security dns-zones list       # gateway

On a re-run, a pair you skip is left as it was. To drop a pair, remove the profile (jamf-cli config remove-profile <name>) and run setup again with only what you want.

Aliases

Command Alias
security sec
device-lifecycle lifecycle, dl
enrollment-activation-profiles eap

Setup

# Radar APIs: interactive wizard, one application ID/secret pair per API
jamf-cli security setup

# Save under a custom profile name (default: "security")
jamf-cli security setup --profile-name radar

# Platform gateway half (default profile name: "platform")
jamf-cli platform setup

The Radar wizard prompts for the Risk, Device Lifecycle, and Shared Signals & Events application ID/secret pairs in turn. Leave an application ID blank to skip that API. Secrets are stored in the system keychain. See Setup Guide#Jamf Security Cloud Quick Start for the full walkthrough.

security setup flags

Flag Description
--profile-name Profile name (default: security)

security setup collects credentials through interactive prompts with hidden input, so it cannot run under --no-input. For CI/CD, use environment variables:

Variable API
JAMFSECURITY_RISK_CLIENT_ID / _SECRET Risk
JAMFSECURITY_LIFECYCLE_CLIENT_ID / _SECRET Device Lifecycle
JAMFSECURITY_SSE_CLIENT_ID / _SECRET Shared Signals & Events
JAMFSECURITY_URL, JAMFSECURITY_SSE_URL Host overrides for a regional or sandbox host. Since v1.32.0 JAMFSECURITY_URL alone moves the Radar host; --url and a profile's url select the gateway only

Set both halves of a pair or neither: an ID from the environment with the secret coming from a profile is refused. The gateway half reads the ordinary platform variables (JAMF_URL, JAMF_CLIENT_ID, JAMF_CLIENT_SECRET, and either JAMF_ENVIRONMENT_ID or JAMF_TENANT_ID, never both). See Configuration & Profiles#Jamf Security Cloud.

Open the Radar console

security open opens https://radar.wandera.com. Jamf Security Cloud has no per-tenant URL, tenancy living in the JWT's customer_id claim, so the target is a compiled-in constant. This command carries jamf:no-auth, makes no request, and needs no credentials and no configuration.

# Open Radar
jamf-cli security open

# Print the URL instead of launching a browser
jamf-cli security open --print

The URL is printed rather than launched under any of --print, --no-input, --dry-run, --out-file, --field, --select, or a non-terminal stdout, so jamf-cli security open | pbcopy and a CI job both get the URL instead of a failed launch; read it with -o json or --field url. On the launch path stdout stays empty and the note goes to stderr. $BROWSER wins where it is set, otherwise the platform's own opener.

The other three products resolve a per-tenant URL instead, and pro open also takes a section name. See Jamf Pro Commands.

Request bodies on write commands

Every security write command that takes a body, on both halves, accepts the same inputs, and --scaffold prints the template:

jamf-cli security dns-zones create --scaffold > zone.json
# ... edit zone.json ...
jamf-cli security dns-zones create --from-file zone.json

# The body can arrive on stdin instead
jamf-cli security dns-zones create --scaffold | vipe | jamf-cli security dns-zones create

# Overlay individual fields onto the body, or build a small body outright
jamf-cli security dns-zones create --from-file zone.json --set name=corp-internal
Flag Behaviour
--from-file <path> Request body from a JSON or YAML file. Omit it and the body is read from stdin
--set key=value Repeatable, supports nested.keys; overlays onto the body
--scaffold Print an example body and exit (no auth needed)

Rules that change what you type:

  • The flag is --from-file, the same name the Jamf Pro, Classic, Protect and School commands use. --file answers unknown flag: --file with hint: did you mean --from-file? and exit 2, in the hint field of -o json as well as on stderr. --file still names an upload payload on the commands that send one, such as pro packages upload; those take no pipe, because the transport needs a filename and a length.
  • Stdin is read when --from-file is absent. An empty pipe is not a body, since a CI runner hands every process a stdin that carries nothing. A named --from-file that is empty is an error.
  • --set overlays onto a --from-file or piped body. Classic's --set builds the whole body and refuses to be combined with a file.
  • --set refuses to overwrite a non-object field: --set ipsec.esp.lifetimeInSec=14400 over a body whose ipsec is a string answers cannot set nested key under non-object field "ipsec".
  • --set builds an object, so it cannot express a request body that is a top-level JSON array. dns-custom-hostname-mappings update is one; use --from-file for it.
  • A piped body over 10MB is refused, never truncated: stdin body exceeds 10MB; use --from-file for a larger payload. Use --from-file for a payload that size.
  • --out-file, --select, --compact, --quiet and --no-hints work on every command in the namespace; --select and --compact narrow rows for every column format.
  • Enum-constrained fields list their values in --help, under Allowed values:, one line per dotted field path. A [] suffix on the path puts the constraint on each array element: ipsec.esp.encryption[]: 3des, aes128, aes256 constrains every element of that array. A scaffold renders an enum as "", so --help is where the legal set lives.
  • A scaffold shows one specimen element inside an array of objects, and prefers a spec example over a placeholder. Edit it; do not pipe it through unread.
  • -n, --dry-run is honoured on these writes: the method, resolved path and body go to stderr and nothing is sent. The preview comes before the confirmation prompt, so -n works on destructive commands without --yes.
  • An empty list prints [], never null, so it is safe to pipe into jq.

apply: create-or-update by name

Five gateway resources take apply, the same idempotent verb the pro namespace has: dns-zones, ztna-apps, ztna-gateways, ztna-grouped-gateways and device-groups. (platform ai-policies is the sixth, outside this namespace; see Jamf Platform Commands.)

apply reads a body, takes the name out of it, and looks that name up in the collection. Absent, it creates; present, it updates after a confirmation, which --yes skips.

# Idempotent: run it twice, get one gateway
jamf-cli security ztna-gateways apply --from-file gateway.yaml --yes

# Scaffold, edit, apply with no temp file
jamf-cli security ztna-gateways apply --scaffold | vipe | jamf-cli security ztna-gateways apply --yes

# Preview the branch: the lookup is a read, so -n reports which one runs
jamf-cli security dns-zones apply --from-file zone.yaml --dry-run
# [dry-run] Would create dns-zone "corp-internal"
# [dry-run] POST /securitycloud/v1/dns/zones

Four things to know:

  • The name comes from the body. There is no --name on apply. A body with no name, or a name that is not a non-empty string, is refused before anything is sent.
  • Four of the five update with PATCH, so fields you omit keep their current values. device-groups updates with a PUT and replaces the group wholesale, as pro and classic apply do. Each command's --help says which it has. To make a resource match your document, send a complete body; --scaffold prints one.
  • A failed lookup takes no branch. Only a genuine "not found" creates; an auth error or a 5xx during the lookup aborts.
  • The lookup and the create are two requests, so two runs racing on the same absent name (a CI retry, or concurrent jobs) can both create one. No spec in this namespace declares a name-uniqueness 409. Serialise apply per resource where that matters.

The lookup lists the collection and accumulates matches across every page before deciding, so a name repeated either side of a 100-item page boundary is an ambiguity error. A name matching an item the list returns no ID for is reported as that; device groups are the live case.

apply carries the same jamf:scopes annotation as its sibling verbs, so it appears under scopes in jamf-cli commands -o json and earns the scope-level note the CLI appends to a missing-scope 400.

The Radar half has no apply: those resources are singletons, actions and read-only documents, with no named collection to match a name against.

Radar API commands

Device Risk (risk)

Read and override the calculated risk status of managed devices. Requires the Risk API credentials.

# List device risk status
jamf-cli security risk list

# Narrow to a single device by its guid
jamf-cli security risk list --guid <device-guid>

# Narrow to a single device by its externalId
jamf-cli security risk list --external-id <external-id>

# Override the calculated risk: scaffold, edit, then apply (deviceIds is a list)
jamf-cli security risk override --scaffold > override.json
# ... edit override.json ...
jamf-cli security risk override --from-file override.json

# Set the scalar fields inline (combine with a body file for deviceIds)
jamf-cli security risk override --set risk=<risk-level> --set source="manual review"

risk list flags

Flag Description
--guid Return a single device by its guid
--external-id Return a single device by its externalId

risk override body

{
  "deviceIds": [],
  "risk": "",
  "source": ""
}

deviceIds is declared upstream as an array with no element schema, so --scaffold renders it as []. Fill it from risk list.

Device Lifecycle (device-lifecycle, dl)

Purge devices from Jamf Security Cloud. The most destructive command in the CLI: it requires --yes or an interactive confirmation. Requires the Device Lifecycle API credentials.

# Scaffold, edit (fill externalIds and/or guids), then purge
jamf-cli security device-lifecycle purge --scaffold > purge.json
# ... edit purge.json ...

# Preview first. The preview prints before any confirmation prompt,
# so this works without --yes
jamf-cli security dl purge --from-file purge.json --dry-run

# Execute
jamf-cli security dl purge --from-file purge.json --yes

device-lifecycle purge body

{
  "externalIds": [],
  "guids": []
}

jamf-cli fills the customer ID in from the Device Lifecycle credentials at request time; it is never user-facing. A body that scopes nothing is refused before the preview.

Shared Signals & Events (SSE)

Configure and monitor the Jamf Security Cloud Shared Signals & Events transmitter, an OpenID Shared Signals Framework stream. Requires the Shared Signals & Events API credentials.

Stream configuration (stream)

# Get the current event stream configuration
jamf-cli security stream get

# Create or replace the event stream configuration
jamf-cli security stream update --from-file stream.json

# Set individual scalar fields
jamf-cli security stream update --set format=<format> --set aud="https://receiver.example.com"

# Print the request-body scaffold
jamf-cli security stream update --scaffold

# Delete the event stream configuration (destructive)
jamf-cli security stream delete --yes

The stream update scaffold:

{
  "aud": "",
  "delivery": null,
  "events_delivered": [],
  "events_requested": [],
  "events_supported": [],
  "format": "",
  "iss": "",
  "min_verification_interval": 0
}

Stream status (status)

# Get the current stream status
jamf-cli security status get

# Update the stream status
jamf-cli security status update --set status=<status> --set reason="reactivating stream"

# Print the request-body scaffold
jamf-cli security status update --scaffold

The status update scaffold:

{
  "reason": "",
  "status": ""
}

Verification (verification)

# Trigger an SSE stream verification event
jamf-cli security verification trigger

# Include a state value echoed back in the verification event
jamf-cli security verification trigger --set state=<opaque-value>

# Print the request-body scaffold
jamf-cli security verification trigger --scaffold

Discovery (well-known, jwks): read-only

# Get the SSE transmitter discovery document (.well-known metadata)
jamf-cli security well-known get

# Get the SSE transmitter's JSON Web Key Set
jamf-cli security jwks get

Platform gateway commands

Every command below needs a platform gateway credential instead of the Radar pairs. An environment-scoped credential reaches all of them.

DNS & content filtering

DNS zones (dns-zones)

jamf-cli security dns-zones list
jamf-cli security dns-zones list --sort name:desc
jamf-cli security dns-zones get <id>
jamf-cli security dns-zones get --name corp-internal

jamf-cli security dns-zones create --scaffold > zone.json
jamf-cli security dns-zones create --from-file zone.json

# Partial update: JSON Merge Patch (RFC 7396). Omitted fields are left alone;
# null clears an optional field.
jamf-cli security dns-zones patch <id> --set name=corp-internal-eu

jamf-cli security dns-zones delete <id> --yes

The create / patch scaffold:

{
  "domains": [
    "example.com"
  ],
  "name": "corp-internal",
  "nameServers": [
    {
      "gatewayId": "gw-eu-01",
      "ip": "203.0.113.53"
    }
  ]
}
  • nameServers[].ip must be a public address. The server refuses a private one with 422 NAMESERVER_IP_RESTRICTED: 10.0.0.53 fails, 198.51.100.53 is accepted. An unknown gatewayId is 422 GATEWAY_NOT_FOUND. The ZTNA rule below runs the other way: ipsec.left.subnets[] must be private.
  • list does not paginate: the full set comes back in one response, bounded by a per-customer zone cap. --sort accepts name:asc / name:desc and no other field.
  • Setting a required field to null in a patch is a 400. There is no optimistic locking, so concurrent patches are last-writer-wins.
  • The privileges are ztna:read / ztna:create / ztna:update / ztna:delete: DNS zones share the ZTNA permission family and have no dns:* one.

Search domain (dns-search-domains): singleton

jamf-cli security dns-search-domains get
jamf-cli security dns-search-domains update --set suffix=corp.example.com
jamf-cli security dns-search-domains delete --yes

Scaffold: {"suffix": "corp.example.com"}. update overwrites any existing suffix.

get on a tenant with no search domain renders {} and exits 0. The server answers 404 SEARCH_DOMAIN_NOT_SET and the CLI carries that documented empty state through as an empty object. The allowance matches on the error code as well as the status, so a 404 arriving for any other reason still fails.

Custom hostname mappings (dns-custom-hostname-mappings): whole-list replace

jamf-cli security dns-custom-hostname-mappings get

# get returns the collection envelope; update wants the bare array
jamf-cli security dns-custom-hostname-mappings get | jq '.results' > mappings.json
# ... edit mappings.json ...
jamf-cli security dns-custom-hostname-mappings update --from-file mappings.json

jamf-cli security dns-custom-hostname-mappings delete --yes

The body is a top-level JSON array that replaces the entire mapping set:

[
  {
    "aRecords": ["203.0.113.10"],
    "aaaaRecords": ["2001:db8::10"],
    "hostname": "app.example.com",
    "secureDns": false,
    "ztna": false
  }
]

--set builds an object and cannot express this body, so --from-file is the only route. update overwrites the previous mappings (at most 500 entries) and there is no per-entry edit. get returns the standard collection envelope {results, totalCount}, so unwrap .results when round-tripping.

Content categories (content-categories): read-only

jamf-cli security content-categories list

Not paginated. These are the categoryName values that ztna-apps create requires.

Zero Trust Network Access (ZTNA)

Apps (ztna-apps)

jamf-cli security ztna-apps list
jamf-cli security ztna-apps get <appId>
jamf-cli security ztna-apps create --from-file app.json
jamf-cli security ztna-apps patch <appId> --set security.riskControls.levelThreshold=HIGH
jamf-cli security ztna-apps delete <appId> --yes

# The catalog of predefined SaaS app definitions: pass an id as predefinedAppId
jamf-cli security ztna-predefined-apps list

Enum values in --help: routing.type (CUSTOM, DIRECT), routing.dnsIpResolutionType (IPv4, IPv6), security.riskControls.levelThreshold (LOW, MEDIUM, HIGH), and the same pair again under groupOverrides.routingOverrides[].routing.*.

Server behaviour the scaffold does not show:

  • categoryName is required and the scaffold renders it empty, and "" is the one value the server rejects (400 [INVALID_FIELD] categoryName: must not be blank). Uncategorized is a value tenants' own apps carry; content-categories list has the rest. A category that does not exist is 409 MISSING_CATEGORY_NAME.
  • Setting predefinedAppId inherits name and hostnames from the template, and the server drops a name you send: the create answers 201 and the app reads back name: null. Those apps cannot be reached with --name, and the failure reads no item with name "…", which also describes a typo. Address them by ID. Reusing a template another app already holds is 409 CONFLICT.
  • The server accepts an unknown key under security and drops it. The schema declares additionalProperties: false and the server does not enforce it, so --set security.riskControls.enabld=true answers 201 and does nothing.
  • create returns {id, href}. Follow with a get for the app's fields.

Dedicated gateways (ztna-gateways)

jamf-cli security ztna-gateways list
jamf-cli security ztna-gateways get <gatewayId>
jamf-cli security ztna-gateways create --from-file gw.json

# Partial IPSec edit, deep-merged server-side, so send only what changes
jamf-cli security ztna-gateways patch <gatewayId> --set ipsec.esp.lifetimeInSec=14400

jamf-cli security ztna-gateways delete <gatewayId> --yes

# Jamf-managed shared gateways available to the tenant (id and name only)
jamf-cli security ztna-shared-gateways list

--help lists the full cipher-suite enums (ipsec.{esp,ike}.{dhGroups,encryption,integrity}[]), ipsec.keyExchange (ikev1, ikev2), the 13 datacenter values, and the 11 ipsec.right.vendor values.

  • ipsec.right.vendor is case-sensitive. "cisco" for "Cisco" earns a 400 INVALID_FIELD carrying no field and the generic "Request body is missing or malformed." The right casing gets per-field errors with attribution.
  • ipsec.left.subnets is capped at one element; right.subnets is not. Two left subnets earn 400 [INVALID_FIELD] field=ipsec "IPSec configuration is not valid.", naming neither the field nor the cardinality. Left subnets must be private (the DNS nameserver rule runs the other way). right.subnets round-trips more than one, and the server returns them in its own order, so an export/re-apply is not byte-stable.
  • contact is required: omitting it is 400 [INVALID_FIELD] contact: must not be null.
  • patch prints nothing on success. It sends application/merge-patch+json and expects 204, so there is no body to render. The server-side deep merge is real: patching ipsec.esp.lifetimeInSec alone leaves ike, left.subnets and right.subnets untouched.
  • create returns {id, href}.
  • dedicatedIps.ips is server-allocated and read-only, so the scaffold omits it.
  • The per-datacenter availability-zone source IPs a peer firewall must allow live in the spec's availabilityZones property description (specs/platform/securitycloud_ztna_api.json); --help renders enums and not property descriptions.

Grouped gateways (ztna-grouped-gateways)

jamf-cli security ztna-grouped-gateways list
jamf-cli security ztna-grouped-gateways get <groupedGatewayId>
jamf-cli security ztna-grouped-gateways create --from-file group.json
jamf-cli security ztna-grouped-gateways patch <groupedGatewayId> --set routingStrategy=NEAREST
jamf-cli security ztna-grouped-gateways delete <groupedGatewayId> --yes

Scaffold:

{
  "gatewayIds": ["a1b2", "c3d4"],
  "name": "EU Grouped Gateway",
  "recoveryDelayInSec": 3600,
  "routingStrategy": "",
  "tenantIds": ["3fa85f64-5717-4562-b3fc-2c963f66afa6"]
}
  • recoveryDelayInSec is a required enum of five integers: 300, 1800, 3600, 10800, 28800. The server rejects 0, the value you get by forgetting the field, under every routing strategy including the two whose own prose says the field is ignored. Omitting it or sending null is 400 … must not be null.
  • routingStrategy is ACTIVE_STANDBY, RANDOM or NEAREST, and the scaffold renders it empty.
  • Grouping a gateway that belongs to a shared gateway is 422 SHARED_GATEWAY_MEMBER.
  • create returns {id, href}.

Device groups (device-groups)

jamf-cli security device-groups list
jamf-cli security device-groups get <groupId>
jamf-cli security device-groups get --name "Engineering Team"
jamf-cli security device-groups create --set name="Engineering Team"
jamf-cli security device-groups update <groupId> --set name="Engineering"
jamf-cli security device-groups apply --set name="Engineering Team" --yes
jamf-cli security device-groups delete <groupId> --yes

A 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 answers 204 with no body. Read the change back with list. The default group cannot be deleted.

The verbs mix API versions, which shows up in a -vv log or an audit event: list and update are v2, create, get and delete are v1. apply resolves the name against the v2 list, creates on the v1 collection and updates on the v2 item. Verified end to end on an EU environment credential 2026-09-11: create, update --set name=… (silent, exit 0), list showing the new name, delete.

The implicit "Default Group" comes back without an ID. list returns it as {"name": "Default Group"} with no id, since it is not a stored group. A group you create carries its id, so --name, get, update and delete all work on it. On that one name the CLI names the problem:

$ jamf-cli security device-groups apply --set "name=Default Group" --yes
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

Observed 2026-09-11 on a tenant holding the Default Group plus one created group.

URI_NOT_TEMPLATED here is the scope level. Under tenant scope, device-groups list answers

[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 on a tenant-scoped credential for the same tenant whose environment-scoped credential returns the group list from that same v2 path. The v2 groups path templates under X-Environment-Id and fails to template under X-Tenant-Id. Use an environment-scoped credential; this is an upstream gateway fault.

UEM Connect (uem-*)

Connect a UEM platform to Jamf Security Cloud and manage its device sync.

jamf-cli security uem-connectors list
jamf-cli security uem-connectors get <configId>   # no name to resolve; take the id off the list
jamf-cli security uem-connectors create --from-file connector.json
jamf-cli security uem-connectors delete <configId> --yes

jamf-cli security uem-connector-enablement enable <configId> --set enabled=true
jamf-cli security uem-connector-enablement disable <configId>

jamf-cli security uem-sync trigger <configId>
jamf-cli security uem-sync list <configId>
jamf-cli security uem-sync cancel <configId>

jamf-cli security uem-sync-settings get <configId>
jamf-cli security uem-sync-settings update <configId> --from-file settings.json

jamf-cli security uem-activation-profiles deploy-to-uem <code> --from-file deploy.json

Creating a UEM connector

jamf-cli security uem-connectors create --scaffold > connector.json
# ... edit, then ...
jamf-cli security uem-connectors create --from-file connector.json
{
  "authStrategy": "JAMF_PRO_OAUTH",
  "deviceSyncAuth": {
    "clientId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "clientSecret": "abcXYZ123secret",
    "password": "s3cr3t",
    "username": "svc-jsc"
  },
  "isoCountry": "US",
  "tenantId": "acme-tenant",
  "url": "https://yourcompany.jamfcloud.com",
  "vendor": "JAMF_PRO"
}
  • A tenant holds at most one connector, whatever its vendor. A second create answers 409 CONNECTOR_CONFIG_ALREADY_EXISTS, and that check runs after auth-config validation and before the credentials are checked against the UEM instance.
  • The request body is a discriminated union over ten vendor shapes, keyed on vendor. --help names all ten and --scaffold renders the first, JamfProConnectorCreateRequest, the only wire-verified branch. The Allowed values: line for authStrategy spans every variant (JAMF_PRO_OAUTH, BASIC, M2M, USERNAME_PASSWORD, CITRIX_CLOUD_ADMIN, CITRIX_CLOUD_ADMIN_OAUTH) and is wider than the shape the scaffold shows: the last three are Citrix/XenMobile strategies.
  • deviceSyncAuth in the scaffold is the union of the OAuth and BASIC credential pairs: clientId/clientSecret and username/password together. Delete the half your authStrategy does not use.
  • authStrategy: M2M takes a tenantId and no credentials: Security Cloud self-provisions what it needs on the named Jamf Pro, and url is then ignored. The Jamf Pro API role and integration it provisions survive the connector's DELETE, so repeated create/delete cycles leak an enabled integration per create onto the target instance, with nothing in the connector response pointing at one. jamf-cli pro api-integrations list against the instance, on an instance profile (a gateway profile refuses that command), is the only way to see them.
  • Put the secret in --from-file, never in --set. --set deviceSyncAuth.clientSecret=… puts it in shell history and ps output.
  • Error ladder, in order: no authStrategy → 500 INTERNAL_ERROR; authStrategy with no deviceSyncAuth → 422 VALIDATION_FAILED ": invalid auth configuration for Jamf PRO"; a complete body on a tenant that already has a connector → 409 CONNECTOR_CONFIG_ALREADY_EXISTS.
  • A read returns clientId and username and no secret, so a JAMF_PRO_OAUTH connector cannot be exported and re-created. An M2M one can, since tenantId is on both the request and the response.

uem-sync-settings update: a full replacement

# Read the current values...
jamf-cli security uem-sync-settings get <configId>

# ...but build the body from the scaffold: get and update do NOT share a shape
jamf-cli security uem-sync-settings update --scaffold > settings.json
# ... edit settings.json ...
jamf-cli security uem-sync-settings update <configId> --from-file settings.json
  • It replaces the whole settings object, so a field you omit resets to its default. Start from the current values.
  • get and update do not share a shape. get returns the whole connector config (id, customerId, connected, latestSync, url, deviceSyncAuth and the rest), with autoDeviceDeletion and disableSyncOnAuthError nested under syncConfig. update accepts ten fields, with those two flat. Read get output and transcribe into the scaffold; piping one into the other fails.
  • refreshRateMinutes enforces its enum: 60, 120, 240, 480, 720, 1440. Anything else (0, 1 and 360 included) is 422 VALIDATION_FAILED, and the message leaks the accepted set while leaving field null.
  • The server discards deviceUnmanagedThreshold for JAMF_PRO connectors. It answers 204 and reads back 0 whatever you sent, so a grace period set on a Jamf Pro connector does not exist, while --help lists the enum (0, 1, 3, 5, 7, 14) as if it applied. On a vendor that honours it, the value is days since last check-in and 0 means the platform default of 3 days.
  • The body's vendor must match the connector's stored vendor: a mismatch is 422 VENDOR_MISMATCH. The field selects which vendor-specific settings apply; the <configId> path selects the connector, and a connector's vendor cannot be changed here. The spec's operation description carries this and --help renders only its first paragraph.
  • concurrentSyncEnabled is writable and part of the replacement, so omitting it resets it to its default.

Enablement, sync runs and UEM deployment

  • uem-connector-enablement enable <configId> takes a body ({"enabled": true}); disable takes none. Both are idempotent.
  • uem-sync trigger returns 202 Accepted and the sync runs in the background. Poll uem-sync list <configId> for progress (newest first). uem-sync cancel is best-effort; the run may finish before it takes effect.
  • uem-activation-profiles deploy-to-uem <code> is the last step of automated UEM Connect onboarding: it pushes the Jamf Security configuration profiles to the UEM platform for one activation-profile code. Enums: platform (SUPERVISED_MAC, SUPERVISED_IOS, UNSUPERVISED_IOS, BYOD_IOS), uem (JAMF).
  • Only uem-sync list declares pagination parameters upstream; most Security Cloud lists ignore page/page-size and return everything.

Enrollment activation profiles (enrollment-activation-profiles, eap)

The object uem-activation-profiles deploy-to-uem deploys. This resource mints and manages the code; UEM Connect holds the deploy action.

# --origin is required, and PUBLIC_API is its only legal value
jamf-cli security eap list --origin PUBLIC_API

jamf-cli security eap create --scaffold > profile.json
# ... edit profile.json ...
jamf-cli security eap create --from-file profile.json

jamf-cli security eap get <activationProfileId>
jamf-cli security eap pause <activationProfileId>
jamf-cli security eap resume <activationProfileId>
jamf-cli security eap delete-multiple --set codes='["<code>"]' --yes

The create scaffold:

{
  "capabilities": {
    "dataPolicy": false,
    "networkSecurity": false,
    "note": "Enabled for compliance requirements",
    "vulnerabilityManagement": false
  },
  "groupId": "group-123",
  "name": "My Activation profile",
  "origin": "PUBLIC_API",
  "platforms": ["iOS"]
}

Server behaviour, none of it visible from the spec:

  • The read model is a code and nothing else. get returns {"code": "…"} and omits the name, platforms, capabilities and state the create sent. list unwraps to a bare array of the same one-field objects. A create's input cannot be read back.
  • create answers {"code": "…"}, despite declaring an {id, href} body with a Location header. Take the code; there is no id to read.
  • pause and resume have no observable effect on any GET, and print nothing on success (both are 204-no-body and idempotent).
  • delete-multiple is a soft delete the read surface does not reflect. After it answers 204, get <code> still answers 200 and list still returns the code. A write reveals it: pause on a deleted code answers 409 STATE_CONFLICT ("… is already deleted."), in a framework error envelope carrying no traceId. You can neither confirm a delete nor filter deleted profiles out of a list, and each profile created is a permanent row.
  • capabilities.networkSecurity and capabilities.vulnerabilityManagement must be both on or both off: 400 [INVALID_FIELD] capabilities: networkSecurity and vulnerabilityManagement must both be enabled or both disabled.
  • platforms accepts iOS and MAC only, and the server applies the 1–2 size limit after de-duplication, so ["iOS","MAC","iOS"] is accepted.
  • groupId is not validated. A mistyped group answers 201 with the profile scoped to nothing. capabilities.note's length limit is unenforced too.
  • list requires --origin, and cobra refuses the call client-side without it. The two halves report an out-of-enum origin differently: 400 … Unknown origin value. on list, 400 … Origin not provided. on create, which misreports the cause.

Command Summary

Command Action API Destructive
security setup Configure Radar credentials n/a
security risk list List device risk status Radar
security risk override Override calculated device risk Radar
security device-lifecycle purge Purge devices Radar ✓
security stream get Get stream configuration Radar
security stream update Create/replace stream configuration Radar
security stream delete Delete stream configuration Radar ✓
security status get Get stream status Radar
security status update Update stream status Radar
security verification trigger Trigger stream verification Radar
security well-known get Get transmitter discovery document Radar
security jwks get Get transmitter JSON Web Key Set Radar
security dns-zones list / get / create / apply / patch / delete DNS zone CRUD Gateway delete
security dns-search-domains get / update / delete Tenant search domain (singleton) Gateway delete
security dns-custom-hostname-mappings get / update / delete Custom hostname mappings (whole-list) Gateway delete
security content-categories list List content categories Gateway
security ztna-apps list / get / create / apply / patch / delete ZTNA access policies Gateway delete
security ztna-predefined-apps list Predefined SaaS app catalog Gateway
security ztna-gateways list / get / create / apply / patch / delete Dedicated ZTNA gateways Gateway delete
security ztna-shared-gateways list Jamf-managed shared gateways Gateway
security ztna-grouped-gateways list / get / create / apply / patch / delete Grouped ZTNA gateways Gateway delete
security device-groups list / get / create / apply / update / delete Security Cloud device groups Gateway delete
security uem-connectors list / get / create / delete UEM connector Gateway delete
security uem-connector-enablement enable / disable Pause/resume connector sync Gateway
security uem-sync trigger / list / cancel Sync runs Gateway
security uem-sync-settings get / update Connector sync settings Gateway
security uem-activation-profiles deploy-to-uem Push JSC profiles to the UEM Gateway
security enrollment-activation-profiles list / get / create / pause / resume / delete-multiple Enrollment activation profiles Gateway delete-multiple

Notes

  • The Radar half has no name-to-ID resolution. Supply every identifier (guid, externalId, deviceIds) directly. The gateway half has --name on most get/patch/update/delete commands, resolved by listing the collection and matching across every page. Four exceptions: a predefined-derived ZTNA app (whose name is null on the wire), the UEM connectors (whose read model carries an id and no name), the enrollment activation profiles (where the read model is a code, so the flag is not emitted), and the implicit device-group "Default Group", which the list returns with no id and the CLI reports as such.

  • Body writes take --from-file (JSON or YAML) and/or --set key=value, with --scaffold to print the template. --set overlays onto a --from-file body. Leave --from-file off and the body is read from stdin, so --scaffold | edit | apply pipes end to end. See CLI Patterns.

  • Privileges. The gateway half carries per-operation permissions the catalog reports: ztna:* (also used by DNS zones), content-categories:read, device-groups:*, search-domains:*, custom-hostname-mappings:*, uem-connect:*, activation-profiles:*. List them with jamf-cli commands -o json | jq -r '.[] | select(.product=="security") | .command + " " + ((.privileges // []) | join(","))'.

  • A 403 names the permission in Jamf Account's own words and exits 5:

    {
      "error": "permission_denied",
      "exitCode": 5,
      "exitCodeName": "permission_denied",
      "hint": "grant the Jamf Platform API integration these permissions in Jamf Account: Secure enterprise access > Content categories: Read (content-categories:read). Names are as the permission picker shows them: https://developer.jamf.com/platform-api/reference/jamf-pro-permissions-map",
      "message": "list: API request failed with status 403, traceId 213a…e4f7 (method=GET, url=https://eu.api.jamfcloud.com/securitycloud/v1/categories): [BAD_PERMISSIONS] The given token was not authorized to access the requested resource."
    }

    You search the picker by name, which is why the hint prints the name alongside the slug. security dns-zones list gives the same shape with Secure enterprise access > Zero-Trust Network Access (ZTNA): Read (ztna:read): DNS zones need the ztna grant, which the command name does not suggest.

  • A 403 BAD_PERMISSIONS on the gateway half is an answer about grants. It says the integration in hand lacks the permission, and says nothing about the endpoint or the scope level. jamf-cli platform setup does not answer it either: it validates the credentials and the scope ID, reports which resource groups the credential's level declares, and points at the 403, which names the permission.

  • Output defaults to JSON. Add -o table/-o yaml/-o csv as needed. See Output Formats.

  • Exit codes, including 8 (refused by policy) and 5 (a platform 403), are in Error Handling & Exit Codes.

For copy-paste recipes, see Security Cloud Workflows. For the gateway itself (regions, scope levels, the GA base URL) see Platform API GA Migration and Jamf Platform Commands.

jamf-cli Wiki


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

Clone this wiki locally