Jamf Pro Hardening Guide
MDM hardening for Jamf Pro macOS and iOS device management
Overview
Jamf Pro is the leading Apple device management platform used by over 70,000 organizations to manage macOS, iOS, iPadOS, and tvOS devices. As a critical infrastructure component for device security, Jamf Pro configurations directly impact endpoint security posture across Apple fleets. Proper hardening ensures devices are configured securely while maintaining user productivity.
Intended Audience
- Security engineers managing Apple device fleets
- IT administrators configuring Jamf Pro
- GRC professionals assessing MDM security
- Third-party risk managers evaluating device management
How to Use This Guide
- L1 (Crawl): Essential controls for all organizations
- L2 (Walk): Enhanced controls for security-sensitive environments
- L3 (Run): Strictest controls for regulated industries
Scope
This guide covers Jamf Pro server security, configuration profiles, CIS benchmark implementation, and managed device hardening.
Table of Contents
- Jamf Pro Server Security
- Device Security Policies
- CIS Benchmark Implementation
- Monitoring & Compliance
- Compliance Quick Reference
1. Jamf Pro Server Security
1.1 Secure Jamf Pro Console Access
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.4 |
| NIST 800-53 | AC-6(1) |
Description
Secure Jamf Pro console access with SSO, MFA, and role-based access controls.
Rationale
Why This Matters:
- Jamf Pro controls all managed device configurations
- Compromised admin can push malicious profiles/scripts
- Role-based access limits blast radius
Attack Prevented: Console account takeover, unauthorized profile and script deployment, privilege escalation via over-broad privilege sets
ClickOps Implementation
Step 1: Configure SSO
- Navigate to: Jamf Pro → Settings → System → SSO
- Click Edit
- Configure SAML SSO:
- Upload IdP metadata
- Configure attribute mappings
- Set group mappings to Jamf roles
- Test SSO authentication
Step 2: Configure User Accounts
- Navigate to: Settings → User Accounts & Groups
- Review existing accounts
- Remove unnecessary admin accounts
- Convert local accounts to SSO where possible
- Any account that exists only to serve an integration should be retired in favor of an API Role and API Client — see 1.2
Step 3: Configure Privilege Sets
- Navigate to: Settings → User Accounts → Privilege Sets
- Create granular privilege sets:
- Help Desk: Device lookup, basic actions
- Deployment: Profile and app management
- Security: Full security policy access
- Administrator: Full access (limit to 2-3)
- Assign minimum required privileges
Time to Complete: ~1 hour
Code Implementation
Code Pack: Terraform
# Create granular API roles for least-privilege access
# Help Desk role: device lookup and basic read-only actions
resource "jamfpro_api_role" "helpdesk" {
display_name = "HTH Help Desk"
privileges = toset(var.helpdesk_privileges)
}
# Deployment role: profile and app management
resource "jamfpro_api_role" "deployment" {
display_name = "HTH Deployment"
privileges = toset(var.deployment_privileges)
}
# Security role: full security policy access
resource "jamfpro_api_role" "security" {
display_name = "HTH Security"
privileges = toset(var.security_privileges)
}
# Dedicated Help Desk account with minimum privileges
resource "jamfpro_account" "helpdesk" {
name = "hth-helpdesk"
enabled = "Enabled"
access_level = "Full Access"
privilege_set = "Custom"
jss_objects_privileges = toset(var.helpdesk_privileges)
}
# Dedicated Deployment account
resource "jamfpro_account" "deployment" {
name = "hth-deployment"
enabled = "Enabled"
access_level = "Full Access"
privilege_set = "Custom"
jss_objects_privileges = toset(var.deployment_privileges)
}
1.2 Secure API Access
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 3.11 |
| NIST 800-53 | SC-12 |
Description
Authenticate Jamf Pro API integrations with API Roles and API Clients using the OAuth 2.0 client credentials flow, instead of standard Jamf Pro user accounts.
Rationale
Why This Matters:
- The Jamf Pro API can read and modify every managed device, push profiles and scripts, and pull full inventory — an exposed API credential is effectively full MDM admin
- API Roles and Clients grant access exclusively to the Jamf Pro APIs, so a leaked integration credential cannot be replayed against the Jamf Pro user interface the way a user account’s credentials can
- Per-integration clients with narrowly scoped roles keep a compromised integration from inheriting human-admin power and make revocation surgical — disable one client without touching any other integration or human admin
- Client secrets can be rotated and access tokens are short-lived, shrinking the window a leaked credential stays useful
- API audit logging exposes credential abuse and automated reconnaissance that the console UI never surfaces
Attack Prevented: API credential theft, credential reuse against the admin console, over-privileged integration abuse, undetected programmatic tampering
Superseded mechanism: Authenticating integrations as Jamf Pro user accounts is superseded by API Roles and API Clients. Jamf’s developer documentation states that “Client Credentials based authorization provides access exclusively to Jamf Pro APIs, helping secure the credentials of your Jamf Pro Administrators and access to the Jamf Pro user interface.” Migrate any integration still authenticating as a user account. (Jamf Pro client credentials)
ClickOps Implementation
Step 1: Create an API Role
- Navigate to: Settings → System Settings → API Roles and Clients
- Create an API Role and select only the privileges that specific integration needs
- Create one role per integration rather than a shared “automation” role
Step 2: Create an API Client
- From the same API Roles and Clients area, create an API Client
- Assign the API Role created in Step 1
- Set the access token lifetime to the shortest value the integration tolerates
- Enable the client only when it is ready to be used
Step 3: Generate and Store the Client Secret
- Generate the client secret and copy it immediately — it is shown once
- Store it in a secrets manager, never in source control or a script
- Rotate the client secret on a defined schedule and whenever an operator with access to it departs
Step 4: Disable or Rotate on Change
- Disable an API Client to immediately revoke an integration’s access without deleting its configuration
- Rotating the secret invalidates the previous secret — coordinate the cutover with the integration owner
Step 5: Enable API Audit Logging
- Navigate to: Settings → Server → Logging
- Enable API request logging
- Export to SIEM for monitoring
Best Practices:
- One API Role and one API Client per integration
- Store client secrets in a secrets manager
- Keep token lifetimes short
- Audit API Client usage and disable dormant clients
Code Implementation
Code Pack: Terraform
# Dedicated API role with read-only privileges for security automation
resource "jamfpro_api_role" "security_automation" {
display_name = "HTH Security Automation"
privileges = toset(var.api_integration_privileges)
}
# API integration with dedicated OAuth2 credentials and scoped privileges
resource "jamfpro_api_integration" "security_automation" {
display_name = var.api_integration_name
enabled = true
authorization_scopes = toset(var.api_integration_privileges)
}
# L2: Restricted API integration with tighter token lifetime
resource "jamfpro_api_integration" "security_automation_hardened" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "${var.api_integration_name} (Hardened)"
enabled = true
access_token_lifetime_seconds = 1800
authorization_scopes = toset(var.api_integration_privileges)
}
2. Device Security Policies
2.1 Configure Password Policies
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 5.2 |
| NIST 800-53 | IA-5 |
Description
Configure device password policies through configuration profiles.
Rationale
Why This Matters:
- Device passcodes are the first barrier against physical access to a lost or stolen Mac before FileVault unlock
- Weak or short passcodes fall quickly to brute-force and shoulder-surfing, exposing local data and cached credentials
- Enforced length, complexity, history, and auto-lock remove the user’s ability to opt out of baseline protection
- Centralized profile enforcement guarantees the policy applies fleet-wide instead of relying on per-user discipline
Attack Prevented: Brute-force passcode guessing, physical device access, shoulder-surfing, credential reuse
ClickOps Implementation
Step 1: Create Password Policy Profile
- Navigate to: Computers → Configuration Profiles
- Click + New
- Select Password payload
- Configure:
- Minimum length: 12+ characters (14+ for L2)
- Require alphanumeric: Yes
- Minimum complex characters: 1+
- Maximum passcode age: 90 days (or disable for L3)
- Passcode history: Remember 5-10 passwords
- Auto-lock: 5 minutes (or less for L2)
Step 2: Scope Profile
- Configure scope:
- All computers
- Or specific groups
- Deploy profile
Time to Complete: ~20 minutes
Code Implementation
Code Pack: Terraform
# Password policy configuration profile for managed macOS devices
resource "jamfpro_macos_configuration_profile_plist" "password_policy" {
name = "HTH Password Policy (L${var.profile_level})"
description = "Device password policy per HTH hardening guide - Profile Level ${var.profile_level}"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.mobiledevice.passwordpolicy</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.password</string>
<key>PayloadUUID</key>
<string>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</string>
<key>PayloadDisplayName</key>
<string>Password Policy</string>
<key>minLength</key>
<integer>${var.password_min_length}</integer>
<key>requireAlphanumeric</key>
<true/>
<key>minComplexChars</key>
<integer>1</integer>
<key>maxPINAgeInDays</key>
<integer>${var.password_max_age_days}</integer>
<key>pinHistory</key>
<integer>${var.password_history_count}</integer>
<key>maxInactivity</key>
<integer>${var.auto_lock_minutes}</integer>
<key>maxFailedAttempts</key>
<integer>10</integer>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH Password Policy</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.password.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>B2C3D4E5-F6A7-8901-BCDE-F12345678901</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
2.2 Configure FileVault Encryption
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 3.11 |
| NIST 800-53 | SC-28 |
Description
Enforce FileVault full disk encryption on all managed macOS devices.
Rationale
Why This Matters:
- Protects data on lost/stolen devices
- Required for most compliance frameworks
- Jamf can escrow recovery keys
Attack Prevented: Data recovery from lost or stolen devices, offline disk imaging, physical access to at-rest data
ClickOps Implementation
Step 1: Create FileVault Profile
- Navigate to: Computers → Configuration Profiles
- Click + New
- Select Security & Privacy → FileVault payload
- Configure:
- Enable FileVault: Yes
- Defer to user: Enable (if not already encrypted)
- Recovery key type: Institutional or Personal
- Escrow to Jamf: Yes
Step 2: Configure Recovery Key Escrow
- Enable Escrow Personal Recovery Key
- Configure key rotation (optional)
- Store institutional key securely
Step 3: Create Remediation Policy
- Create Smart Group: Computers without FileVault
- Create policy to enforce or notify
Code Implementation
Code Pack: Terraform
# Disk encryption configuration for FileVault escrow
resource "jamfpro_disk_encryption_configuration" "filevault" {
name = "HTH FileVault Encryption"
key_type = "Individual"
file_vault_enabled_users = "Current or Next User"
}
# FileVault enforcement configuration profile
resource "jamfpro_macos_configuration_profile_plist" "filevault" {
name = "HTH FileVault Enforcement"
description = "Enforce FileVault full disk encryption per HTH hardening guide"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.MCX.FileVault2</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.filevault</string>
<key>PayloadUUID</key>
<string>C3D4E5F6-A7B8-9012-CDEF-123456789012</string>
<key>PayloadDisplayName</key>
<string>FileVault 2</string>
<key>Enable</key>
<string>On</string>
<key>Defer</key>
<true/>
<key>ShowRecoveryKey</key>
<${var.filevault_escrow_enabled ? "false" : "true"}/>
<key>UseRecoveryKey</key>
<true/>
<key>UseKeychain</key>
<false/>
<key>DeferForceAtUserLoginMaxBypassAttempts</key>
<integer>3</integer>
<key>DeferDontAskAtUserLogout</key>
<false/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH FileVault Enforcement</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.filevault.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>D4E5F6A7-B8C9-0123-DEFA-234567890123</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# Smart group to identify computers without FileVault enabled
resource "jamfpro_smart_computer_group" "filevault_not_enabled" {
name = "HTH - FileVault Not Enabled"
criteria {
name = "FileVault 2 Status"
search_type = "is not"
value = "Encrypted"
priority = 0
}
}
2.3 Configure Firewall
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 4.4 |
| NIST 800-53 | SC-7 |
Description
Enable and configure macOS firewall on all managed devices.
Rationale
Why This Matters:
- The macOS application firewall blocks unsolicited inbound connections to listening services on the endpoint
- Stealth mode stops the device from responding to network probes, reducing its visibility to attackers scanning the LAN or untrusted Wi-Fi
- Endpoints roam onto hostile networks (cafes, airports, hotels) where host-based filtering is the only perimeter the device has
- Centrally enforcing the firewall stops users from disabling it and closes the gap for lateral movement and remote service exploitation
Attack Prevented: Network-based exploitation, port scanning, lateral movement, unsolicited inbound connections
ClickOps Implementation
Step 1: Create Firewall Profile
- Navigate to: Computers → Configuration Profiles
- Click + New
- Select Security & Privacy → Firewall payload
- Configure:
- Enable firewall: Yes
- Block all incoming connections: No (or Yes for L3)
- Enable stealth mode: Yes
- Automatically allow built-in software: Yes
- Automatically allow signed software: Yes (or No for L3)
Step 2: Scope and Deploy
- Scope to all computers
- Deploy profile
Code Implementation
Code Pack: Terraform
# macOS firewall configuration profile
resource "jamfpro_macos_configuration_profile_plist" "firewall" {
name = "HTH Firewall Policy (L${var.profile_level})"
description = "Enable and configure macOS firewall per HTH hardening guide"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.security.firewall</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.firewall</string>
<key>PayloadUUID</key>
<string>E5F6A7B8-C9D0-1234-EFAB-345678901234</string>
<key>PayloadDisplayName</key>
<string>Firewall</string>
<key>EnableFirewall</key>
<true/>
<key>BlockAllIncoming</key>
<${var.firewall_block_all_incoming || var.profile_level >= 3 ? "true" : "false"}/>
<key>EnableStealthMode</key>
<${var.firewall_stealth_mode ? "true" : "false"}/>
<key>AllowSigned</key>
<${var.profile_level >= 3 ? "false" : "true"}/>
<key>AllowSignedApp</key>
<${var.profile_level >= 3 ? "false" : "true"}/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH Firewall Policy</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.firewall.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>F6A7B8C9-D0E1-2345-FABC-456789012345</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# Smart group to identify computers with firewall disabled
resource "jamfpro_smart_computer_group" "firewall_disabled" {
name = "HTH - Firewall Disabled"
criteria {
name = "Firewall Status"
search_type = "is not"
value = "On"
priority = 0
}
}
2.4 Configure Software Updates
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 7.3 |
| NIST 800-53 | SI-2 |
Description
Configure automatic software updates and patch management.
Rationale
Why This Matters:
- Unpatched macOS and application vulnerabilities are among the most reliably exploited paths to endpoint compromise
- Automatic update enforcement closes the window between a security fix shipping and devices actually applying it
- Rapid security responses address actively exploited zero-days that attackers weaponize within days of disclosure
- Patch compliance monitoring surfaces the long tail of devices that silently fall behind and become the weakest link
Attack Prevented: Exploitation of known vulnerabilities, zero-day weaponization, drive-by malware, outdated-component compromise
ClickOps Implementation
Step 1: Create Software Update Profile
- Navigate to: Computers → Configuration Profiles
- Click + New
- Select Software Update payload
- Configure:
- Automatic update check: Yes
- Download updates in background: Yes
- Install app updates: Yes
- Install macOS updates: Yes
- Install security responses: Yes (Critical)
Step 2: Configure Update Deferral (L2)
- For controlled environments:
- Defer major OS updates: 30-90 days
- Defer security updates: 0-7 days (test first)
Step 3: Create Patch Policies
- Use Jamf Pro patch management
- Configure automatic patching for critical apps
- Monitor patch compliance
Code Implementation
Code Pack: Terraform
# Automatic software update configuration profile
resource "jamfpro_macos_configuration_profile_plist" "software_updates" {
name = "HTH Software Update Policy (L${var.profile_level})"
description = "Configure automatic software updates per HTH hardening guide"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.SoftwareUpdate</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.softwareupdate</string>
<key>PayloadUUID</key>
<string>A7B8C9D0-E1F2-3456-ABCD-567890123456</string>
<key>PayloadDisplayName</key>
<string>Software Update</string>
<key>AutomaticCheckEnabled</key>
<true/>
<key>AutomaticDownload</key>
<true/>
<key>AutomaticallyInstallAppUpdates</key>
<true/>
<key>AutomaticallyInstallMacOSUpdates</key>
<true/>
<key>CriticalUpdateInstall</key>
<true/>
<key>ConfigDataInstall</key>
<true/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH Software Update Policy</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.softwareupdate.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>B8C9D0E1-F2A3-4567-BCDE-678901234567</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# L2: Software update deferral profile for controlled environments
resource "jamfpro_macos_configuration_profile_plist" "software_update_deferral" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH Software Update Deferral (L${var.profile_level})"
description = "Defer OS updates for testing before deployment per HTH hardening guide"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.applicationaccess</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.updatedeferral</string>
<key>PayloadUUID</key>
<string>C9D0E1F2-A3B4-5678-CDEF-789012345678</string>
<key>PayloadDisplayName</key>
<string>Software Update Deferral</string>
<key>enforcedSoftwareUpdateDelay</key>
<integer>${var.software_update_deferral_days}</integer>
<key>enforcedSoftwareUpdateMajorOSDeferredInstallDelay</key>
<integer>${var.software_update_deferral_days}</integer>
<key>enforcedSoftwareUpdateMinorOSDeferredInstallDelay</key>
<integer>${var.security_update_deferral_days}</integer>
<key>enforcedSoftwareUpdateNonOSDeferredInstallDelay</key>
<integer>${var.security_update_deferral_days}</integer>
<key>forceDelayedSoftwareUpdates</key>
<true/>
<key>forceDelayedMajorSoftwareUpdates</key>
<true/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH Software Update Deferral</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.updatedeferral.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>D0E1F2A3-B4C5-6789-DEFA-890123456789</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# Smart group to identify computers with outdated OS
resource "jamfpro_smart_computer_group" "os_not_current" {
name = "HTH - OS Not Current"
criteria {
name = "Operating System Version"
search_type = "less than"
value = "15.0"
priority = 0
}
}
3. CIS Benchmark Implementation
3.1 Deploy CIS Benchmark Profiles
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CM-6 |
Description
Deploy CIS macOS Benchmark security configurations using Jamf Compliance Editor.
Rationale
Why This Matters:
- CIS Benchmarks are industry-standard security baselines
- Jamf Compliance Editor generates MDM-ready profiles
- Supports both Level 1 and Level 2 hardening
Attack Prevented: Insecure macOS defaults, configuration drift from an approved baseline, exploitation of unhardened OS services
Prerequisites
- Download Jamf Compliance Editor from GitHub
- Review CIS macOS Benchmark requirements
- Test in non-production environment
Implementation
Step 1: Generate CIS Profiles
- Download Jamf Compliance Editor (the project moved to the
Jamf-Conceptsorganization; the formerjamf/Jamf-Compliance-Editorrepository now returns 404) - Open application
- Select CIS Benchmark level:
- Level 1: Baseline security
- Level 2: Enhanced security
- Generate configuration profiles
Step 2: Review and Customize
- Review generated settings
- Customize based on business needs
- Document any deviations
Step 3: Deploy Profiles
- Upload profiles to Jamf Pro
- Create test scope first
- Validate functionality
- Deploy to production
Key CIS Controls:
| CIS Control | Description | Implementation |
|---|---|---|
| 2.1.1 | Enable FileVault | Configuration Profile |
| 2.1.2 | Enable Gatekeeper | Configuration Profile |
| 2.3.1 | Set screen saver | Configuration Profile |
| 2.5.1 | Enable Firewall | Configuration Profile |
| 3.3 | Disable Remote Login | Configuration Profile |
Time to Complete: ~4 hours (testing included)
Code Implementation
Code Pack: Terraform
# CIS 2.1.2 - Gatekeeper enforcement profile (L2+)
resource "jamfpro_macos_configuration_profile_plist" "cis_gatekeeper" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH CIS - Gatekeeper Enforcement"
description = "CIS macOS Benchmark 2.1.2: Enable Gatekeeper"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.systempolicy.control</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.gatekeeper</string>
<key>PayloadUUID</key>
<string>E1F2A3B4-C5D6-7890-EFAB-901234567890</string>
<key>PayloadDisplayName</key>
<string>Gatekeeper</string>
<key>AllowIdentifiedDevelopers</key>
<${var.cis_gatekeeper_enabled ? "true" : "false"}/>
<key>EnableAssessment</key>
<true/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH CIS Gatekeeper</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.gatekeeper.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>F2A3B4C5-D6E7-8901-FABC-012345678901</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# CIS 2.3.1 - Screen saver idle time profile (L2+)
resource "jamfpro_macos_configuration_profile_plist" "cis_screensaver" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH CIS - Screen Saver Idle Time"
description = "CIS macOS Benchmark 2.3.1: Set screen saver idle time"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.screensaver</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.screensaver</string>
<key>PayloadUUID</key>
<string>A3B4C5D6-E7F8-9012-ABCD-123456789012</string>
<key>PayloadDisplayName</key>
<string>Screen Saver</string>
<key>idleTime</key>
<integer>${var.cis_screen_saver_idle_time}</integer>
<key>askForPassword</key>
<true/>
<key>askForPasswordDelay</key>
<integer>0</integer>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH CIS Screen Saver</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.screensaver.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>B4C5D6E7-F8A9-0123-BCDE-234567890123</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
# CIS 3.3 - Disable Remote Login (SSH) profile (L2+)
resource "jamfpro_macos_configuration_profile_plist" "cis_disable_remote_login" {
count = var.profile_level >= 2 && var.cis_disable_remote_login ? 1 : 0
name = "HTH CIS - Disable Remote Login"
description = "CIS macOS Benchmark 3.3: Disable Remote Login (SSH)"
distribution_method = "Install Automatically"
user_removable = false
level = "System"
redeploy_on_update = "All"
payloads = <<-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>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.MCX</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.remotelogin</string>
<key>PayloadUUID</key>
<string>C5D6E7F8-A9B0-1234-CDEF-345678901234</string>
<key>PayloadDisplayName</key>
<string>Disable Remote Login</string>
<key>com.apple.access_ssh</key>
<array/>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>HTH CIS Disable Remote Login</string>
<key>PayloadIdentifier</key>
<string>com.howtoharden.jamf.cis.remotelogin.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>D6E7F8A9-B0C1-2345-DEFA-456789012345</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
XML
scope {
all_computers = var.scope_all_computers
computer_group_ids = var.scope_all_computers ? [] : toset(var.scope_computer_group_ids)
}
}
3.2 Monitor CIS Compliance
Profile Level: L2 (Walk)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CA-7 |
Description
Monitor device compliance with CIS Benchmark configurations.
Rationale
Why This Matters:
- Deploying a benchmark profile is not the same as devices staying compliant — drift, user tampering, and failed installs erode the baseline over time
- Smart groups and extension attributes give continuous, evidence-based visibility into which devices have actually fallen out of policy
- Early detection of non-compliant devices lets you remediate before an attacker exploits the missing control
- Compliance reports provide audit-ready proof of control effectiveness for SOC 2, ISO 27001, and regulatory assessments
Attack Prevented: Configuration drift, silent control failure, undetected non-compliance, audit evidence gaps
ClickOps Implementation
Step 1: Create Smart Groups
- Create smart groups for compliance monitoring:
- FileVault not enabled
- Firewall not enabled
- OS not updated
- Non-compliant configurations
Step 2: Configure Compliance Reporting
- Navigate to: Settings → Computer Management → Extension Attributes
- Create extension attributes for CIS checks
- Use in smart groups for compliance tracking
Step 3: Generate Compliance Reports
- Use Jamf Pro reporting
- Export for compliance documentation
- Track compliance trends
Code Implementation
Code Pack: Terraform
# Extension attribute to track CIS compliance status via script (L2+)
resource "jamfpro_computer_extension_attribute" "cis_compliance" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH CIS Compliance Status"
description = "Reports CIS macOS Benchmark compliance status"
data_type = "String"
enabled = true
input_type = "SCRIPT"
inventory_display_type = "EXTENSION_ATTRIBUTES"
script_contents = var.cis_compliance_ea_script != "" ? var.cis_compliance_ea_script : <<-BASH
#!/bin/bash
# HTH CIS Compliance Check Extension Attribute
# Reports: Compliant, Non-Compliant, or Partial
ISSUES=0
CHECKS=0
# Check FileVault (CIS 2.1.1)
CHECKS=$((CHECKS + 1))
FV_STATUS=$(fdesetup status 2>/dev/null)
if [[ "$FV_STATUS" != *"FileVault is On"* ]]; then
ISSUES=$((ISSUES + 1))
fi
# Check Firewall (CIS 2.5.1)
CHECKS=$((CHECKS + 1))
FW_STATUS=$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2>/dev/null)
if [[ "$FW_STATUS" != *"enabled"* ]]; then
ISSUES=$((ISSUES + 1))
fi
# Check Gatekeeper (CIS 2.1.2)
CHECKS=$((CHECKS + 1))
GK_STATUS=$(spctl --status 2>/dev/null)
if [[ "$GK_STATUS" != *"assessments enabled"* ]]; then
ISSUES=$((ISSUES + 1))
fi
# Check Remote Login disabled (CIS 3.3)
CHECKS=$((CHECKS + 1))
SSH_STATUS=$(systemsetup -getremotelogin 2>/dev/null)
if [[ "$SSH_STATUS" == *"On"* ]]; then
ISSUES=$((ISSUES + 1))
fi
# Report result
if [ $ISSUES -eq 0 ]; then
echo "<result>Compliant</result>"
elif [ $ISSUES -lt $CHECKS ]; then
echo "<result>Partial ($((CHECKS - ISSUES))/$CHECKS passed)</result>"
else
echo "<result>Non-Compliant</result>"
fi
BASH
}
# Smart group: CIS non-compliant computers (L2+)
resource "jamfpro_smart_computer_group" "cis_non_compliant" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH - CIS Non-Compliant"
criteria {
name = "HTH CIS Compliance Status"
search_type = "is not"
value = "Compliant"
priority = 0
}
}
# Smart group: FileVault not enabled (L2+)
resource "jamfpro_smart_computer_group" "cis_filevault_missing" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH - CIS FileVault Not Enabled"
criteria {
name = "FileVault 2 Status"
search_type = "is not"
value = "Encrypted"
priority = 0
}
}
# Smart group: OS not updated (L2+)
resource "jamfpro_smart_computer_group" "cis_os_outdated" {
count = var.profile_level >= 2 ? 1 : 0
name = "HTH - CIS OS Not Updated"
criteria {
name = "Number of Available Updates"
search_type = "more than"
value = "0"
priority = 0
}
}
4. Monitoring & Compliance
4.1 Enable Audit Logging
Profile Level: L1 (Crawl)
| Framework | Control |
|---|---|
| CIS Controls | 8.2 |
| NIST 800-53 | AU-2 |
Description
Enable comprehensive audit logging for Jamf Pro activities.
Rationale
Why This Matters:
- Jamf Pro admin actions — profile pushes, script runs, MDM commands, and account changes — are high-impact and must be attributable to a specific actor
- Without comprehensive logs, a compromised admin account or malicious insider can tamper with the fleet undetected and without forensic trail
- Forwarding logs to a SIEM enables real-time alerting on anomalous activity and preserves evidence beyond the console’s local retention
- Audit trails are a hard requirement for SOC 2, ISO 27001, and most regulatory frameworks
Attack Prevented: Insider abuse, undetected admin compromise, repudiation, evidence tampering
ClickOps Implementation
Step 1: Configure Server Logs
- Navigate to: Settings → Server → Logging
- Configure log retention
- Enable all relevant log types
Step 2: Configure SIEM Integration
- Configure syslog forwarding to SIEM
- Or use Jamf Pro webhooks for events
- Monitor for security events
Code Implementation
Code Pack: Terraform
# Webhook for admin login events to SIEM
resource "jamfpro_webhook" "admin_login" {
count = var.siem_webhook_url != "" ? 1 : 0
name = "HTH Audit - Admin Login"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "JSSLoginSuccess"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
# Webhook for computer policy changes
resource "jamfpro_webhook" "policy_change" {
count = var.siem_webhook_url != "" ? 1 : 0
name = "HTH Audit - Policy Change"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "PolicyFinished"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
# Webhook for computer enrollment events
resource "jamfpro_webhook" "computer_enrollment" {
count = var.siem_webhook_url != "" ? 1 : 0
name = "HTH Audit - Computer Enrollment"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "ComputerAdded"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
# Webhook for computer check-in events (L2+)
resource "jamfpro_webhook" "computer_checkin" {
count = var.siem_webhook_url != "" && var.profile_level >= 2 ? 1 : 0
name = "HTH Audit - Computer Check-In"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "ComputerCheckIn"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
# Webhook for push command sent events (L2+)
resource "jamfpro_webhook" "push_sent" {
count = var.siem_webhook_url != "" && var.profile_level >= 2 ? 1 : 0
name = "HTH Audit - Push Sent"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "PushSent"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
# Webhook for mobile device enrollment events (L2+)
resource "jamfpro_webhook" "mobile_enrollment" {
count = var.siem_webhook_url != "" && var.profile_level >= 2 ? 1 : 0
name = "HTH Audit - Mobile Device Enrollment"
enabled = true
url = var.siem_webhook_url
content_type = "application/json"
event = "MobileDeviceEnrolled"
connection_timeout = 5
read_timeout = 10
authentication_type = var.siem_webhook_auth_type
username = var.siem_webhook_username
password = var.siem_webhook_password
}
4.2 Key Events to Monitor
| Event | Detection Use Case |
|---|---|
| Admin login | Unauthorized access |
| Profile deployment | Unauthorized configuration |
| Script execution | Malicious scripts |
| Policy changes | Configuration drift |
| Failed enrollment | Enrollment issues |
| MDM command | Unauthorized commands |
5. Compliance Quick Reference
SOC 2 Trust Services Criteria Mapping
| Control ID | Jamf Control | Guide Section |
|---|---|---|
| CC6.1 | Console access control | 1.1 |
| CC6.6 | Device policies | 2.1 |
| CC6.7 | Encryption | 2.2 |
| CC7.1 | Firewall | 2.3 |
| CC7.2 | Audit logging | 4.1 |
NIST 800-53 Rev 5 Mapping
| Control | Jamf Control | Guide Section |
|---|---|---|
| AC-6(1) | Admin roles | 1.1 |
| IA-5 | Password policy | 2.1 |
| SC-28 | FileVault encryption | 2.2 |
| SC-7 | Firewall | 2.3 |
| CM-6 | CIS Benchmarks | 3.1 |
Appendix A: Plan Compatibility
| Feature | Jamf Now | Jamf Pro | Jamf Connect |
|---|---|---|---|
| Configuration Profiles | Basic | Full | N/A |
| Scripts | ❌ | ✅ | N/A |
| Extension Attributes | ❌ | ✅ | N/A |
| Patch Management | ❌ | ✅ | N/A |
| SSO | ❌ | ✅ | N/A |
| API Access | Limited | Full | N/A |
| SIEM Integration | ❌ | ✅ | N/A |
Appendix B: References
Official Jamf Hardening Documentation:
learn.jamf.comis a JavaScript-rendered documentation host: the two links above are the canonical/r/URLs that the older/bundle/.../page/...htmlforms redirect to, and they render content only to a browser. Automated fetchers will see an empty shell.
API & Developer Tools:
Security Incidents:
- No major public security incidents affecting Jamf’s hosted infrastructure identified as of February 2026. Product-level CVEs (e.g., broken access control in Jamf Pro Server before 10.46.1) have been patched through standard release cycles. Tier 3/4 research sources were not re-surveyed in this pass.
Community Resources:
Third-Party Baselines:
- The CIS macOS Benchmark backs section 3, but the current benchmark major version was not re-verified in this pass — CIS numbering shifts between majors, so treat the control numbers in 3.1 as name-matched rather than version-pinned. DISA STIG and CISA SCuBA coverage for Jamf Pro was not surveyed.
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2026-08-08 | 0.2.0 | draft | Rewrite 1.2 around API Roles and API Clients (OAuth client credentials), superseding user-account API auth; fix Jamf Compliance Editor repo URL (moved to Jamf-Concepts); update learn.jamf.com links to canonical /r/ form (host renders only to browsers, so a browser pass is still needed to re-verify page content); add Attack Prevented to 1.1, 2.2, 3.1; remove non-standard lang= from pack includes; drop Trust Center references. DDM / Managed Software Updates coverage and current CIS macOS Benchmark numbering remain unverified and are deferred | Claude Code (Opus 4.8) |
| 2025-02-05 | 0.1.0 | draft | Initial guide with server security, device policies, and CIS implementation | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide