Featured image of post How we manage our Security Policies in Terraform

How we manage our Security Policies in Terraform

Part 6, Automatically managing Auth Policies, Authenticators, Global Session Policies, and other ideas.

Introduction

Last week we took on:

  • Overview: Addressing Okta’s 100 network zone limit through automation with Terraform.
  • Network Zones Challenges: Managing SaaS services, office networks, and proxies efficiently under zone limits.
  • Cloud Services: Automating IP address handling for API key restrictions using dynamic data sources.
  • Office Network Zones: Leveraging Meraki Terraform provider for dynamic office gateway IP configurations.
  • Proxies: Automating proxy blocklists and policy categories with Okta’s new dynamic IP service features.
  • Key Improvements:
    • Incorporating multiple data formats (JSON, plaintext) and sources (local, public, private).
    • Chunking IP ranges to respect Okta’s 100 gateway limit per zone.
    • Transitioning to Okta’s new features like IP Exempt Zones to handle false positives.
  • Limitations: Issues with Terraform Cloud environment variables and Okta’s IP blocklist sensitivity.
  • Best Practices:
    • Avoid overreaching categories like “All IP Services” to minimize false positives.
    • Leverage IP Exempt Zones for static IPs sparingly.
    • Utilize debugging outputs to inspect and validate configurations dynamically.
  • Resources: Contact #okta-terraform on MacAdmins for community insights or visit alternative forums.
  • Future Topics: Automating security policies (authentication, password, authenticator policies).

This week, we are looking at automating Security Policies, like Authenticators, Global Session Policies, Application Authentication Policies, Password Policies & Rules, and more. Let’s check it out below.

Manage Security Policies

This will be less about Automation and more about controlling security policies through Terraform. You could automate this through various situations, events, if statements, etc., but keep it simple (stupid).

Managing Authenticators

Authenticator Configurations

This is probably the simplest thing to do with Terraform and Okta.

We need to import all of our existing authenticators from Okta into Terraform’s State. As we use Kolide, but only have Kolide in a single environment, we need to have special conditions to only import it in the production environment, not the preview environment. Initially, we tried to deploy our imports this way, as I was hoping to make this very easy… but… of course not.

Warning

The code below does not work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

locals {
  authenticator_import_ids = {
    # List Authenticator IDs
    okta-preview = {
      "okta_verify"        = "aut1-preview-fakeid"
      "fido"               = "aut2-preview-fakeid"
      "generic_totp"       = "aut3-preview-fakeid"
      "email_magic_link"   = "aut4-preview-fakeid"
      "password"           = "aut5-preview-fakeid"
      
    }
    okta-production = {
      "okta_verify"        = "aut1-production-fakeid"
      "fido"               = "aut2-production-fakeid"
      "generic_totp"       = "aut3-production-fakeid"
      "email_magic_link"   = "aut4-production-fakeid"
      "password"           = "aut5-production-fakeid"
      "external_idp"       = "aut6-production-fakeid"
    }
  }

  # Set active import list based on workspace
  active_authenticator_import_ids = local.authenticator_import_ids[terraform.workspace]
}

# Import block to handle all active authenticators
import {
  for_each = local.active_authenticator_import_ids
  to       = okta_authenticator.${each.key}
  id       = each.value
}

Unfortunately, we are not able to use the import block or the for_each functions in this way. We constantly were receiving Terraform plan errors, though it would make the code such a lot easier to read and write. So we have to structure the code in a different way:

Tip

The code below is working.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
locals {
  okta_verify = {
    okta-preview = "fakeauth1id"
    okta-production = "fakeauth2id"
  }

  fido = {
    okta-preview = "fakeauth3id"
    okta-production = "fakeauth4id"
  }

  google_otp = {
    okta-preview = "fakeauth5id"
    okta-production = "fakeauth6id"
  }

  email_magic_link = {
    okta-preview = "fakeauth7id"
    okta-production = "fakeauth8id"
  }

  password = {
    okta-preview = "fakeauth9id"
    okta-production = "fakeauth10id"
  }

  external_idp = {
    okta-preview = "" # Excluded from preview
    okta-production = "fakeauth11id"
  }
}

# # Import block to handle all active authenticators for both environments
import {
  id = local.okta_verify[terraform.workspace]
  to = okta_authenticator.okta_verify
}

import {
  id = local.fido[terraform.workspace]
  to = okta_authenticator.fido
}

import {
  id = local.google_otp[terraform.workspace]
  to = okta_authenticator.google_otp
}

