
This guide walks through setting up the Okta Terraform provider, authenticating to it, and managing the core Okta resources you'll touch most often: users, groups, applications, and policies. It also gets into operational practices, CI/CD integration, and the errors that tend to come up.
You'll need to set up two things: your Okta account and your local Terraform environment.
It can be helpful to use a TACO as well.
The Okta Terraform provider comes from the official Terraform Registry. To configure it, create a file named versions.tf and set the provider source and version.
// versions.tf
terraform {
required_providers {
okta = {
source = "okta/okta"
version = "~> 4.17.0" // Check the Terraform Registry for the latest version
}
}
}Running terraform init in your configuration directory will download and install the specified provider version.
The provider has to authenticate to your Okta organization's API. There are two main methods: API Token and OAuth 2.0. Use OAuth 2.0 for all environments, especially production.
This method uses a static API token generated from the Okta admin console (Security > API > Tokens).
Configuration:
// main.tf
provider "okta" {
org_name = "your-org-name" // e.g., dev-123456
base_url = "okta.com" // e.g., okta.com, oktapreview.com
api_token = var.okta_api_token // Provided as a variable
}
variable "okta_api_token" {
description = "Okta API Token"
type = string
sensitive = true
}This method uses an Okta API Service Application set up for the OAuth 2.0 client credentials flow with a public/private key pair. It gives you granular, scope-based access control.
Setup Steps:
okta.groups.manage, okta.apps.read).Configuration:
// main.tf
provider "okta" {
org_name = "your-org-name"
base_url = "okta.com"
client_id = var.okta_oauth_client_id
private_key = var.okta_oauth_private_key
scopes = ["okta.groups.manage", "okta.apps.read", "okta.policies.manage"]
}
variable "okta_oauth_client_id" {
description = "Client ID for the Okta Service App"
type = string
sensitive = true
}
variable "okta_oauth_private_key" {
description = "Private key (PEM format) for the Okta Service App"
type = string
sensitive = true
}Never hardcode secrets in configuration files. The best approach is a secrets management tool like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. You can also use environment variables, which the Okta provider picks up automatically (OKTA_CLIENT_ID, OKTA_PRIVATE_KEY, etc.).
You manage Okta resources with the standard Terraform workflow: init, plan, and apply. Read the output of terraform plan carefully, since it's what keeps you from making unintended changes to your identity infrastructure.
| Okta Object | Terraform Resource(s) | Key Arguments/Purpose | Example Required Scopes |
|---|---|---|---|
| User | okta_user, okta_user_schema_property |
login, email, firstName, lastName, status, custom_profile_attributes |
okta.users.manage, okta.users.read, okta.userSchemas.manage |
| Group | okta_group, okta_group_schema_property |
name, description, custom_profile_attributes |
okta.groups.manage, okta.groups.read, okta.groupSchemas.manage |
| Group Membership | okta_group_memberships, okta_user_group_memberships |
group_id, user_id, users (list), groups (list) |
okta.groups.manage |
| OIDC Application | okta_app_oauth |
label, type, grant_types, redirect_uris, token_endpoint_auth_method |
okta.apps.manage, okta.apps.read |
| SAML Application | okta_app_saml |
label, preconfigured_app, sso_url, audience, attribute_statements |
okta.apps.manage, okta.apps.read |
| Sign-On Policy | okta_policy_signon |
name, status, priority, groups_included, session settings |
okta.policies.manage, okta.policies.read |
| MFA Enrollment Policy | okta_policy_mfa |
name, status, priority, groups_included, factor enrollment settings |
okta.policies.manage, okta.policies.read |
| Policy Rule | okta_policy_rule_signon, okta_policy_rule_mfa |
policy_id, name, priority, conditions and actions |
okta.policies.manage, okta.policies.read |
| Authenticator | okta_authenticator |
name, key, status, settings |
okta.authenticators.manage, okta.authenticators.read |
Groups are the backbone of access control. You can manage memberships from the group side or the user side.
// groups.tf
resource "okta_group" "engineering_team" {
name = "Engineering Team"
description = "All members of the engineering department"
}
// User resources (e.g., okta_user.jdoe) must be defined elsewhere
resource "okta_group_memberships" "engineering_team_members" {
group_id = okta_group.engineering_team.id
users = [
okta_user.jdoe.id,
okta_user.jsmith.id
]
}You can put application configs in code, which keeps them consistent across environments.
OIDC Service Application:
// apps_oidc.tf
resource "okta_app_oauth" "backend_api_service" {
label = "Backend API Service"
type = "service"
grant_types = ["client_credentials"]
token_endpoint_auth_method = "client_secret_basic"
}More on OIDC.
SAML Application:
// apps_saml.tf
resource "okta_app_saml" "aws_account_federation" {
preconfigured_app = "amazon_aws"
label = "AWS Account Federation (SAML)"
attribute_statements {
name = "https://aws.amazon.com/SAML/Attributes/Role"
namespace = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
values = ["arn:aws:iam::123456789012:saml-provider/Okta,arn:aws:iam::123456789012:role/OktaPowerUser"]
}
}Policies and their rules are the core of Okta's security posture.
// policies.tf
data "okta_group" "everyone" {
name = "Everyone"
}
resource "okta_policy_password" "strong_password_policy" {
name = "Strong Password Policy"
status = "ACTIVE"
groups_included = [data.okta_group.everyone.id]
password_min_length = 14
password_min_lowercase = 1
password_min_uppercase = 1
password_min_number = 1
password_min_symbol = 1
password_history_count = 6
password_max_age_days = 90
}Example password policy with Terraform
One thing that matters a lot in policy management is priority. Okta evaluates policies of the same type in order of their priority value (lowest number wins). To get a deterministic order, set the priority argument explicitly on every policy of the same type, and use the depends_on meta-argument to control creation order.
// policies.tf
resource "okta_policy_signon" "contractor_access_policy" {
name = "Contractor Access Policy"
priority = 10
status = "ACTIVE"
# ... other settings
}
resource "okta_policy_signon" "employee_access_policy" {
name = "Employee Access Policy"
priority = 20
status = "ACTIVE"
# ... other settings
depends_on = [okta_policy_signon.contractor_access_policy]
}Managing Okta with Terraform over the long run takes a few habits beyond the basic workflow.
Split your codebase into files that make sense (e.g., variables.tf, apps_oidc.tf, policies.tf). For reusable components, build Terraform modules. That keeps things consistent and easier to maintain. To manage multiple Okta environments (dev, prod), use Terraform Workspaces, which let you keep separate state files for the same configuration.
The Terraform state file maps your configuration to real-world resources. For any team or production use, you have to store the state file remotely. Remote backends like Terraform Cloud, AWS S3, or Azure Blob Storage give you state locking (so two runs can't clobber each other), secure storage, and shared access.
S3 Backend Example:
// backend.tf
terraform {
backend "s3" {
bucket = "my-secure-okta-terraform-state"
key = "okta/production/terraform.tfstate"
region = "us-west-2"
dynamodb_table = "terraform-okta-state-locks"
encrypt = true
}
}Configuration drift happens when the actual state of your Okta resources no longer matches the state in your code, usually because someone made a manual change in the Okta UI.
terraform plan often. The plan output shows any discrepancies.terraform import to bring the resource's current state under Terraform management.More on how to detect and remediate Terraform drift.
Terraform can fire off a lot of API calls and hit Okta's rate limits. To manage this:
max_api_capacity argument in the provider block to tell Terraform to pause when it gets close to a set percentage of your organization's API rate limit.Automate your Okta changes through a CI/CD pipeline. A typical workflow looks like this:
terraform plan and posts the output to the pull request for review.terraform apply -auto-approve to deploy the changes to the target environment.Conceptual GitHub Actions Step:
- name: Terraform Plan
id: plan
run: terraform plan -no-color
env:
OKTA_CLIENT_ID: ${{ secrets.OKTA_CLIENT_ID }}
OKTA_PRIVATE_KEY: ${{ secrets.OKTA_PRIVATE_KEY_PEM }}
# ... other env varsSee the using Gitlab with Terraform guide for more info.
When you hit errors, turning on debug logging tells you the most.
# Enable debug logging for the current command
TF_LOG=DEBUG terraform plan
# Log debug output to a file
TF_LOG=DEBUG TF_LOG_PATH="terraform-debug.log" terraform applyWatch out: debug logs can contain sensitive data. Redact secrets before you share logs.
Common errors usually come down to:
terraform plan -replace="resource.address" to force a recreation of the problematic resource, which refreshes its state.depends_on to manage dependencies between resources created in the same configuration.Using GitLab? You might be interested in the following guide too:
