Managing Jamf Protect with Terraform: The Jamf Protect Provider

Prev Next

In our previous write-up, Managing Jamf Configuration with Terraform: An Introduction, we walked through the basics of Infrastructure as Code and how to get started with Terraform and Jamf Pro. If you haven't read that yet, we'd recommend starting there as it covers the fundamentals of Terraform, installation, project structure, and state management.

This time around, we're focusing on Jamf Protect. We've built a dedicated Terraform provider that lets you manage your Jamf Protect product configuration as code - plans, action configurations, exception sets, and much more.

Why a dedicated provider for Jamf Protect?

Just like Jamf Pro, Jamf Protect is a product in its own right, and part of the Jamf Platform family. It has its own API surface (GraphQL-based) and its own set of resources that are distinct from Jamf Pro and other Jamf Platform microservices (e.g. Blueprints and Compliance Benchmarks). Things like endpoint security plans, telemetry configurations, and removable storage policies all live in Jamf Protect and need their own provider to manage them properly.

If you've been managing your Jamf Protect configuration through the console, you'll know that recreating a setup across tenants or rolling back a change means a lot of manual work. With this provider, your entire Jamf Protect configuration can live in Git, go through code review, and be applied consistently across environments.

What does it cover?

The provider ships with 16 resources and 18 data sources. Here are some of the key areas it covers:

Threat Detection & Prevention

  • Custom Prevent Lists - allow/block lists by Team ID, file hash, CDHash, or signing ID.
  • Exception Sets - exceptions for analytics and endpoint security controls.

Endpoint Configuration

  • Plans - full endpoint security configurations including threat prevention modes, communication protocols, reporting intervals, and device information collection.
  • Action Configurations - alert data enrichment and reporting endpoints (HTTP, Kafka, cloud).
  • Telemetry - event and log collection: security events, authentication, file integrity monitoring, performance metrics, and more.

Device Control

  • Removable Storage Control Sets - USB device policies with overrides by vendor ID, product ID, or serial number.

Operational Settings

  • Data Forwarding - route data to Amazon S3, Microsoft Sentinel, or cloud endpoints.
  • Data Retention - tenant-wide retention policies.
  • Change Management - configuration freezes.
  • Unified Logging Filters - macOS unified log collection using NSPredicate filters.

Access Control

  • Users, Groups, Roles, and API Clients - manage access and permissions in code.

All resources support full CRUD operations and terraform import.

It's worth noting that jamfprotect_change_management, jamfprotect_data_forwarding, and jamfprotect_data_retention are singleton resources - they manage tenant-wide settings rather than individually identifiable objects.

Getting started

If you followed along with the introduction article, you should already have Terraform installed and a basic understanding of how projects are structured. The steps here are similar.

1. Create an API client in Jamf Protect

Head to Administration > API Clients in your Jamf Protect console and create a new API client with the permissions you need for the resources you want to manage.

2. Configure the provider

Add the provider to your Terraform configuration:

terraform {
  required_providers {
    jamfprotect = {
      source  = "Jamf-Concepts/jamfprotect"
      version = "0.1.0"
    }
  }
}

provider "jamfprotect" {
  url           = "https://your-tenant.protect.jamfcloud.com"
  client_id     = var.jamfprotect_client_id
  client_secret = var.jamfprotect_client_secret
}

You can also use environment variables if you prefer to keep the provider block clean:

export JAMFPROTECT_URL="https://your-tenant.protect.jamfcloud.com"
export JAMFPROTECT_CLIENT_ID="your-client-id"
export JAMFPROTECT_CLIENT_SECRET="your-client-secret"

As with the Jamf Pro provider, make sure these credentials don't end up in your Git repository. Use a terraform.tfvars file and add it to your .gitignore.

3. Define your resources

Here's an example of an action configuration that defines what data is collected and where alerts are sent:

resource "jamfprotect_action_configuration" "default" {
  name = "Default Action Config"

  alert_data_collection = {
    binary_included_data_attributes                = ["Sha256", "Signing Information"]
    download_event_included_data_attributes        = ["File"]
    file_included_data_attributes                  = ["Sha256", "Signing Information"]
    file_system_event_included_data_attributes     = ["File", "Process"]
    gatekeeper_event_included_data_attributes      = ["Blocked Process"]
    group_included_data_attributes                 = ["Name"]
    keylog_register_event_included_data_attributes = ["Source Process"]
    process_included_data_attributes               = ["Args", "Signing Information", "Binary", "User", "Parent"]
    process_event_included_data_attributes         = ["Process"]
    screenshot_event_included_data_attributes      = ["File"]
    synthetic_click_event_included_data_attributes = ["Process"]
    user_included_data_attributes                  = ["Name"]
  }

  jamf_protect_cloud_endpoint = {
    collect_alerts = ["low", "medium", "high"]
    collect_logs   = ["telemetry"]
  }
}

A removable storage policy that blocks all USB storage except approved encrypted drives:

resource "jamfprotect_removable_storage_control_set" "strict" {
  name                               = "Strict USB Policy"
  description                        = "Block all removable storage except encrypted devices."
  default_permission                 = "Prevent"
  default_local_notification_message = "Removable storage devices are not permitted."

  override_encrypted_devices = [
    {
      permission = "Read and Write"
    },
  ]
}

An exception set to exclude known IT administration tools from threat prevention:

resource "jamfprotect_exception_set" "it_admin_tools" {
  name        = "IT Admin Tools"
  description = "Exceptions for trusted IT administration software."

  exceptions = [
    {
      type     = "Override Endpoint Threat Prevention"
      sub_type = "Process"
      rules = [
        {
          rule_type = "Team ID"
          value     = "EQHXZ8M8AV"
        },
      ]
    },
  ]
}

You can also use data sources to reference existing resources rather than creating them. Here we look up the Jamf Managed Default Exceptions set that already exists in the tenant:

data "jamfprotect_exception_sets" "all" {}

locals {
  jamf_managed_default_exceptions = [
    for es in data.jamfprotect_exception_sets.all.exception_sets :
    es if es.name == "Jamf Managed Default Exceptions"
  ][0]
}

These resources can then be tied together in a plan, which is the configuration that gets deployed to endpoints:

resource "jamfprotect_plan" "endpoint_security" {
  name        = "Endpoint Security Plan"
  description = "Standard endpoint security plan with threat prevention."

  action_configuration          = jamfprotect_action_configuration.default.id
  removable_storage_control_set = jamfprotect_removable_storage_control_set.strict.id

  exception_sets = [
    jamfprotect_exception_set.it_admin_tools.id,
    local.jamf_managed_default_exceptions.uuid,
  ]

  endpoint_threat_prevention = "Block and report"
  advanced_threat_controls   = "Block and report"
  tamper_prevention          = "Block and report"

  reporting_interval            = 1440
  compliance_baseline_reporting = true
  auto_update                   = true
  communications_protocol       = "MQTT:443"
  log_level                     = "Error"

  report_architecture  = true
  report_hostname      = true
  report_serial_number = true
  report_model_name    = true
  report_os_version    = true
}

The plan references other resources by their IDs, so Terraform understands the dependency graph and creates everything in the right order.

4. Apply your configuration

The same commands you learned in the introduction apply here:

terraform init       # Initialize and download the provider
terraform plan       # Review what will be created, changed, or destroyed
terraform apply      # Apply the changes (you'll be asked to confirm)

Bringing an existing tenant under management

If you already have a Jamf Protect tenant with an existing configuration, you don't need to start from scratch. Every resource in the provider supports terraform import, so you can bring existing resources under Terraform management without recreating them.

Discovering existing resources with Terraform query

If you're running Terraform 1.14+, the provider supports list resources - a new feature that lets you query your existing infrastructure directly from Terraform. This is useful for discovering what's already in your tenant before importing it.

Create a query file (e.g. discover.tfquery.hcl):

list "jamfprotect_plan" "all" {
  provider         = jamfprotect
  include_resource = true
}

list "jamfprotect_exception_set" "custom" {
  provider = jamfprotect

  config {
    name_prefix = "Custom"
  }
}

Then run:

terraform query -generate-config-out=generated.tf

Terraform will query your Jamf Protect tenant, return the matching resources along with their IDs, and generate both the resource blocks and import blocks into generated.tf. Every list resource in the provider supports a name_prefix filter in the config block (or email_prefix for users) so you can narrow results down to what you're looking for.

All resource types support list queries except the following singleton resources:

  • jamfprotect_change_management
  • jamfprotect_data_forwarding
  • jamfprotect_data_retention

Importing resources

Once you have the IDs you need, you can use import blocks to bring them under management. With Terraform 1.5+:

import {
  to = jamfprotect_plan.endpoint_security
  id = "your-plan-uuid"
}

Run terraform plan to generate the configuration for the imported resource, refine it as needed, and from that point forward it's managed as code. For more on bulk importing resources, see HashiCorp's bulk import documentation.

Using alongside other Jamf providers

This provider is designed to work alongside the other Terraform providers in the Jamf ecosystem. You can use it together with:

Between these providers, you can manage your full Jamf environment as code from a single Terraform project.

Background

This provider was originally created by James Smith (@smithjw), who generously donated the source code to Jamf Concepts. Thank you to James for the foundational work that made this possible.

The provider is open-source under the MPL 2.0 license and built on HashiCorp's Terraform Plugin Framework (Protocol v6). It includes unit and acceptance tests throughout, and we welcome contributions - bug reports, feature requests, and pull requests.

What's next

This is a v0.1.0 release and we're actively gathering feedback. If you try it out, we'd like to hear what works and what doesn't. Open an issue on the GitHub repository or reach out on Jamf Nation.

References