import {
  id = local.email_magic_link[terraform.workspace]
  to = okta_authenticator.email_magic_link
}

import {
  id = local.password[terraform.workspace]
  to = okta_authenticator.password
}

But what do we do with the External IDP? Unfortunately here, we would need to select the workspace, and manually import it at the command line.

1
2
3
4
terraform workspace list
terraform workspace select $yourworkspace
terraform state list | grep -i okta_authenticator # To double check what authenticators are already in the state
terraform import okta_authenticator.$resourcename $resourceidoftenant

Now, we can manage the Authenticators in Terraform.

As an example, Okta Verify:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
resource "okta_authenticator" "okta_verify" {
  key    = "okta_verify"
  name   = "Okta Verify"
  status = "ACTIVE"

  settings = jsonencode({
    "allowedFor" : "any"
    "compliance" : {
      "fips" : "OPTIONAL"
    },
    "channelBinding" : {
      "style" : "NUMBER_CHALLENGE",
      "required" : "HIGH_RISK_ONLY"
    },
    "userVerification" : "REQUIRED",
    "enrollmentSecurityLevel" : "HIGH",
    "userVerificationMethods" : [
      "BIOMETRICS"
    ],
    "userVerification" : "REQUIRED"
  })
  lifecycle {
    create_before_destroy = true
  }
}

This would setup the Okta Verify Configuration, but it wouldn’t do two things:

  1. Enforce Okta Verify Push
  2. Enforce Okta Verify FastPass

This is not currently manageable in the authenticator resource, so how do we resolve this?

In a variable.tf file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
variable "TFC_OKTA_ORG_NAME" {

}

variable "TFC_OKTA_BASE_URL" {

}

variable "TFC_OKTA_API_TOKEN" {
  sensitive = true
}

In the Authenticator’s Terraform file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# OV Push Authenticator Methods cannot be managed via provider at the moment. Additional call to enable Okta Verify Push & FastPass below (workaround)
resource "null_resource" "enable_ov_push" {
  depends_on = [okta_authenticator.okta_verify]
  triggers = {
    always_run = "${timestamp()}" # Forces this resource to run every time
  }

  provisioner "local-exec" {
    command = "curl -X POST -H \"Authorization:SSWS ${var.TFC_OKTA_API_TOKEN}\" https://${var.TFC_OKTA_ORG_NAME}.${var.TFC_OKTA_BASE_URL}/api/v1/authenticators/${okta_authenticator.okta_verify.id}/methods/push/lifecycle/activate"
  }
}



resource "null_resource" "enable_ov_fastpass" {
  depends_on = [okta_authenticator.okta_verify]
  triggers = {
    always_run = "${timestamp()}" # Forces this resource to run every time
  }

  provisioner "local-exec" {
    command = "curl -X POST -H \"Authorization:SSWS ${var.TFC_OKTA_API_TOKEN}\" https://${var.TFC_OKTA_ORG_NAME}.${var.TFC_OKTA_BASE_URL}/api/v1/authenticators/${okta_authenticator.okta_verify.id}/methods/signed_nonce/lifecycle/activate"
  }
}
# End section for workaround

This would then allow for us to be sure that we are enforcing Okta Verify Push and FastPass on every single run, making sure that if someone has accidentally opted to turn off the configurations, they get re-enabled.

For the External IDP example in the beginning of this section, we need the ability to manage this without it being imported into Okta in the preview environment, the best way to do that is to use the count function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
resource "okta_authenticator" "external_idp" {
  count  = terraform.workspace == "okta-production" ? 1 : 0

  name   = "Kolide"
  key    = "external_idp"
  status = "ACTIVE"

  provider {
    type = "CLAIMS"
    configuration {
      idp_id = "0oaci-externalidentity-id"
    }
  }
}

Authenticator Enrollment Configuration

I can’t post what our actual Authenticator Enrollment Configuration looks like, but, what I can do is provide the code we used to import our existing Okta Authenticator Enrollment configuration via a python script.

The github okta-terraform-tools repo, has a Policy - MFA Enrollment script that will generate a full blown configuration for you automatically using the Okta API and the version 4.14.0 of the Okta Terraform Provider.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./bin/python3 main.py --full-domain $YOURDOMAIN --api-token $YOURAPITOKEN --terraform-fmt --output ./policy-mfa-enroll.tf
Organization pipeline: idx -> is_oie set to True
Terraform configuration written to ./policy-mfa-enroll.tf
policy-mfa-enroll.tf
Formatted ./policy-mfa-enroll.tf successfully.

$ ./bin/python3 main.py --help
usage: main.py [-h] [--subdomain SUBDOMAIN] [--domain DOMAIN] [--full-domain FULL_DOMAIN] --api-token API_TOKEN [--output OUTPUT] [--terraform-fmt]

Generate Terraform code for Okta MFA policies and rules from the Okta API.

options:
  -h, --help            show this help message and exit
  --subdomain SUBDOMAIN
                        Subdomain for the Okta domain
  --domain DOMAIN       Domain for the Okta domain
  --full-domain FULL_DOMAIN
                        Full domain for the Okta domain (without protocol)
  --api-token API_TOKEN
                        Okta API token (used only for fetching policies and rules)
  --output OUTPUT       Output file name for the Terraform configuration
  --terraform-fmt       Run 'terraform fmt' on the generated file

Running this, will automatically determine if the tenant is an OIE environment, and then generate an OIE compatible environment or a Classic Engine compatible environment terraform file.

Which will then generate something that looks like this (based on a demo environment):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
data "okta_group" "group_00ge1abom64vn6eI94x7" {
  id = "00ge1abom64vn6eI94x7"
}

import {
  to = okta_policy_mfa.passwordless_requirement
  id = "00pe1a9vsaFNS9dLd4x7"
}

import {
  to = okta_policy_rule_mfa.passwordless_requirement_default
  id = "00pe1a9vsaFNS9dLd4x7/0pre1aa40kevzyvxK4x7"
}

import {
  to = okta_policy_mfa_default.default_policy
  id = "00p2v6mvcqYvXQE7g4x7"
}

import {
  to = okta_policy_rule_mfa.default_policy_default_rule
  id = "00p2v6mvcqYvXQE7g4x7/0pr2v6mvcrP0E2wcm4x7"
}


resource "okta_policy_mfa" "passwordless_requirement" {
  name            = "Passwordless Requirement"
  description     = ""
  status          = "ACTIVE"
  priority        = 1
  is_oie          = true
  groups_included = [data.okta_group.group_00ge1abom64vn6eI94x7.id]
  okta_email = {
    enroll = "REQUIRED"
  }
  google_otp = {
    enroll = "OPTIONAL"
  }
  okta_verify = {
    enroll = "REQUIRED"
  }
  okta_password = {
    enroll = "NOT_ALLOWED"
  }
  webauthn = {
    enroll = "OPTIONAL"
  }
}

resource "okta_policy_rule_mfa" "passwordless_requirement_default" {
  policy_id = okta_policy_mfa.passwordless_requirement.id
  name      = "Default"
  enroll    = "CHALLENGE"
  network_connection = "ANYWHERE"
  network_excludes   = null
  network_includes   = null
  priority  = 1
  status    = "ACTIVE"
  users_excluded = null
  depends_on = [ okta_policy_mfa.passwordless_requirement ]
}

resource "okta_policy_mfa_default" "default_policy" {
  is_oie = true
  okta_email = {
    enroll = "REQUIRED"
  }
  google_otp = {
    enroll = "OPTIONAL"
  }
  okta_verify = {
    enroll = "OPTIONAL"
  }
  okta_password = {
    enroll = "REQUIRED"
  }
  webauthn = {
    enroll = "OPTIONAL"
  }
}

resource "okta_policy_rule_mfa" "default_policy_default_rule" {
  policy_id = okta_policy_mfa_default.default_policy.id
  name      = "Default Rule"
  enroll    = "CHALLENGE"
  network_connection = "ANYWHERE"
  network_excludes   = null
  network_includes   = null
  priority  = 1
  status    = "ACTIVE"
  users_excluded = null
  depends_on = [ okta_policy_mfa_default.default_policy ]
}

You can then drop that file into your existing terraform repo.

Managing Password Policies & Rules

Additionally, I am unable to provide the password policy and rules we have set, but we will accomplish the same thing. Let’s import the configuration we have in Okta into a Terraform file automatically. This will query the API for all policies as a PASSWORD,

1
2
3
4
./bin/python3 main.py --full-domain $YOURDOMAIN --api-token $YOURAPITOKEN --terraform-fmt --output ./policy-password.tf
Terraform configuration generated and written to ./policy-password.tf
policy-password.tf
Formatted ./policy-password.tf successfully.

Which will automatically generate a file that looks like this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
data "okta_group" "group_00gq12s54WHn2SW3o4x6" {
  id = "00gq12s54WHn2SW3o4x6"
}

import {
  to = okta_policy_password.legacy_policy_00p1lg5ihrBeXuDl14x7
  id = "00p1lg5ihrBeXuDl14x7"
}

import {
  to = okta_policy_rule_password.legacy_rule_0pr1lg5ihsQNFvagv4x7
  id = "0pr1lg5ihsQNFvagv4x7"
}

import {
  to = okta_policy_rule_password.default_rule_0pr1lg5ihvzD5QZOl4x7
  id = "0pr1lg5ihvzD5QZOl4x7"
}

import {
  to = okta_policy_password_default.active_directory_policy_00p1lg5ihyidXVVwI4x7
  id = "00p1lg5ihyidXVVwI4x7"
}

import {
  to = okta_policy_rule_password.legacy_rule_0pr1lg5ii24K7hF164x7
  id = "0pr1lg5ii24K7hF164x7"
}

import {
  to = okta_policy_rule_password.default_rule_0pr1lg5ihz5ELUfXF4x7
  id = "0pr1lg5ihz5ELUfXF4x7"
}

import {
  to = okta_policy_password_default.default_policy_00p1lg5ihnvfxSAmX4x7
  id = "00p1lg5ihnvfxSAmX4x7"
}

import {
  to = okta_policy_rule_password.default_rule_0pr1lg5ihoeCxjUsJ4x7
  id = "0pr1lg5ihoeCxjUsJ4x7"
}

resource "okta_policy_password" "legacy_policy_00p1lg5ihrBeXuDl14x7" {
  name                           = "Legacy Policy"
  description                    = "The legacy policy contains any existing settings from the legacy password policy."
  status                         = "ACTIVE"
  priority                       = 1
  groups_included                = [data.okta_group.group_00gq12s54WHn2SW3o4x6.id]
  password_history_count         = 4
  password_min_length            = 8
  password_min_lowercase         = 1
  password_min_uppercase         = 1
  password_min_number            = 1
  password_min_symbol            = 0
  password_exclude_username      = true
  password_expire_warn_days      = 0
  password_min_age_minutes       = 0
  password_max_age_days          = 0
  password_max_lockout_attempts  = 10
  password_auto_unlock_minutes   = 0
  password_show_lockout_failures = false
  recovery_email_token           = 10080
}

resource "okta_policy_rule_password" "legacy_rule_0pr1lg5ihsQNFvagv4x7" {
  name               = "Legacy Rule"
  policy_id          = okta_policy_password.legacy_policy_00p1lg5ihrBeXuDl14x7.id
  priority           = 1
  status             = "ACTIVE"
  network_connection = "ANYWHERE"
  password_change    = "ALLOW"
  password_reset     = "ALLOW"
  password_unlock    = "DENY"
  users_excluded     = null
}

resource "okta_policy_rule_password" "default_rule_0pr1lg5ihvzD5QZOl4x7" {
  name               = "Default Rule"
  policy_id          = okta_policy_password.legacy_policy_00p1lg5ihrBeXuDl14x7.id
  priority           = 2
  status             = "ACTIVE"
  network_connection = "ANYWHERE"
  password_change    = "ALLOW"
  password_reset     = "ALLOW"
  password_unlock    = "DENY"
  users_excluded     = null
}

resource "okta_policy_password_default" "active_directory_policy_00p1lg5ihyidXVVwI4x7" {
  name            = "Active Directory Policy"
  description     = "The active directory policy contains settings that apply to users using delegated authentication from active directory integrations."
  status          = "ACTIVE"
  priority        = 2
  groups_included = [data.okta_group.group_00gq12s54WHn2SW3o4x6.id]
}

resource "okta_policy_rule_password" "legacy_rule_0pr1lg5ii24K7hF164x7" {
  name               = "Legacy Rule"
  policy_id          = okta_policy_password_default.active_directory_policy_00p1lg5ihyidXVVwI4x7.id
  priority           = 1
  status             = "ACTIVE"
  network_connection = "ANYWHERE"
  password_change    = "DENY"
  password_reset     = "DENY"
  password_unlock    = "DENY"
  users_excluded     = null
}

resource "okta_policy_rule_password" "default_rule_0pr1lg5ihz5ELUfXF4x7" {
  name               = "Default Rule"
  policy_id          = okta_policy_password_default.active_directory_policy_00p1lg5ihyidXVVwI4x7.id
  priority           = 2
  status             = "ACTIVE"
  network_connection = "ANYWHERE"
  password_change    = "DENY"
  password_reset     = "DENY"
  password_unlock    = "DENY"
  users_excluded     = null
}

resource "okta_policy_password_default" "default_policy_00p1lg5ihnvfxSAmX4x7" {
  name            = "Default Policy"
  description     = "The default policy applies in all situations if no other policy applies."
  status          = "ACTIVE"
  priority        = 3
  groups_included = [data.okta_group.group_00gq12s54WHn2SW3o4x6.id]
}

resource "okta_policy_rule_password" "default_rule_0pr1lg5ihoeCxjUsJ4x7" {
  name               = "Default Rule"
  policy_id          = okta_policy_password_default.default_policy_00p1lg5ihnvfxSAmX4x7.id
  priority           = 1
  status             = "ACTIVE"
  network_connection = "ANYWHERE"
  password_change    = "ALLOW"
  password_reset     = "ALLOW"
  password_unlock    = "DENY"
  users_excluded     = null
}

Managing Global Session Policies

And again, as above, using the policy-global-session-policies-generator, this will generate all of the global session configurations you have in Okta in to a Terraform file. Similar to the items above.

Managing Application Authentication Policies

Automatically creating all configurations

We will be using Python to generate terraform files for this. Key things to note here though, are:

  • This will create multiple terraform files, one for each authentication policy
  • This will run terraform fmt on all of the files created

I created a policy-auth_signon-dual_env generator, you can find a lot more information here. This will automate the creation of all policies & rules, however unlike the others, this one does not do interpolation - instead it will add a count = var.CONFIG == "prod" ? 1 : 0 (dependent on prod or test which you can change depending on your environment).

This will also add import blocks for policy or rule, so that the entire file will be a drop in replacement.

So as an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ ./bin/python3 main.py --prod-api-token $YOURAPIKEY --prod-subdomain $YOURDOMAIN --preview-api-token $YOURAPIKEY --preview-subdomain $YOURDOMAIN --dual
Using Okta domain: https://$YOURDOMAIN.oktapreview.com for environment: test
Generated Terraform file: test/zz_legacy_policy-okta_dashboard_test.tf
Generated Terraform file: test/zz_legacy_policy-okta_admin_console_test.tf
Generated Terraform file: test/zz_legacy_policy-microsoft_office_365_test.tf
Generated Terraform file: test/zz_legacy_policy-seamless_access_based_on_risk_context_test.tf
Generated Terraform file: test/zz_legacy_policy-password_only_test.tf
Generated Terraform file: test/zz_legacy_policy-one_factor_access_test.tf
Generated Terraform file: test/zz_legacy_policy-seamless_access_based_on_network_context_test.tf
Generated Terraform file: test/zz_legacy_policy-any_two_factors_test.tf
Generated Terraform file: test/zz_legacy_policy-classic_migrated_test.tf
Generated Terraform file: test/zz_legacy_policy-example_authentication_policy_no-dt_test.tf
Generated Terraform file: test/zz_legacy_policy-example_authentication_policy_dt_test.tf
Generated Terraform file: test/zz_legacy_policy-okta_workflows_policy_test.tf
Generated Terraform file: test/zz_legacy_policy-okta_end_user_settings_test.tf
Generated Terraform file: test/zz_legacy_policy-okta_account_management_policy_test.tf
Using Okta domain: https://$YOURDOMAIN.okta.com for environment: prod
Generated Terraform file: prod/zz_legacy_policy-okta_dashboard_prod.tf
Generated Terraform file: prod/zz_legacy_policy-okta_admin_console_prod.tf
Generated Terraform file: prod/zz_legacy_policy-microsoft_office_365_prod.tf
Generated Terraform file: prod/zz_legacy_policy-seamless_access_based_on_risk_context_prod.tf
Generated Terraform file: prod/zz_legacy_policy-password_only_prod.tf
Generated Terraform file: prod/zz_legacy_policy-one_factor_access_prod.tf
Generated Terraform file: prod/zz_legacy_policy-seamless_access_based_on_network_context_prod.tf
Generated Terraform file: prod/zz_legacy_policy-any_two_factors_prod.tf
Generated Terraform file: prod/zz_legacy_policy-classic_migrated_prod.tf
Generated Terraform file: prod/zz_legacy_policy-example_authentication_policy_no-dt_prod.tf
Generated Terraform file: prod/zz_legacy_policy-example_authentication_policy_dt_prod.tf
Generated Terraform file: prod/zz_legacy_policy-okta_workflows_policy_prod.tf
Generated Terraform file: prod/zz_legacy_policy-okta_end_user_settings_prod.tf
Generated Terraform file: prod/zz_legacy_policy-okta_account_management_policy_prod.tf

Which will generate a file that looks like this as an example:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
resource "okta_app_signon_policy" "policy_seamless_access_based_on_risk_context_test" {
  count = var.CONFIG == "test" ? 1 : 0
  name = "Seamless access based on risk context"
  description = "Allow seamless log in with Okta FastPass for low risk authentication scenarios with enhanced security for medium and high risk."
}

import {
  for_each = var.CONFIG == "test" ? toset(["test"]) : []
  to = okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0]
  id = "rste00mls6FX82hSu4x7"
}

resource "okta_app_signon_policy_rule" "rule_seamless_access_based_on_risk_context_low_risk_test" {
  count = var.CONFIG == "test" ? 1 : 0
  policy_id = okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0].id
  depends_on = [okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0]]
  name      = "Low risk"
  inactivity_period = ""
  lifecycle {
    ignore_changes = [inactivity_period]
  }
  status = "ACTIVE"
  access = "ALLOW"
  factor_mode = "1FA"
  re_authentication_frequency = "PT12H"
  type = "ASSURANCE"
  constraints = [
    jsonencode({"possession":{"required":true,"userPresence":"OPTIONAL"}}),
  ]
  network_connection = "ANYWHERE"
  risk_score = "LOW"
  priority = 0
}

import {
  for_each = var.CONFIG == "test" ? toset(["test"]) : []
  to = okta_app_signon_policy_rule.rule_seamless_access_based_on_risk_context_low_risk_test[0]
  id = "rste00mls6FX82hSu4x7/rule00mls71I2661B4x7"
}

resource "okta_app_signon_policy_rule" "rule_seamless_access_based_on_risk_context_med_risk_test" {
  count = var.CONFIG == "test" ? 1 : 0
  policy_id = okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0].id
  depends_on = [okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0]]
  name      = "Med risk"
  inactivity_period = ""
  lifecycle {
    ignore_changes = [inactivity_period]
  }
  status = "ACTIVE"
  access = "ALLOW"
  factor_mode = "1FA"
  re_authentication_frequency = "PT12H"
  type = "ASSURANCE"
  constraints = [
    jsonencode({"possession":{"required":true,"userPresence":"OPTIONAL"}}),
  ]
  network_connection = "ANYWHERE"
  risk_score = "MEDIUM"
  priority = 1
}

import {
  for_each = var.CONFIG == "test" ? toset(["test"]) : []
  to = okta_app_signon_policy_rule.rule_seamless_access_based_on_risk_context_med_risk_test[0]
  id = "rste00mls6FX82hSu4x7/rule00mls8jLD2eXq4x7"
}

resource "okta_app_signon_policy_rule" "rule_seamless_access_based_on_risk_context_high_risk_test" {
  count = var.CONFIG == "test" ? 1 : 0
  policy_id = okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0].id
  depends_on = [okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0]]
  name      = "High risk"
  inactivity_period = ""
  lifecycle {
    ignore_changes = [inactivity_period]
  }
  status = "ACTIVE"
  access = "ALLOW"
  factor_mode = "1FA"
  re_authentication_frequency = "PT12H"
  type = "ASSURANCE"
  constraints = [
    jsonencode({"possession":{"required":true,"userPresence":"OPTIONAL"}}),
  ]
  network_connection = "ANYWHERE"
  risk_score = "HIGH"
  priority = 2
}

import {
  for_each = var.CONFIG == "test" ? toset(["test"]) : []
  to = okta_app_signon_policy_rule.rule_seamless_access_based_on_risk_context_high_risk_test[0]
  id = "rste00mls6FX82hSu4x7/rule00mls9Mn7AaVn4x7"
}

resource "okta_app_signon_policy_rule" "rule_seamless_access_based_on_risk_context_catch-all_rule_test" {
  count = var.CONFIG == "test" ? 1 : 0
  policy_id = okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0].id
  depends_on = [okta_app_signon_policy.policy_seamless_access_based_on_risk_context_test[0]]
  name      = "Catch-all Rule"
  status = "ACTIVE"
  access = "DENY"
  factor_mode = "2FA"
  re_authentication_frequency = "PT12H"
  type = "ASSURANCE"
  constraints = [
    jsonencode({"possession":{"required":true,"deviceBound":"REQUIRED"}}),
  ]
  priority = 99
  lifecycle {
    ignore_changes = [
      network_connection,
      network_excludes,
      network_includes,
      platform_include,
      custom_expression,
      inactivity_period,
      device_is_registered,
      device_is_managed,
      users_excluded,
      users_included,
      groups_excluded,
      groups_included,
      user_types_excluded,
      user_types_included,
      re_authentication_frequency,
      factor_mode,
      constraints,
    ]
  }
}

import {
  for_each = var.CONFIG == "test" ? toset(["test"]) : []
  to = okta_app_signon_policy_rule.rule_seamless_access_based_on_risk_context_catch-all_rule_test[0]
  id = "rste00mls6FX82hSu4x7/rule00mlsb2fBXzSE4x7"
}

Creating Custom Configurations - Importing the Okta Default / “Any Two Factors” Policy

Note

There seem to be several bugs with this resource/provider, when it comes to Auth and Sign On policies. Currently, we have run into issues with:

  • Auth Chain
  • Default or Okta Created Authentication Policies

You need to do this in steps though, as if you do all of them at the same time, it will cause issues.

First off, we previously had renamed the policies to be something more relevant with what we used the policy for. The default labeled policy must be named Any two factors, and must have a description of Require two factors to access., otherwise Terraform will fail to import the resource. Complaining of one of two things:

  • 403 Forbidden
  • Cannot modify the priority attribute because it is read-only.

The same things happens in Postman:

Postman Error - thanks @crowt

But this behavior does not show up in the Admin GUI Console, where you can edit the Name and Description to whatever you would like. It seems like there is a provider bug already filed. You can find a discussion about this on MacAdmins.

But, we can work around that via a hybrid of Importing the resource to Terraform, and then changing the Name using ClickOps with what will eventually be the Terraform managed resource name, by implementing a lifecycle and ignore_change statements. I added the ignore_change statements after the import had succeeded for both the policy and the rule.

So Step 1:

  1. Obtain the Policy IDs, you can easily obtain this by grabbing the URL from a browser, or by using an API Tool (like Postman)
  2. Add the Locals data
  3. PR your feature branch to preview
  4. Run Terraform Plan & Apply for the Preview Environment (based on the setup that I have covered over the past 6 parts)
  5. PR preview to main
  6. Run Terraform Plan & Apply for the Production Environment
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Step 1
# ## TODO: Adjust for Production and run during first run for import
locals {
  app_signon_policy_id = {
    okta-preview = "previewid" # Preview
    okta-production = "prodid" # Prod
  }[terraform.workspace]
}

import {
  to = okta_app_signon_policy.default_standard
  id = local.app_signon_policy_id
}

# App Sign-On Policy
resource "okta_app_signon_policy" "default_standard" {
  name        = "Any two factors"
  description = "Require two factors to access."
  lifecycle {
    create_before_destroy = true
    ignore_changes = [
      name,
      description
    ]
  }
}

Once all of this is done, you can then go in and rename the Authentication Policy in Okta via ClickOps, and when the bug is fixed, plan to fix this via the Terraform Provider.

Now we move on to Step 2, importing the Catch-All Rule:

  1. Obtain the Rule IDs, you can more easily with an API Tool (like Postman) or by opening the Developer Tools and switching to the Network tab in Chrome or Firefox
  2. Add the Locals data
  3. PR your feature branch to preview
  4. Run Terraform Plan & Apply for the Preview Environment (based on the setup that I have covered over the past 6 parts)
  5. PR preview to main
  6. Run Terraform Plan & Apply for the Production Environment
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
locals {
  catch_all_rule_id = {
    okta-preview = "rulpreviewenv" # Preview
    okta-production = "rulprodenv" # Prod
  }[terraform.workspace]
}

import {
  to = okta_app_signon_policy_rule.default_standard_catch_all_rule
  id = "${local.app_signon_policy_id}/${local.catch_all_rule_id}"
}

resource "okta_app_signon_policy_rule" "default_standard_catch_all_rule" {
  name      = "Catch-all Rule"
  policy_id = okta_app_signon_policy.default_standard.id
  access    = "DENY"
  priority  = 99

  constraints = [
    jsonencode({
      "knowledge" : {
        "types" : ["password"],
        "reauthenticateIn" : "PT43800H"
      }
    })
  ]
  lifecycle {
    create_before_destroy = true
    ignore_changes = [
      inactivity_period,
      network_connection,
      constraints,
      factor_mode,
      re_authentication_frequency
    ]
  }

  depends_on = [okta_app_signon_policy.default_standard]
}

This should properly allow for the Drift Configuration, to function if Access is ever re-configured by someone, and revert the change.

Adding Custom Rules via Terraform to the Default Policy

Now that we have the default auth policies in state and managed, we now need to add custom rules:

We will use local blocks, to manage the list and priority of the rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
locals {
  standard_signon_policy_rules = [
    {
      name            = "Example"
      access          = "ALLOW"
      groups_included = [okta_group.groups["auth-webauthn_fido2-bypass"].id]
      constraints = jsonencode({
        "possession" : {
          "required" : true,
          "deviceBound" : "REQUIRED"
          "reauthenticateIn" : "PT0S"
        }
      })
    }
  ]
}

You can create multiple rules as needed, and we will use a for_each loop to create the rules automatically, using any naming scheme.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
locals {
  # Extract all static priorities from rules (only ones explicitly set)
  static_priorities = {
    for rule in local.standard_signon_policy_rules :
    rule.name => rule.priority
    if try(rule.priority, null) != null
  }

  # Sort rules by explicit priority (if exists) or order in the list
  sorted_rules = sort([
    for rule in local.standard_signon_policy_rules : {
      name     = rule.name
      priority = try(rule.priority, null)
    }
  ], "priority")

  # Assign incremental priorities, ensuring static values stay in place and max priority < 95
  dynamic_priorities = {
    for i, rule in local.sorted_rules :
    rule.name => (
      try(rule.priority, null) != null
      ? rule.priority # Use the static priority if it exists
      : min( # Ensure we do not exceed 95
          max([for p in values(local.static_priorities) : p]...) + 1 + i,
          95
        )
    )
  }
}

# Resource Block for Sign-On Policy Rules
resource "okta_app_signon_policy_rule" "standard_signon_rules" {
  for_each = { for rule in local.standard_signon_policy_rules : rule.name => rule }

  name      = "tf-${each.value.name}"
  policy_id = okta_app_signon_policy.default_standard.id
  access    = each.value.access

  # Assign dynamically computed priority or use static priority if defined
  priority  = local.dynamic_priorities[each.value.name]

  groups_included      = try(each.value.groups_included, null)
  device_is_registered = try(each.value.device_is_registered, null)
  device_is_managed    = try(each.value.device_is_managed, null)
  factor_mode          = try(each.value.factor_mode, null)
  constraints          = [each.value.constraints]

  depends_on = [
    okta_app_signon_policy.default_standard,
    okta_app_signon_policy_rule.default_standard_catch_all_rule
  ]
}

What I would recommend:

Is iterating over the rules you have and put them in a list, so that for each item in the list, is automatically recorded and kept up to date, and that moves and shifts for each (hopefully not frequent) update of the list. Additionally, when you need to leave certain options blank or excluded, the API will fail with various errors, so it is better to null the value and not pass it through in the code wherever possible.

Automated Unit Testing of Authentication Policies

Okta has a tool called “Access Testing”, but I have seen little usage of the actual API behind it being used outside of Okta’s Access Testing Tool. What would happen if we were to use this as part of our Terraform Plan / Apply to validate that the Authentication Policies we have in place all pass, based on the requirements we have set out in the policy. This is currently in the works, but I will see if I can share it when I am done. It should be an exciting option when finished. Need to verify that particular VIP, C-Staff, Service Accounts, or Employees can access a resource before a policy change is pushed out? Done; quickly test all of that in your staging environment first and then push to prod after completing the unit test.

And that is it

Thanks for taking the time to read this, be on the lookout next week for another blog post about automating and managing User & Group Schemas, and creating dependency files for other teams.

A lot will be covered over the next several parts, which sums up how we have terraformed certain pieces of our Okta environment. If you have questions and are looking for a community resource, I would heavily recommend reaching out to #okta-terraform on MacAdmins, as I would say at least 30% (note, I made this statistic up) of the organizations using Terraform hang out in this channel. Otherwise, you can always find an alternative unofficial community for assistance or ideas.

Licensed under CC BY-NC-SA 4.0
Last updated on February 14, 2025 at 11:30 CET
 
Thanks for stopping by!
Built with Hugo